]> BookStack Code Mirror - bookstack/blob - resources/js/markdown/inputs/textarea.ts
MD Editor: Added plaintext/cm switching
[bookstack] / resources / js / markdown / inputs / textarea.ts
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     protected onChange: () => void;
12     protected eventController = new AbortController();
13
14     constructor(
15         input: HTMLTextAreaElement,
16         shortcuts: MarkdownEditorShortcutMap,
17         events: MarkdownEditorEventMap,
18         onChange: () => void
19     ) {
20         this.input = input;
21         this.shortcuts = shortcuts;
22         this.events = events;
23         this.onChange = onChange;
24
25         this.onKeyDown = this.onKeyDown.bind(this);
26         this.configureListeners();
27
28         this.input.style.removeProperty("display");
29     }
30
31     teardown() {
32         this.eventController.abort('teardown');
33     }
34
35     configureListeners(): void {
36         // Keyboard shortcuts
37         this.input.addEventListener('keydown', this.onKeyDown, {signal: this.eventController.signal});
38
39         // Shared event listeners
40         for (const [name, listener] of Object.entries(this.events)) {
41             this.input.addEventListener(name, listener, {signal: this.eventController.signal});
42         }
43
44         // Input change handling
45         this.input.addEventListener('input', () => {
46             this.onChange();
47         }, {signal: this.eventController.signal});
48     }
49
50     onKeyDown(e: KeyboardEvent) {
51         const isApple = navigator.platform.startsWith("Mac") || navigator.platform === "iPhone";
52         const keyParts = [
53             e.shiftKey ? 'Shift' : null,
54             isApple && e.metaKey ? 'Mod' : null,
55             !isApple && e.ctrlKey ? 'Mod' : null,
56             e.key,
57         ];
58
59         const keyString = keyParts.filter(Boolean).join('-');
60         if (this.shortcuts[keyString]) {
61             e.preventDefault();
62             this.shortcuts[keyString]();
63         }
64     }
65
66     appendText(text: string): void {
67         this.input.value += `\n${text}`;
68     }
69
70     coordsToSelection(x: number, y: number): MarkdownEditorInputSelection {
71         // TODO
72         return this.getSelection();
73     }
74
75     focus(): void {
76         this.input.focus();
77     }
78
79     getLineRangeFromPosition(position: number): MarkdownEditorInputSelection {
80         const lines = this.getText().split('\n');
81         let lineStart = 0;
82         for (let i = 0; i < lines.length; i++) {
83             const line = lines[i];
84             const newEnd = lineStart + line.length + 1;
85             if (position < newEnd) {
86                 return {from: lineStart, to: newEnd};
87             }
88             lineStart = newEnd;
89         }
90
91         return {from: 0, to: 0};
92     }
93
94     getLineText(lineIndex: number): string {
95         const text = this.getText();
96         const lines = text.split("\n");
97         return lines[lineIndex] || '';
98     }
99
100     getSelection(): MarkdownEditorInputSelection {
101         return {from: this.input.selectionStart, to: this.input.selectionEnd};
102     }
103
104     getSelectionText(selection?: MarkdownEditorInputSelection): string {
105         const text = this.getText();
106         const range = selection || this.getSelection();
107         return text.slice(range.from, range.to);
108     }
109
110     getText(): string {
111         return this.input.value;
112     }
113
114     getTextAboveView(): string {
115         const scrollTop = this.input.scrollTop;
116         const computedStyles = window.getComputedStyle(this.input);
117         const lines = this.getText().split('\n');
118         const paddingTop = Number(computedStyles.paddingTop.replace('px', ''));
119         const paddingBottom = Number(computedStyles.paddingBottom.replace('px', ''));
120
121         const avgLineHeight = (this.input.scrollHeight - paddingBottom - paddingTop) / lines.length;
122         const roughLinePos = Math.max(Math.floor((scrollTop - paddingTop) / avgLineHeight), 0);
123         const linesAbove = this.getText().split('\n').slice(0, roughLinePos);
124         return linesAbove.join('\n');
125     }
126
127     searchForLineContaining(text: string): MarkdownEditorInputSelection | null {
128         const textPosition = this.getText().indexOf(text);
129         if (textPosition > -1) {
130             return this.getLineRangeFromPosition(textPosition);
131         }
132
133         return null;
134     }
135
136     setSelection(selection: MarkdownEditorInputSelection, scrollIntoView: boolean): void {
137         this.input.selectionStart = selection.from;
138         this.input.selectionEnd = selection.to;
139     }
140
141     setText(text: string, selection?: MarkdownEditorInputSelection): void {
142         this.input.value = text;
143         if (selection) {
144             this.setSelection(selection, false);
145         }
146     }
147
148     spliceText(from: number, to: number, newText: string, selection: Partial<MarkdownEditorInputSelection> | null): void {
149         const text = this.getText();
150         const updatedText = text.slice(0, from) + newText + text.slice(to);
151         this.setText(updatedText);
152         if (selection && selection.from) {
153             const newSelection = {from: selection.from, to: selection.to || selection.from};
154             this.setSelection(newSelection, false);
155         }
156     }
157 }