mirror of
https://github.com/BookStackApp/BookStack.git
synced 2025-07-28 17:02:04 +03:00
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.
This commit is contained in:
@ -236,7 +236,7 @@ export class Actions {
|
|||||||
if (lineStart === newStart) {
|
if (lineStart === newStart) {
|
||||||
const newLineContent = lineContent.replace(`${newStart} `, '');
|
const newLineContent = lineContent.replace(`${newStart} `, '');
|
||||||
const selectFrom = selectionRange.from + (newLineContent.length - lineContent.length);
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -353,8 +353,8 @@ export class Actions {
|
|||||||
* Fetch and insert the template of the given ID.
|
* Fetch and insert the template of the given ID.
|
||||||
* The page-relative position provided can be used to determine insert location if possible.
|
* The page-relative position provided can be used to determine insert location if possible.
|
||||||
*/
|
*/
|
||||||
async insertTemplate(templateId: string, posX: number, posY: number): Promise<void> {
|
async insertTemplate(templateId: string, event: MouseEvent): Promise<void> {
|
||||||
const cursorPos = this.editor.input.coordsToSelection(posX, posY).from;
|
const cursorPos = this.editor.input.eventToPosition(event).from;
|
||||||
const responseData = (await window.$http.get(`/templates/${templateId}`)).data as {markdown: string, html: string};
|
const responseData = (await window.$http.get(`/templates/${templateId}`)).data as {markdown: string, html: string};
|
||||||
const content = responseData.markdown || responseData.html;
|
const content = responseData.markdown || responseData.html;
|
||||||
this.editor.input.spliceText(cursorPos, cursorPos, content, {from: cursorPos});
|
this.editor.input.spliceText(cursorPos, cursorPos, content, {from: cursorPos});
|
||||||
@ -364,8 +364,8 @@ export class Actions {
|
|||||||
* Insert multiple images from the clipboard from an event at the provided
|
* Insert multiple images from the clipboard from an event at the provided
|
||||||
* screen coordinates (Typically form a paste event).
|
* screen coordinates (Typically form a paste event).
|
||||||
*/
|
*/
|
||||||
insertClipboardImages(images: File[], posX: number, posY: number): void {
|
insertClipboardImages(images: File[], event: MouseEvent): void {
|
||||||
const cursorPos = this.editor.input.coordsToSelection(posX, posY).from;
|
const cursorPos = this.editor.input.eventToPosition(event).from;
|
||||||
for (const image of images) {
|
for (const image of images) {
|
||||||
this.uploadImage(image, cursorPos);
|
this.uploadImage(image, cursorPos);
|
||||||
}
|
}
|
||||||
|
@ -25,7 +25,7 @@ export function getMarkdownDomEventHandlers(editor: MarkdownEditor): MarkdownEdi
|
|||||||
const templateId = event.dataTransfer.getData('bookstack/template');
|
const templateId = event.dataTransfer.getData('bookstack/template');
|
||||||
if (templateId) {
|
if (templateId) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
editor.actions.insertTemplate(templateId, event.pageX, event.pageY);
|
editor.actions.insertTemplate(templateId, event);
|
||||||
}
|
}
|
||||||
|
|
||||||
const clipboard = new Clipboard(event.dataTransfer);
|
const clipboard = new Clipboard(event.dataTransfer);
|
||||||
@ -33,7 +33,7 @@ export function getMarkdownDomEventHandlers(editor: MarkdownEditor): MarkdownEdi
|
|||||||
if (clipboardImages.length > 0) {
|
if (clipboardImages.length > 0) {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
editor.actions.insertClipboardImages(clipboardImages, event.pageX, event.pageY);
|
editor.actions.insertClipboardImages(clipboardImages, event);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// Handle dragover event to allow as drop-target in chrome
|
// Handle dragover event to allow as drop-target in chrome
|
||||||
|
@ -62,7 +62,7 @@ export async function init(config: MarkdownEditorConfig): Promise<MarkdownEditor
|
|||||||
editor.input.teardown();
|
editor.input.teardown();
|
||||||
editor.input = newInput;
|
editor.input = newInput;
|
||||||
});
|
});
|
||||||
// window.devinput = editor.input;
|
window.devinput = editor.input;
|
||||||
|
|
||||||
listenToCommonEvents(editor);
|
listenToCommonEvents(editor);
|
||||||
|
|
||||||
|
@ -72,8 +72,8 @@ export class CodemirrorInput implements MarkdownEditorInput {
|
|||||||
return this.cm.state.doc.lineAt(index).text;
|
return this.cm.state.doc.lineAt(index).text;
|
||||||
}
|
}
|
||||||
|
|
||||||
coordsToSelection(x: number, y: number): MarkdownEditorInputSelection {
|
eventToPosition(event: MouseEvent): MarkdownEditorInputSelection {
|
||||||
const cursorPos = this.cm.posAtCoords({x, y}, false);
|
const cursorPos = this.cm.posAtCoords({x: event.screenX, y: event.screenY}, false);
|
||||||
return {from: cursorPos, to: cursorPos};
|
return {from: cursorPos, to: cursorPos};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -65,9 +65,9 @@ export interface MarkdownEditorInput {
|
|||||||
getLineRangeFromPosition(position: number): MarkdownEditorInputSelection;
|
getLineRangeFromPosition(position: number): MarkdownEditorInputSelection;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert the given screen coords to a selection position within the input.
|
* Convert the given event position to a selection position within the input.
|
||||||
*/
|
*/
|
||||||
coordsToSelection(x: number, y: number): MarkdownEditorInputSelection;
|
eventToPosition(event: MouseEvent): MarkdownEditorInputSelection;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Search and return a line range which includes the provided text.
|
* Search and return a line range which includes the provided text.
|
||||||
|
@ -11,6 +11,8 @@ export class TextareaInput implements MarkdownEditorInput {
|
|||||||
protected onChange: () => void;
|
protected onChange: () => void;
|
||||||
protected eventController = new AbortController();
|
protected eventController = new AbortController();
|
||||||
|
|
||||||
|
protected textSizeCache: {x: number; y: number}|null = null;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
input: HTMLTextAreaElement,
|
input: HTMLTextAreaElement,
|
||||||
shortcuts: MarkdownEditorShortcutMap,
|
shortcuts: MarkdownEditorShortcutMap,
|
||||||
@ -25,6 +27,8 @@ export class TextareaInput implements MarkdownEditorInput {
|
|||||||
this.onKeyDown = this.onKeyDown.bind(this);
|
this.onKeyDown = this.onKeyDown.bind(this);
|
||||||
this.configureListeners();
|
this.configureListeners();
|
||||||
|
|
||||||
|
// TODO - Undo/Redo
|
||||||
|
|
||||||
this.input.style.removeProperty("display");
|
this.input.style.removeProperty("display");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -45,15 +49,24 @@ export class TextareaInput implements MarkdownEditorInput {
|
|||||||
this.input.addEventListener('input', () => {
|
this.input.addEventListener('input', () => {
|
||||||
this.onChange();
|
this.onChange();
|
||||||
}, {signal: this.eventController.signal});
|
}, {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) {
|
onKeyDown(e: KeyboardEvent) {
|
||||||
const isApple = navigator.platform.startsWith("Mac") || navigator.platform === "iPhone";
|
const isApple = navigator.platform.startsWith("Mac") || navigator.platform === "iPhone";
|
||||||
|
const key = e.key.length > 1 ? e.key : e.key.toLowerCase();
|
||||||
const keyParts = [
|
const keyParts = [
|
||||||
e.shiftKey ? 'Shift' : null,
|
e.shiftKey ? 'Shift' : null,
|
||||||
isApple && e.metaKey ? 'Mod' : null,
|
isApple && e.metaKey ? 'Mod' : null,
|
||||||
!isApple && e.ctrlKey ? 'Mod' : null,
|
!isApple && e.ctrlKey ? 'Mod' : null,
|
||||||
e.key,
|
key,
|
||||||
];
|
];
|
||||||
|
|
||||||
const keyString = keyParts.filter(Boolean).join('-');
|
const keyString = keyParts.filter(Boolean).join('-');
|
||||||
@ -65,10 +78,37 @@ export class TextareaInput implements MarkdownEditorInput {
|
|||||||
|
|
||||||
appendText(text: string): void {
|
appendText(text: string): void {
|
||||||
this.input.value += `\n${text}`;
|
this.input.value += `\n${text}`;
|
||||||
|
this.input.dispatchEvent(new Event('input'));
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
coordsToSelection(x: number, y: number): MarkdownEditorInputSelection {
|
|
||||||
// TODO
|
|
||||||
return this.getSelection();
|
return this.getSelection();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -81,11 +121,11 @@ export class TextareaInput implements MarkdownEditorInput {
|
|||||||
let lineStart = 0;
|
let lineStart = 0;
|
||||||
for (let i = 0; i < lines.length; i++) {
|
for (let i = 0; i < lines.length; i++) {
|
||||||
const line = lines[i];
|
const line = lines[i];
|
||||||
const newEnd = lineStart + line.length + 1;
|
const lineEnd = lineStart + line.length;
|
||||||
if (position < newEnd) {
|
if (position <= lineEnd) {
|
||||||
return {from: lineStart, to: newEnd};
|
return {from: lineStart, to: lineEnd};
|
||||||
}
|
}
|
||||||
lineStart = newEnd;
|
lineStart = lineEnd + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {from: 0, to: 0};
|
return {from: 0, to: 0};
|
||||||
@ -140,6 +180,7 @@ export class TextareaInput implements MarkdownEditorInput {
|
|||||||
|
|
||||||
setText(text: string, selection?: MarkdownEditorInputSelection): void {
|
setText(text: string, selection?: MarkdownEditorInputSelection): void {
|
||||||
this.input.value = text;
|
this.input.value = text;
|
||||||
|
this.input.dispatchEvent(new Event('input'));
|
||||||
if (selection) {
|
if (selection) {
|
||||||
this.setSelection(selection, false);
|
this.setSelection(selection, false);
|
||||||
}
|
}
|
||||||
@ -154,4 +195,52 @@ export class TextareaInput implements MarkdownEditorInput {
|
|||||||
this.setSelection(newSelection, 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};
|
||||||
|
}
|
||||||
}
|
}
|
Reference in New Issue
Block a user