]> BookStack Code Mirror - bookstack/blob - resources/js/markdown/inputs/textarea.ts
MD Editor: Added plaintext input implementation
[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
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 }