Skip to content

Commit 6b4b500

Browse files
committed
MD Editor: Added plaintext input implementation
1 parent 5ffec2c commit 6b4b500

File tree

6 files changed

+225
-62
lines changed

6 files changed

+225
-62
lines changed

resources/js/markdown/codemirror.ts

Lines changed: 3 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,72 +1,19 @@
11
import {provideKeyBindings} from './shortcuts';
2-
import {debounce} from '../services/util';
3-
import {Clipboard} from '../services/clipboard';
42
import {EditorView, ViewUpdate} from "@codemirror/view";
53
import {MarkdownEditor} from "./index.mjs";
64
import {CodeModule} from "../global";
5+
import {MarkdownEditorEventMap} from "./dom-handlers";
76

87
/**
9-
* Initiate the codemirror instance for the MarkDown editor.
8+
* Initiate the codemirror instance for the Markdown editor.
109
*/
11-
export function init(editor: MarkdownEditor, Code: CodeModule): EditorView {
10+
export function init(editor: MarkdownEditor, Code: CodeModule, domEventHandlers: MarkdownEditorEventMap): EditorView {
1211
function onViewUpdate(v: ViewUpdate) {
1312
if (v.docChanged) {
1413
editor.actions.updateAndRender();
1514
}
1615
}
1716

18-
const onScrollDebounced = debounce(editor.actions.syncDisplayPosition.bind(editor.actions), 100, false);
19-
let syncActive = editor.settings.get('scrollSync');
20-
editor.settings.onChange('scrollSync', val => {
21-
syncActive = val;
22-
});
23-
24-
const domEventHandlers = {
25-
// Handle scroll to sync display view
26-
scroll: (event: Event) => syncActive && onScrollDebounced(event),
27-
// Handle image & content drag n drop
28-
drop: (event: DragEvent) => {
29-
if (!event.dataTransfer) {
30-
return;
31-
}
32-
33-
const templateId = event.dataTransfer.getData('bookstack/template');
34-
if (templateId) {
35-
event.preventDefault();
36-
editor.actions.insertTemplate(templateId, event.pageX, event.pageY);
37-
}
38-
39-
const clipboard = new Clipboard(event.dataTransfer);
40-
const clipboardImages = clipboard.getImages();
41-
if (clipboardImages.length > 0) {
42-
event.stopPropagation();
43-
event.preventDefault();
44-
editor.actions.insertClipboardImages(clipboardImages, event.pageX, event.pageY);
45-
}
46-
},
47-
// Handle dragover event to allow as drop-target in chrome
48-
dragover: (event: DragEvent) => {
49-
event.preventDefault();
50-
},
51-
// Handle image paste
52-
paste: (event: ClipboardEvent) => {
53-
if (!event.clipboardData) {
54-
return;
55-
}
56-
57-
const clipboard = new Clipboard(event.clipboardData);
58-
59-
// Don't handle the event ourselves if no items exist of contains table-looking data
60-
if (!clipboard.hasItems() || clipboard.containsTabularData()) {
61-
return;
62-
}
63-
64-
const images = clipboard.getImages();
65-
for (const image of images) {
66-
editor.actions.uploadImage(image);
67-
}
68-
},
69-
};
7017

7118
const cm = Code.markdownEditor(
7219
editor.config.inputEl,
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import {Clipboard} from "../services/clipboard";
2+
import {MarkdownEditor} from "./index.mjs";
3+
import {debounce} from "../services/util";
4+
5+
6+
export type MarkdownEditorEventMap = Record<string, (event: any) => void>;
7+
8+
export function getMarkdownDomEventHandlers(editor: MarkdownEditor): MarkdownEditorEventMap {
9+
10+
const onScrollDebounced = debounce(editor.actions.syncDisplayPosition.bind(editor.actions), 100, false);
11+
let syncActive = editor.settings.get('scrollSync');
12+
editor.settings.onChange('scrollSync', val => {
13+
syncActive = val;
14+
});
15+
16+
return {
17+
// Handle scroll to sync display view
18+
scroll: (event: Event) => syncActive && onScrollDebounced(event),
19+
// Handle image & content drag n drop
20+
drop: (event: DragEvent) => {
21+
if (!event.dataTransfer) {
22+
return;
23+
}
24+
25+
const templateId = event.dataTransfer.getData('bookstack/template');
26+
if (templateId) {
27+
event.preventDefault();
28+
editor.actions.insertTemplate(templateId, event.pageX, event.pageY);
29+
}
30+
31+
const clipboard = new Clipboard(event.dataTransfer);
32+
const clipboardImages = clipboard.getImages();
33+
if (clipboardImages.length > 0) {
34+
event.stopPropagation();
35+
event.preventDefault();
36+
editor.actions.insertClipboardImages(clipboardImages, event.pageX, event.pageY);
37+
}
38+
},
39+
// Handle dragover event to allow as drop-target in chrome
40+
dragover: (event: DragEvent) => {
41+
event.preventDefault();
42+
},
43+
// Handle image paste
44+
paste: (event: ClipboardEvent) => {
45+
if (!event.clipboardData) {
46+
return;
47+
}
48+
49+
const clipboard = new Clipboard(event.clipboardData);
50+
51+
// Don't handle the event ourselves if no items exist of contains table-looking data
52+
if (!clipboard.hasItems() || clipboard.containsTabularData()) {
53+
return;
54+
}
55+
56+
const images = clipboard.getImages();
57+
for (const image of images) {
58+
editor.actions.uploadImage(image);
59+
}
60+
},
61+
};
62+
}

resources/js/markdown/index.mts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ import {init as initCodemirror} from './codemirror';
77
import {CodeModule} from "../global";
88
import {MarkdownEditorInput} from "./inputs/interface";
99
import {CodemirrorInput} from "./inputs/codemirror";
10+
import {TextareaInput} from "./inputs/textarea";
11+
import {provideShortcutMap} from "./shortcuts";
12+
import {getMarkdownDomEventHandlers} from "./dom-handlers";
1013

1114
export interface MarkdownEditorConfig {
1215
pageId: string;
@@ -31,7 +34,7 @@ export interface MarkdownEditor {
3134
* Initiate a new Markdown editor instance.
3235
*/
3336
export async function init(config: MarkdownEditorConfig): Promise<MarkdownEditor> {
34-
const Code = await window.importVersioned('code') as CodeModule;
37+
// const Code = await window.importVersioned('code') as CodeModule;
3538

3639
const editor: MarkdownEditor = {
3740
config,
@@ -42,8 +45,17 @@ export async function init(config: MarkdownEditorConfig): Promise<MarkdownEditor
4245
editor.actions = new Actions(editor);
4346
editor.display = new Display(editor);
4447

45-
const codeMirror = initCodemirror(editor, Code);
46-
editor.input = new CodemirrorInput(codeMirror);
48+
const eventHandlers = getMarkdownDomEventHandlers(editor);
49+
// TODO - Switching
50+
// const codeMirror = initCodemirror(editor, Code);
51+
// editor.input = new CodemirrorInput(codeMirror);
52+
editor.input = new TextareaInput(
53+
config.inputEl,
54+
provideShortcutMap(editor),
55+
eventHandlers
56+
);
57+
58+
// window.devinput = editor.input;
4759

4860
listenToCommonEvents(editor);
4961

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
import {MarkdownEditorInput, MarkdownEditorInputSelection} from "./interface";
2+
import {MarkdownEditorShortcutMap} from "../shortcuts";
3+
import {MarkdownEditorEventMap} from "../dom-handlers";
4+
5+
6+
export class TextareaInput implements MarkdownEditorInput {
7+
8+
protected input: HTMLTextAreaElement;
9+
protected shortcuts: MarkdownEditorShortcutMap;
10+
protected events: MarkdownEditorEventMap;
11+
12+
constructor(input: HTMLTextAreaElement, shortcuts: MarkdownEditorShortcutMap, events: MarkdownEditorEventMap) {
13+
this.input = input;
14+
this.shortcuts = shortcuts;
15+
this.events = events;
16+
17+
this.onKeyDown = this.onKeyDown.bind(this);
18+
this.configureListeners();
19+
}
20+
21+
configureListeners(): void {
22+
// TODO - Teardown handling
23+
this.input.addEventListener('keydown', this.onKeyDown);
24+
25+
for (const [name, listener] of Object.entries(this.events)) {
26+
this.input.addEventListener(name, listener);
27+
}
28+
}
29+
30+
onKeyDown(e: KeyboardEvent) {
31+
const isApple = navigator.platform.startsWith("Mac") || navigator.platform === "iPhone";
32+
const keyParts = [
33+
e.shiftKey ? 'Shift' : null,
34+
isApple && e.metaKey ? 'Mod' : null,
35+
!isApple && e.ctrlKey ? 'Mod' : null,
36+
e.key,
37+
];
38+
39+
const keyString = keyParts.filter(Boolean).join('-');
40+
if (this.shortcuts[keyString]) {
41+
e.preventDefault();
42+
this.shortcuts[keyString]();
43+
}
44+
}
45+
46+
appendText(text: string): void {
47+
this.input.value += `\n${text}`;
48+
}
49+
50+
coordsToSelection(x: number, y: number): MarkdownEditorInputSelection {
51+
// TODO
52+
return this.getSelection();
53+
}
54+
55+
focus(): void {
56+
this.input.focus();
57+
}
58+
59+
getLineRangeFromPosition(position: number): MarkdownEditorInputSelection {
60+
const lines = this.getText().split('\n');
61+
let lineStart = 0;
62+
for (let i = 0; i < lines.length; i++) {
63+
const line = lines[i];
64+
const newEnd = lineStart + line.length + 1;
65+
if (position < newEnd) {
66+
return {from: lineStart, to: newEnd};
67+
}
68+
lineStart = newEnd;
69+
}
70+
71+
return {from: 0, to: 0};
72+
}
73+
74+
getLineText(lineIndex: number): string {
75+
const text = this.getText();
76+
const lines = text.split("\n");
77+
return lines[lineIndex] || '';
78+
}
79+
80+
getSelection(): MarkdownEditorInputSelection {
81+
return {from: this.input.selectionStart, to: this.input.selectionEnd};
82+
}
83+
84+
getSelectionText(selection?: MarkdownEditorInputSelection): string {
85+
const text = this.getText();
86+
const range = selection || this.getSelection();
87+
return text.slice(range.from, range.to);
88+
}
89+
90+
getText(): string {
91+
return this.input.value;
92+
}
93+
94+
getTextAboveView(): string {
95+
const scrollTop = this.input.scrollTop;
96+
const computedStyles = window.getComputedStyle(this.input);
97+
const lines = this.getText().split('\n');
98+
const paddingTop = Number(computedStyles.paddingTop.replace('px', ''));
99+
const paddingBottom = Number(computedStyles.paddingBottom.replace('px', ''));
100+
101+
const avgLineHeight = (this.input.scrollHeight - paddingBottom - paddingTop) / lines.length;
102+
const roughLinePos = Math.max(Math.floor((scrollTop - paddingTop) / avgLineHeight), 0);
103+
const linesAbove = this.getText().split('\n').slice(0, roughLinePos);
104+
return linesAbove.join('\n');
105+
}
106+
107+
searchForLineContaining(text: string): MarkdownEditorInputSelection | null {
108+
const textPosition = this.getText().indexOf(text);
109+
if (textPosition > -1) {
110+
return this.getLineRangeFromPosition(textPosition);
111+
}
112+
113+
return null;
114+
}
115+
116+
setSelection(selection: MarkdownEditorInputSelection, scrollIntoView: boolean): void {
117+
this.input.selectionStart = selection.from;
118+
this.input.selectionEnd = selection.to;
119+
}
120+
121+
setText(text: string, selection?: MarkdownEditorInputSelection): void {
122+
this.input.value = text;
123+
if (selection) {
124+
this.setSelection(selection, false);
125+
}
126+
}
127+
128+
spliceText(from: number, to: number, newText: string, selection: Partial<MarkdownEditorInputSelection> | null): void {
129+
const text = this.getText();
130+
const updatedText = text.slice(0, from) + newText + text.slice(to);
131+
this.setText(updatedText);
132+
if (selection && selection.from) {
133+
const newSelection = {from: selection.from, to: selection.to || selection.from};
134+
this.setSelection(newSelection, false);
135+
}
136+
}
137+
}

resources/js/markdown/shortcuts.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import {MarkdownEditor} from "./index.mjs";
22
import {KeyBinding} from "@codemirror/view";
33

4+
export type MarkdownEditorShortcutMap = Record<string, () => void>;
5+
46
/**
57
* Provide shortcuts for the editor instance.
68
*/
7-
function provide(editor: MarkdownEditor): Record<string, () => void> {
8-
const shortcuts: Record<string, () => void> = {};
9+
export function provideShortcutMap(editor: MarkdownEditor): MarkdownEditorShortcutMap {
10+
const shortcuts: MarkdownEditorShortcutMap = {};
911

1012
// Insert Image shortcut
1113
shortcuts['Shift-Mod-i'] = () => editor.actions.insertImage();
@@ -45,7 +47,7 @@ function provide(editor: MarkdownEditor): Record<string, () => void> {
4547
* Get the editor shortcuts in CodeMirror keybinding format.
4648
*/
4749
export function provideKeyBindings(editor: MarkdownEditor): KeyBinding[] {
48-
const shortcuts = provide(editor);
50+
const shortcuts = provideShortcutMap(editor);
4951
const keyBindings = [];
5052

5153
const wrapAction = (action: ()=>void) => () => {

resources/sass/_forms.scss

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,9 @@
5757
padding: vars.$xs vars.$m;
5858
color: #444;
5959
border-radius: 0;
60+
height: 100%;
61+
font-size: 14px;
62+
line-height: 1.2;
6063
max-height: 100%;
6164
flex: 1;
6265
border: 0;

0 commit comments

Comments
 (0)