Skip to content

Commit 6621d55

Browse files
committed
MD Editor: Worked to improve/fix positioning code
Still pending testing. Old logic did not work when lines would wrap, so changing things to a character/line measuring technique. Fixed some other isues too while testing shortcuts.
1 parent d55db06 commit 6621d55

File tree

6 files changed

+108
-19
lines changed

6 files changed

+108
-19
lines changed

resources/js/markdown/actions.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,7 @@ export class Actions {
236236
if (lineStart === newStart) {
237237
const newLineContent = lineContent.replace(`${newStart} `, '');
238238
const selectFrom = selectionRange.from + (newLineContent.length - lineContent.length);
239-
this.editor.input.spliceText(selectionRange.from, selectionRange.to, newLineContent, {from: selectFrom});
239+
this.editor.input.spliceText(lineRange.from, lineRange.to, newLineContent, {from: selectFrom});
240240
return;
241241
}
242242

@@ -353,8 +353,8 @@ export class Actions {
353353
* Fetch and insert the template of the given ID.
354354
* The page-relative position provided can be used to determine insert location if possible.
355355
*/
356-
async insertTemplate(templateId: string, posX: number, posY: number): Promise<void> {
357-
const cursorPos = this.editor.input.coordsToSelection(posX, posY).from;
356+
async insertTemplate(templateId: string, event: MouseEvent): Promise<void> {
357+
const cursorPos = this.editor.input.eventToPosition(event).from;
358358
const responseData = (await window.$http.get(`/templates/${templateId}`)).data as {markdown: string, html: string};
359359
const content = responseData.markdown || responseData.html;
360360
this.editor.input.spliceText(cursorPos, cursorPos, content, {from: cursorPos});
@@ -364,8 +364,8 @@ export class Actions {
364364
* Insert multiple images from the clipboard from an event at the provided
365365
* screen coordinates (Typically form a paste event).
366366
*/
367-
insertClipboardImages(images: File[], posX: number, posY: number): void {
368-
const cursorPos = this.editor.input.coordsToSelection(posX, posY).from;
367+
insertClipboardImages(images: File[], event: MouseEvent): void {
368+
const cursorPos = this.editor.input.eventToPosition(event).from;
369369
for (const image of images) {
370370
this.uploadImage(image, cursorPos);
371371
}

resources/js/markdown/dom-handlers.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,15 +25,15 @@ export function getMarkdownDomEventHandlers(editor: MarkdownEditor): MarkdownEdi
2525
const templateId = event.dataTransfer.getData('bookstack/template');
2626
if (templateId) {
2727
event.preventDefault();
28-
editor.actions.insertTemplate(templateId, event.pageX, event.pageY);
28+
editor.actions.insertTemplate(templateId, event);
2929
}
3030

3131
const clipboard = new Clipboard(event.dataTransfer);
3232
const clipboardImages = clipboard.getImages();
3333
if (clipboardImages.length > 0) {
3434
event.stopPropagation();
3535
event.preventDefault();
36-
editor.actions.insertClipboardImages(clipboardImages, event.pageX, event.pageY);
36+
editor.actions.insertClipboardImages(clipboardImages, event);
3737
}
3838
},
3939
// Handle dragover event to allow as drop-target in chrome

resources/js/markdown/index.mts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ export async function init(config: MarkdownEditorConfig): Promise<MarkdownEditor
6262
editor.input.teardown();
6363
editor.input = newInput;
6464
});
65-
// window.devinput = editor.input;
65+
window.devinput = editor.input;
6666

6767
listenToCommonEvents(editor);
6868

resources/js/markdown/inputs/codemirror.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,8 @@ export class CodemirrorInput implements MarkdownEditorInput {
7272
return this.cm.state.doc.lineAt(index).text;
7373
}
7474

75-
coordsToSelection(x: number, y: number): MarkdownEditorInputSelection {
76-
const cursorPos = this.cm.posAtCoords({x, y}, false);
75+
eventToPosition(event: MouseEvent): MarkdownEditorInputSelection {
76+
const cursorPos = this.cm.posAtCoords({x: event.screenX, y: event.screenY}, false);
7777
return {from: cursorPos, to: cursorPos};
7878
}
7979

resources/js/markdown/inputs/interface.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,9 +65,9 @@ export interface MarkdownEditorInput {
6565
getLineRangeFromPosition(position: number): MarkdownEditorInputSelection;
6666

6767
/**
68-
* Convert the given screen coords to a selection position within the input.
68+
* Convert the given event position to a selection position within the input.
6969
*/
70-
coordsToSelection(x: number, y: number): MarkdownEditorInputSelection;
70+
eventToPosition(event: MouseEvent): MarkdownEditorInputSelection;
7171

7272
/**
7373
* Search and return a line range which includes the provided text.

resources/js/markdown/inputs/textarea.ts

Lines changed: 96 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ export class TextareaInput implements MarkdownEditorInput {
1111
protected onChange: () => void;
1212
protected eventController = new AbortController();
1313

14+
protected textSizeCache: {x: number; y: number}|null = null;
15+
1416
constructor(
1517
input: HTMLTextAreaElement,
1618
shortcuts: MarkdownEditorShortcutMap,
@@ -25,6 +27,8 @@ export class TextareaInput implements MarkdownEditorInput {
2527
this.onKeyDown = this.onKeyDown.bind(this);
2628
this.configureListeners();
2729

30+
// TODO - Undo/Redo
31+
2832
this.input.style.removeProperty("display");
2933
}
3034

@@ -45,15 +49,24 @@ export class TextareaInput implements MarkdownEditorInput {
4549
this.input.addEventListener('input', () => {
4650
this.onChange();
4751
}, {signal: this.eventController.signal});
52+
53+
this.input.addEventListener('click', (event: MouseEvent) => {
54+
const x = event.clientX;
55+
const y = event.clientY;
56+
const range = this.eventToPosition(event);
57+
const text = this.getText().split('');
58+
console.log(range, text.slice(0, 20));
59+
});
4860
}
4961

5062
onKeyDown(e: KeyboardEvent) {
5163
const isApple = navigator.platform.startsWith("Mac") || navigator.platform === "iPhone";
64+
const key = e.key.length > 1 ? e.key : e.key.toLowerCase();
5265
const keyParts = [
5366
e.shiftKey ? 'Shift' : null,
5467
isApple && e.metaKey ? 'Mod' : null,
5568
!isApple && e.ctrlKey ? 'Mod' : null,
56-
e.key,
69+
key,
5770
];
5871

5972
const keyString = keyParts.filter(Boolean).join('-');
@@ -65,10 +78,37 @@ export class TextareaInput implements MarkdownEditorInput {
6578

6679
appendText(text: string): void {
6780
this.input.value += `\n${text}`;
81+
this.input.dispatchEvent(new Event('input'));
6882
}
6983

70-
coordsToSelection(x: number, y: number): MarkdownEditorInputSelection {
71-
// TODO
84+
eventToPosition(event: MouseEvent): MarkdownEditorInputSelection {
85+
const eventCoords = this.mouseEventToTextRelativeCoords(event);
86+
const textSize = this.measureTextSize();
87+
const lineWidth = this.measureLineCharCount(textSize.x);
88+
89+
const lines = this.getText().split('\n');
90+
91+
// TODO - Check this
92+
93+
let currY = 0;
94+
let currPos = 0;
95+
for (const line of lines) {
96+
let linePos = 0;
97+
const wrapCount = Math.max(Math.ceil(line.length / lineWidth), 1);
98+
for (let i = 0; i < wrapCount; i++) {
99+
currY += textSize.y;
100+
if (currY > eventCoords.y) {
101+
const targetX = Math.floor(eventCoords.x / textSize.x);
102+
const maxPos = Math.min(currPos + linePos + targetX, currPos + line.length);
103+
return {from: maxPos, to: maxPos};
104+
}
105+
106+
linePos += lineWidth;
107+
}
108+
109+
currPos += line.length + 1;
110+
}
111+
72112
return this.getSelection();
73113
}
74114

@@ -81,11 +121,11 @@ export class TextareaInput implements MarkdownEditorInput {
81121
let lineStart = 0;
82122
for (let i = 0; i < lines.length; i++) {
83123
const line = lines[i];
84-
const newEnd = lineStart + line.length + 1;
85-
if (position < newEnd) {
86-
return {from: lineStart, to: newEnd};
124+
const lineEnd = lineStart + line.length;
125+
if (position <= lineEnd) {
126+
return {from: lineStart, to: lineEnd};
87127
}
88-
lineStart = newEnd;
128+
lineStart = lineEnd + 1;
89129
}
90130

91131
return {from: 0, to: 0};
@@ -140,6 +180,7 @@ export class TextareaInput implements MarkdownEditorInput {
140180

141181
setText(text: string, selection?: MarkdownEditorInputSelection): void {
142182
this.input.value = text;
183+
this.input.dispatchEvent(new Event('input'));
143184
if (selection) {
144185
this.setSelection(selection, false);
145186
}
@@ -154,4 +195,52 @@ export class TextareaInput implements MarkdownEditorInput {
154195
this.setSelection(newSelection, false);
155196
}
156197
}
198+
199+
protected measureTextSize(): {x: number; y: number} {
200+
if (this.textSizeCache) {
201+
return this.textSizeCache;
202+
}
203+
204+
const el = document.createElement("div");
205+
el.textContent = `a\nb`;
206+
const inputStyles = window.getComputedStyle(this.input)
207+
el.style.font = inputStyles.font;
208+
el.style.lineHeight = inputStyles.lineHeight;
209+
el.style.padding = '0px';
210+
el.style.display = 'inline-block';
211+
el.style.visibility = 'hidden';
212+
el.style.position = 'absolute';
213+
el.style.whiteSpace = 'pre';
214+
this.input.after(el);
215+
216+
const bounds = el.getBoundingClientRect();
217+
el.remove();
218+
this.textSizeCache = {
219+
x: bounds.width,
220+
y: bounds.height / 2,
221+
};
222+
return this.textSizeCache;
223+
}
224+
225+
protected measureLineCharCount(textWidth: number): number {
226+
const inputStyles = window.getComputedStyle(this.input);
227+
const paddingLeft = Number(inputStyles.paddingLeft.replace('px', ''));
228+
const paddingRight = Number(inputStyles.paddingRight.replace('px', ''));
229+
const width = Number(inputStyles.width.replace('px', ''));
230+
const textSpace = width - (paddingLeft + paddingRight);
231+
232+
return Math.floor(textSpace / textWidth);
233+
}
234+
235+
protected mouseEventToTextRelativeCoords(event: MouseEvent): {x: number; y: number} {
236+
const inputBounds = this.input.getBoundingClientRect();
237+
const inputStyles = window.getComputedStyle(this.input);
238+
const paddingTop = Number(inputStyles.paddingTop.replace('px', ''));
239+
const paddingLeft = Number(inputStyles.paddingLeft.replace('px', ''));
240+
241+
const xPos = Math.max(event.clientX - (inputBounds.left + paddingLeft), 0);
242+
const yPos = Math.max((event.clientY - (inputBounds.top + paddingTop)) + this.input.scrollTop, 0);
243+
244+
return {x: xPos, y: yPos};
245+
}
157246
}

0 commit comments

Comments
 (0)