1 import {MarkdownEditorInput, MarkdownEditorInputSelection} from "./interface";
2 import {MarkdownEditorShortcutMap} from "../shortcuts";
3 import {MarkdownEditorEventMap} from "../dom-handlers";
6 export class TextareaInput implements MarkdownEditorInput {
8 protected input: HTMLTextAreaElement;
9 protected shortcuts: MarkdownEditorShortcutMap;
10 protected events: MarkdownEditorEventMap;
11 protected onChange: () => void;
12 protected eventController = new AbortController();
15 input: HTMLTextAreaElement,
16 shortcuts: MarkdownEditorShortcutMap,
17 events: MarkdownEditorEventMap,
21 this.shortcuts = shortcuts;
23 this.onChange = onChange;
25 this.onKeyDown = this.onKeyDown.bind(this);
26 this.configureListeners();
28 this.input.style.removeProperty("display");
32 this.eventController.abort('teardown');
35 configureListeners(): void {
37 this.input.addEventListener('keydown', this.onKeyDown, {signal: this.eventController.signal});
39 // Shared event listeners
40 for (const [name, listener] of Object.entries(this.events)) {
41 this.input.addEventListener(name, listener, {signal: this.eventController.signal});
44 // Input change handling
45 this.input.addEventListener('input', () => {
47 }, {signal: this.eventController.signal});
50 onKeyDown(e: KeyboardEvent) {
51 const isApple = navigator.platform.startsWith("Mac") || navigator.platform === "iPhone";
53 e.shiftKey ? 'Shift' : null,
54 isApple && e.metaKey ? 'Mod' : null,
55 !isApple && e.ctrlKey ? 'Mod' : null,
59 const keyString = keyParts.filter(Boolean).join('-');
60 if (this.shortcuts[keyString]) {
62 this.shortcuts[keyString]();
66 appendText(text: string): void {
67 this.input.value += `\n${text}`;
70 coordsToSelection(x: number, y: number): MarkdownEditorInputSelection {
72 return this.getSelection();
79 getLineRangeFromPosition(position: number): MarkdownEditorInputSelection {
80 const lines = this.getText().split('\n');
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};
91 return {from: 0, to: 0};
94 getLineText(lineIndex: number): string {
95 const text = this.getText();
96 const lines = text.split("\n");
97 return lines[lineIndex] || '';
100 getSelection(): MarkdownEditorInputSelection {
101 return {from: this.input.selectionStart, to: this.input.selectionEnd};
104 getSelectionText(selection?: MarkdownEditorInputSelection): string {
105 const text = this.getText();
106 const range = selection || this.getSelection();
107 return text.slice(range.from, range.to);
111 return this.input.value;
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', ''));
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');
127 searchForLineContaining(text: string): MarkdownEditorInputSelection | null {
128 const textPosition = this.getText().indexOf(text);
129 if (textPosition > -1) {
130 return this.getLineRangeFromPosition(textPosition);
136 setSelection(selection: MarkdownEditorInputSelection, scrollIntoView: boolean): void {
137 this.input.selectionStart = selection.from;
138 this.input.selectionEnd = selection.to;
141 setText(text: string, selection?: MarkdownEditorInputSelection): void {
142 this.input.value = text;
144 this.setSelection(selection, false);
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);