import {provideKeyBindings} from './shortcuts';
-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";
+import {MarkdownEditorEventMap} from "./dom-handlers";
/**
- * Initiate the codemirror instance for the MarkDown editor.
+ * Initiate the codemirror instance for the Markdown editor.
*/
-export function init(editor: MarkdownEditor, Code: CodeModule): EditorView {
+export function init(editor: MarkdownEditor, Code: CodeModule, domEventHandlers: MarkdownEditorEventMap): EditorView {
function onViewUpdate(v: ViewUpdate) {
if (v.docChanged) {
editor.actions.updateAndRender();
}
}
- const onScrollDebounced = debounce(editor.actions.syncDisplayPosition.bind(editor.actions), 100, false);
- let syncActive = editor.settings.get('scrollSync');
- editor.settings.onChange('scrollSync', val => {
- syncActive = val;
- });
-
- const domEventHandlers = {
- // Handle scroll to sync display view
- scroll: (event: Event) => syncActive && onScrollDebounced(event),
- // Handle image & content drag n drop
- drop: (event: DragEvent) => {
- if (!event.dataTransfer) {
- return;
- }
-
- const templateId = event.dataTransfer.getData('bookstack/template');
- if (templateId) {
- event.preventDefault();
- editor.actions.insertTemplate(templateId, event.pageX, event.pageY);
- }
-
- const clipboard = new Clipboard(event.dataTransfer);
- const clipboardImages = clipboard.getImages();
- if (clipboardImages.length > 0) {
- event.stopPropagation();
- event.preventDefault();
- editor.actions.insertClipboardImages(clipboardImages, event.pageX, event.pageY);
- }
- },
- // Handle dragover event to allow as drop-target in chrome
- dragover: (event: DragEvent) => {
- event.preventDefault();
- },
- // Handle image paste
- paste: (event: ClipboardEvent) => {
- if (!event.clipboardData) {
- return;
- }
-
- const clipboard = new Clipboard(event.clipboardData);
-
- // Don't handle the event ourselves if no items exist of contains table-looking data
- if (!clipboard.hasItems() || clipboard.containsTabularData()) {
- return;
- }
-
- const images = clipboard.getImages();
- for (const image of images) {
- editor.actions.uploadImage(image);
- }
- },
- };
const cm = Code.markdownEditor(
editor.config.inputEl,
--- /dev/null
+import {Clipboard} from "../services/clipboard";
+import {MarkdownEditor} from "./index.mjs";
+import {debounce} from "../services/util";
+
+
+export type MarkdownEditorEventMap = Record<string, (event: any) => void>;
+
+export function getMarkdownDomEventHandlers(editor: MarkdownEditor): MarkdownEditorEventMap {
+
+ const onScrollDebounced = debounce(editor.actions.syncDisplayPosition.bind(editor.actions), 100, false);
+ let syncActive = editor.settings.get('scrollSync');
+ editor.settings.onChange('scrollSync', val => {
+ syncActive = val;
+ });
+
+ return {
+ // Handle scroll to sync display view
+ scroll: (event: Event) => syncActive && onScrollDebounced(event),
+ // Handle image & content drag n drop
+ drop: (event: DragEvent) => {
+ if (!event.dataTransfer) {
+ return;
+ }
+
+ const templateId = event.dataTransfer.getData('bookstack/template');
+ if (templateId) {
+ event.preventDefault();
+ editor.actions.insertTemplate(templateId, event.pageX, event.pageY);
+ }
+
+ const clipboard = new Clipboard(event.dataTransfer);
+ const clipboardImages = clipboard.getImages();
+ if (clipboardImages.length > 0) {
+ event.stopPropagation();
+ event.preventDefault();
+ editor.actions.insertClipboardImages(clipboardImages, event.pageX, event.pageY);
+ }
+ },
+ // Handle dragover event to allow as drop-target in chrome
+ dragover: (event: DragEvent) => {
+ event.preventDefault();
+ },
+ // Handle image paste
+ paste: (event: ClipboardEvent) => {
+ if (!event.clipboardData) {
+ return;
+ }
+
+ const clipboard = new Clipboard(event.clipboardData);
+
+ // Don't handle the event ourselves if no items exist of contains table-looking data
+ if (!clipboard.hasItems() || clipboard.containsTabularData()) {
+ return;
+ }
+
+ const images = clipboard.getImages();
+ for (const image of images) {
+ editor.actions.uploadImage(image);
+ }
+ },
+ };
+}
\ No newline at end of file
import {CodeModule} from "../global";
import {MarkdownEditorInput} from "./inputs/interface";
import {CodemirrorInput} from "./inputs/codemirror";
+import {TextareaInput} from "./inputs/textarea";
+import {provideShortcutMap} from "./shortcuts";
+import {getMarkdownDomEventHandlers} from "./dom-handlers";
export interface MarkdownEditorConfig {
pageId: string;
* Initiate a new Markdown editor instance.
*/
export async function init(config: MarkdownEditorConfig): Promise<MarkdownEditor> {
- const Code = await window.importVersioned('code') as CodeModule;
+ // const Code = await window.importVersioned('code') as CodeModule;
const editor: MarkdownEditor = {
config,
editor.actions = new Actions(editor);
editor.display = new Display(editor);
- const codeMirror = initCodemirror(editor, Code);
- editor.input = new CodemirrorInput(codeMirror);
+ const eventHandlers = getMarkdownDomEventHandlers(editor);
+ // TODO - Switching
+ // const codeMirror = initCodemirror(editor, Code);
+ // editor.input = new CodemirrorInput(codeMirror);
+ editor.input = new TextareaInput(
+ config.inputEl,
+ provideShortcutMap(editor),
+ eventHandlers
+ );
+
+ // window.devinput = editor.input;
listenToCommonEvents(editor);
--- /dev/null
+import {MarkdownEditorInput, MarkdownEditorInputSelection} from "./interface";
+import {MarkdownEditorShortcutMap} from "../shortcuts";
+import {MarkdownEditorEventMap} from "../dom-handlers";
+
+
+export class TextareaInput implements MarkdownEditorInput {
+
+ protected input: HTMLTextAreaElement;
+ protected shortcuts: MarkdownEditorShortcutMap;
+ protected events: MarkdownEditorEventMap;
+
+ constructor(input: HTMLTextAreaElement, shortcuts: MarkdownEditorShortcutMap, events: MarkdownEditorEventMap) {
+ this.input = input;
+ this.shortcuts = shortcuts;
+ this.events = events;
+
+ this.onKeyDown = this.onKeyDown.bind(this);
+ this.configureListeners();
+ }
+
+ configureListeners(): void {
+ // TODO - Teardown handling
+ this.input.addEventListener('keydown', this.onKeyDown);
+
+ for (const [name, listener] of Object.entries(this.events)) {
+ this.input.addEventListener(name, listener);
+ }
+ }
+
+ onKeyDown(e: KeyboardEvent) {
+ const isApple = navigator.platform.startsWith("Mac") || navigator.platform === "iPhone";
+ const keyParts = [
+ e.shiftKey ? 'Shift' : null,
+ isApple && e.metaKey ? 'Mod' : null,
+ !isApple && e.ctrlKey ? 'Mod' : null,
+ e.key,
+ ];
+
+ const keyString = keyParts.filter(Boolean).join('-');
+ if (this.shortcuts[keyString]) {
+ e.preventDefault();
+ this.shortcuts[keyString]();
+ }
+ }
+
+ appendText(text: string): void {
+ this.input.value += `\n${text}`;
+ }
+
+ coordsToSelection(x: number, y: number): MarkdownEditorInputSelection {
+ // TODO
+ return this.getSelection();
+ }
+
+ focus(): void {
+ this.input.focus();
+ }
+
+ getLineRangeFromPosition(position: number): MarkdownEditorInputSelection {
+ const lines = this.getText().split('\n');
+ let lineStart = 0;
+ for (let i = 0; i < lines.length; i++) {
+ const line = lines[i];
+ const newEnd = lineStart + line.length + 1;
+ if (position < newEnd) {
+ return {from: lineStart, to: newEnd};
+ }
+ lineStart = newEnd;
+ }
+
+ return {from: 0, to: 0};
+ }
+
+ getLineText(lineIndex: number): string {
+ const text = this.getText();
+ const lines = text.split("\n");
+ return lines[lineIndex] || '';
+ }
+
+ getSelection(): MarkdownEditorInputSelection {
+ return {from: this.input.selectionStart, to: this.input.selectionEnd};
+ }
+
+ getSelectionText(selection?: MarkdownEditorInputSelection): string {
+ const text = this.getText();
+ const range = selection || this.getSelection();
+ return text.slice(range.from, range.to);
+ }
+
+ getText(): string {
+ return this.input.value;
+ }
+
+ getTextAboveView(): string {
+ const scrollTop = this.input.scrollTop;
+ const computedStyles = window.getComputedStyle(this.input);
+ const lines = this.getText().split('\n');
+ const paddingTop = Number(computedStyles.paddingTop.replace('px', ''));
+ const paddingBottom = Number(computedStyles.paddingBottom.replace('px', ''));
+
+ const avgLineHeight = (this.input.scrollHeight - paddingBottom - paddingTop) / lines.length;
+ const roughLinePos = Math.max(Math.floor((scrollTop - paddingTop) / avgLineHeight), 0);
+ const linesAbove = this.getText().split('\n').slice(0, roughLinePos);
+ return linesAbove.join('\n');
+ }
+
+ searchForLineContaining(text: string): MarkdownEditorInputSelection | null {
+ const textPosition = this.getText().indexOf(text);
+ if (textPosition > -1) {
+ return this.getLineRangeFromPosition(textPosition);
+ }
+
+ return null;
+ }
+
+ setSelection(selection: MarkdownEditorInputSelection, scrollIntoView: boolean): void {
+ this.input.selectionStart = selection.from;
+ this.input.selectionEnd = selection.to;
+ }
+
+ setText(text: string, selection?: MarkdownEditorInputSelection): void {
+ this.input.value = text;
+ if (selection) {
+ this.setSelection(selection, false);
+ }
+ }
+
+ spliceText(from: number, to: number, newText: string, selection: Partial<MarkdownEditorInputSelection> | null): void {
+ const text = this.getText();
+ const updatedText = text.slice(0, from) + newText + text.slice(to);
+ this.setText(updatedText);
+ if (selection && selection.from) {
+ const newSelection = {from: selection.from, to: selection.to || selection.from};
+ this.setSelection(newSelection, false);
+ }
+ }
+}
\ No newline at end of file
import {MarkdownEditor} from "./index.mjs";
import {KeyBinding} from "@codemirror/view";
+export type MarkdownEditorShortcutMap = Record<string, () => void>;
+
/**
* Provide shortcuts for the editor instance.
*/
-function provide(editor: MarkdownEditor): Record<string, () => void> {
- const shortcuts: Record<string, () => void> = {};
+export function provideShortcutMap(editor: MarkdownEditor): MarkdownEditorShortcutMap {
+ const shortcuts: MarkdownEditorShortcutMap = {};
// Insert Image shortcut
shortcuts['Shift-Mod-i'] = () => editor.actions.insertImage();
* Get the editor shortcuts in CodeMirror keybinding format.
*/
export function provideKeyBindings(editor: MarkdownEditor): KeyBinding[] {
- const shortcuts = provide(editor);
+ const shortcuts = provideShortcutMap(editor);
const keyBindings = [];
const wrapAction = (action: ()=>void) => () => {
padding: vars.$xs vars.$m;
color: #444;
border-radius: 0;
+ height: 100%;
+ font-size: 14px;
+ line-height: 1.2;
max-height: 100%;
flex: 1;
border: 0;