if (lineStart === newStart) {
const newLineContent = lineContent.replace(`${newStart} `, '');
const selectFrom = selectionRange.from + (newLineContent.length - lineContent.length);
- this.editor.input.spliceText(selectionRange.from, selectionRange.to, newLineContent, {from: selectFrom});
+ this.editor.input.spliceText(lineRange.from, lineRange.to, newLineContent, {from: selectFrom});
return;
}
* Fetch and insert the template of the given ID.
* The page-relative position provided can be used to determine insert location if possible.
*/
- async insertTemplate(templateId: string, posX: number, posY: number): Promise<void> {
- const cursorPos = this.editor.input.coordsToSelection(posX, posY).from;
+ async insertTemplate(templateId: string, event: MouseEvent): Promise<void> {
+ const cursorPos = this.editor.input.eventToPosition(event).from;
const responseData = (await window.$http.get(`/templates/${templateId}`)).data as {markdown: string, html: string};
const content = responseData.markdown || responseData.html;
this.editor.input.spliceText(cursorPos, cursorPos, content, {from: cursorPos});
* Insert multiple images from the clipboard from an event at the provided
* screen coordinates (Typically form a paste event).
*/
- insertClipboardImages(images: File[], posX: number, posY: number): void {
- const cursorPos = this.editor.input.coordsToSelection(posX, posY).from;
+ insertClipboardImages(images: File[], event: MouseEvent): void {
+ const cursorPos = this.editor.input.eventToPosition(event).from;
for (const image of images) {
this.uploadImage(image, cursorPos);
}
protected onChange: () => void;
protected eventController = new AbortController();
+ protected textSizeCache: {x: number; y: number}|null = null;
+
constructor(
input: HTMLTextAreaElement,
shortcuts: MarkdownEditorShortcutMap,
this.onKeyDown = this.onKeyDown.bind(this);
this.configureListeners();
+ // TODO - Undo/Redo
+
this.input.style.removeProperty("display");
}
this.input.addEventListener('input', () => {
this.onChange();
}, {signal: this.eventController.signal});
+
+ this.input.addEventListener('click', (event: MouseEvent) => {
+ const x = event.clientX;
+ const y = event.clientY;
+ const range = this.eventToPosition(event);
+ const text = this.getText().split('');
+ console.log(range, text.slice(0, 20));
+ });
}
onKeyDown(e: KeyboardEvent) {
const isApple = navigator.platform.startsWith("Mac") || navigator.platform === "iPhone";
+ const key = e.key.length > 1 ? e.key : e.key.toLowerCase();
const keyParts = [
e.shiftKey ? 'Shift' : null,
isApple && e.metaKey ? 'Mod' : null,
!isApple && e.ctrlKey ? 'Mod' : null,
- e.key,
+ key,
];
const keyString = keyParts.filter(Boolean).join('-');
appendText(text: string): void {
this.input.value += `\n${text}`;
+ this.input.dispatchEvent(new Event('input'));
}
- coordsToSelection(x: number, y: number): MarkdownEditorInputSelection {
- // TODO
+ eventToPosition(event: MouseEvent): MarkdownEditorInputSelection {
+ const eventCoords = this.mouseEventToTextRelativeCoords(event);
+ const textSize = this.measureTextSize();
+ const lineWidth = this.measureLineCharCount(textSize.x);
+
+ const lines = this.getText().split('\n');
+
+ // TODO - Check this
+
+ let currY = 0;
+ let currPos = 0;
+ for (const line of lines) {
+ let linePos = 0;
+ const wrapCount = Math.max(Math.ceil(line.length / lineWidth), 1);
+ for (let i = 0; i < wrapCount; i++) {
+ currY += textSize.y;
+ if (currY > eventCoords.y) {
+ const targetX = Math.floor(eventCoords.x / textSize.x);
+ const maxPos = Math.min(currPos + linePos + targetX, currPos + line.length);
+ return {from: maxPos, to: maxPos};
+ }
+
+ linePos += lineWidth;
+ }
+
+ currPos += line.length + 1;
+ }
+
return this.getSelection();
}
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};
+ const lineEnd = lineStart + line.length;
+ if (position <= lineEnd) {
+ return {from: lineStart, to: lineEnd};
}
- lineStart = newEnd;
+ lineStart = lineEnd + 1;
}
return {from: 0, to: 0};
setText(text: string, selection?: MarkdownEditorInputSelection): void {
this.input.value = text;
+ this.input.dispatchEvent(new Event('input'));
if (selection) {
this.setSelection(selection, false);
}
this.setSelection(newSelection, false);
}
}
+
+ protected measureTextSize(): {x: number; y: number} {
+ if (this.textSizeCache) {
+ return this.textSizeCache;
+ }
+
+ const el = document.createElement("div");
+ el.textContent = `a\nb`;
+ const inputStyles = window.getComputedStyle(this.input)
+ el.style.font = inputStyles.font;
+ el.style.lineHeight = inputStyles.lineHeight;
+ el.style.padding = '0px';
+ el.style.display = 'inline-block';
+ el.style.visibility = 'hidden';
+ el.style.position = 'absolute';
+ el.style.whiteSpace = 'pre';
+ this.input.after(el);
+
+ const bounds = el.getBoundingClientRect();
+ el.remove();
+ this.textSizeCache = {
+ x: bounds.width,
+ y: bounds.height / 2,
+ };
+ return this.textSizeCache;
+ }
+
+ protected measureLineCharCount(textWidth: number): number {
+ const inputStyles = window.getComputedStyle(this.input);
+ const paddingLeft = Number(inputStyles.paddingLeft.replace('px', ''));
+ const paddingRight = Number(inputStyles.paddingRight.replace('px', ''));
+ const width = Number(inputStyles.width.replace('px', ''));
+ const textSpace = width - (paddingLeft + paddingRight);
+
+ return Math.floor(textSpace / textWidth);
+ }
+
+ protected mouseEventToTextRelativeCoords(event: MouseEvent): {x: number; y: number} {
+ const inputBounds = this.input.getBoundingClientRect();
+ const inputStyles = window.getComputedStyle(this.input);
+ const paddingTop = Number(inputStyles.paddingTop.replace('px', ''));
+ const paddingLeft = Number(inputStyles.paddingLeft.replace('px', ''));
+
+ const xPos = Math.max(event.clientX - (inputBounds.left + paddingLeft), 0);
+ const yPos = Math.max((event.clientY - (inputBounds.top + paddingTop)) + this.input.scrollTop, 0);
+
+ return {x: xPos, y: yPos};
+ }
}
\ No newline at end of file