Lexical: Extracted mouse drag tracking to new helper

This commit is contained in:
Dan Brown 2024-06-25 18:33:29 +01:00
parent 3af22ce754
commit 59936631ec
No known key found for this signature in database
GPG Key ID: 46D9F943C24A2EF9
3 changed files with 149 additions and 66 deletions

View File

@ -3,6 +3,7 @@ import {el} from "../../helpers";
import {$createNodeSelection, $setSelection} from "lexical"; import {$createNodeSelection, $setSelection} from "lexical";
import {EditorUiContext} from "../framework/core"; import {EditorUiContext} from "../framework/core";
import {ImageNode} from "../../nodes/image"; import {ImageNode} from "../../nodes/image";
import {MouseDragTracker, MouseDragTrackerDistance} from "../framework/helpers/mouse-drag-tracker";
export class ImageDecorator extends EditorDecorator { export class ImageDecorator extends EditorDecorator {
@ -15,6 +16,7 @@ export class ImageDecorator extends EditorDecorator {
class: 'editor-image-decorator', class: 'editor-image-decorator',
}, []); }, []);
let selected = false; let selected = false;
let tracker: MouseDragTracker|null = null;
const windowClick = (event: MouseEvent) => { const windowClick = (event: MouseEvent) => {
if (!decorateEl.contains(event.target as Node) && (Date.now() - this.dragLastMouseUp > 100)) { if (!decorateEl.contains(event.target as Node) && (Date.now() - this.dragLastMouseUp > 100)) {
@ -22,14 +24,6 @@ export class ImageDecorator extends EditorDecorator {
} }
}; };
const mouseDown = (event: MouseEvent) => {
const handle = (event.target as HTMLElement).closest('.editor-image-decorator-handle') as HTMLElement|null;
if (handle) {
// handlingResize = true;
this.startHandlingResize(handle, event, context);
}
};
const select = () => { const select = () => {
if (selected) { if (selected) {
return; return;
@ -44,7 +38,7 @@ export class ImageDecorator extends EditorDecorator {
return el('div', {class: `editor-image-decorator-handle ${c}`}); return el('div', {class: `editor-image-decorator-handle ${c}`});
}); });
decorateEl.append(...handleElems); decorateEl.append(...handleElems);
decorateEl.addEventListener('mousedown', mouseDown); tracker = this.setupTracker(decorateEl, context);
context.editor.update(() => { context.editor.update(() => {
const nodeSelection = $createNodeSelection(); const nodeSelection = $createNodeSelection();
@ -55,10 +49,9 @@ export class ImageDecorator extends EditorDecorator {
const unselect = () => { const unselect = () => {
selected = false; selected = false;
// handlingResize = false;
decorateEl.classList.remove('selected'); decorateEl.classList.remove('selected');
window.removeEventListener('click', windowClick); window.removeEventListener('click', windowClick);
decorateEl.removeEventListener('mousedown', mouseDown); tracker?.teardown();
for (const el of handleElems) { for (const el of handleElems) {
el.remove(); el.remove();
} }
@ -80,62 +73,61 @@ export class ImageDecorator extends EditorDecorator {
return this.dom; return this.dom;
} }
startHandlingResize(element: HTMLElement, event: MouseEvent, context: EditorUiContext) { setupTracker(container: HTMLElement, context: EditorUiContext): MouseDragTracker {
const startingX = event.screenX; let startingWidth: number = 0;
const startingY = event.screenY; let startingHeight: number = 0;
const node = this.getNode() as ImageNode; let startingRatio: number = 0;
let startingWidth = element.clientWidth;
let startingHeight = element.clientHeight;
let startingRatio = startingWidth / startingHeight;
let hasHeight = false; let hasHeight = false;
let firstChange = true; let firstChange = true;
context.editor.getEditorState().read(() => { let node: ImageNode = this.getNode() as ImageNode;
startingWidth = node.getWidth() || startingWidth; let _this = this;
startingHeight = node.getHeight() || startingHeight; let flipXChange: boolean = false;
if (node.getHeight()) { let flipYChange: boolean = false;
hasHeight = true;
return new MouseDragTracker(container, '.editor-image-decorator-handle', {
down(event: MouseEvent, handle: HTMLElement) {
context.editor.getEditorState().read(() => {
startingWidth = node.getWidth() || startingWidth;
startingHeight = node.getHeight() || startingHeight;
if (node.getHeight()) {
hasHeight = true;
}
startingRatio = startingWidth / startingHeight;
});
flipXChange = handle.classList.contains('nw') || handle.classList.contains('sw');
flipYChange = handle.classList.contains('nw') || handle.classList.contains('ne');
},
move(event: MouseEvent, handle: HTMLElement, distance: MouseDragTrackerDistance) {
let xChange = distance.x;
if (flipXChange) {
xChange = 0 - xChange;
}
let yChange = distance.y;
if (flipYChange) {
yChange = 0 - yChange;
}
const balancedChange = Math.sqrt(Math.pow(Math.abs(xChange), 2) + Math.pow(Math.abs(yChange), 2));
const increase = xChange + yChange > 0;
const directedChange = increase ? balancedChange : 0-balancedChange;
const newWidth = Math.max(5, Math.round(startingWidth + directedChange));
let newHeight = 0;
if (hasHeight) {
newHeight = newWidth * startingRatio;
}
const updateOptions = firstChange ? {} : {tag: 'history-merge'};
context.editor.update(() => {
const node = _this.getNode() as ImageNode;
node.setWidth(newWidth);
node.setHeight(newHeight);
}, updateOptions);
firstChange = false;
},
up() {
_this.dragLastMouseUp = Date.now();
} }
startingRatio = startingWidth / startingHeight;
}); });
const flipXChange = element.classList.contains('nw') || element.classList.contains('sw');
const flipYChange = element.classList.contains('nw') || element.classList.contains('ne');
const mouseMoveListener = (event: MouseEvent) => {
let xChange = event.screenX - startingX;
if (flipXChange) {
xChange = 0 - xChange;
}
let yChange = event.screenY - startingY;
if (flipYChange) {
yChange = 0 - yChange;
}
const balancedChange = Math.sqrt(Math.pow(Math.abs(xChange), 2) + Math.pow(Math.abs(yChange), 2));
const increase = xChange + yChange > 0;
const directedChange = increase ? balancedChange : 0-balancedChange;
const newWidth = Math.max(5, Math.round(startingWidth + directedChange));
let newHeight = 0;
if (hasHeight) {
newHeight = newWidth * startingRatio;
}
const updateOptions = firstChange ? {} : {tag: 'history-merge'};
context.editor.update(() => {
const node = this.getNode() as ImageNode;
node.setWidth(newWidth);
node.setHeight(newHeight);
}, updateOptions);
firstChange = false;
};
const mouseUpListener = (event: MouseEvent) => {
window.removeEventListener('mousemove', mouseMoveListener);
window.removeEventListener('mouseup', mouseUpListener);
this.dragLastMouseUp = Date.now();
};
window.addEventListener('mousemove', mouseMoveListener);
window.addEventListener('mouseup', mouseUpListener);
} }
} }

View File

@ -0,0 +1,76 @@
export type MouseDragTrackerDistance = {
x: number;
y: number;
}
export type MouseDragTrackerOptions = {
down?: (event: MouseEvent, element: HTMLElement) => any;
move?: (event: MouseEvent, element: HTMLElement, distance: MouseDragTrackerDistance) => any;
up?: (event: MouseEvent, element: HTMLElement, distance: MouseDragTrackerDistance) => any;
}
export class MouseDragTracker {
protected container: HTMLElement;
protected dragTargetSelector: string;
protected options: MouseDragTrackerOptions;
protected startX: number = 0;
protected startY: number = 0;
protected target: HTMLElement|null = null;
constructor(container: HTMLElement, dragTargetSelector: string, options: MouseDragTrackerOptions) {
this.container = container;
this.dragTargetSelector = dragTargetSelector;
this.options = options;
this.onMouseDown = this.onMouseDown.bind(this);
this.onMouseMove = this.onMouseMove.bind(this);
this.onMouseUp = this.onMouseUp.bind(this);
this.container.addEventListener('mousedown', this.onMouseDown);
}
teardown() {
this.container.removeEventListener('mousedown', this.onMouseDown);
this.container.removeEventListener('mouseup', this.onMouseUp);
this.container.removeEventListener('mousemove', this.onMouseMove);
}
protected onMouseDown(event: MouseEvent) {
this.target = (event.target as HTMLElement).closest(this.dragTargetSelector);
if (!this.target) {
return;
}
this.startX = event.screenX;
this.startY = event.screenY;
window.addEventListener('mousemove', this.onMouseMove);
window.addEventListener('mouseup', this.onMouseUp);
if (this.options.down) {
this.options.down(event, this.target);
}
}
protected onMouseMove(event: MouseEvent) {
if (this.options.move && this.target) {
this.options.move(event, this.target, {
x: event.screenX - this.startX,
y: event.screenY - this.startY,
});
}
}
protected onMouseUp(event: MouseEvent) {
window.removeEventListener('mousemove', this.onMouseMove);
window.removeEventListener('mouseup', this.onMouseUp);
if (this.options.up && this.target) {
this.options.up(event, this.target, {
x: event.screenX - this.startX,
y: event.screenY - this.startY,
});
}
}
}

View File

@ -1,5 +1,6 @@
import {LexicalEditor} from "lexical"; import {LexicalEditor} from "lexical";
import {el} from "../../../helpers"; import {el} from "../../../helpers";
import {MouseDragTracker, MouseDragTrackerDistance} from "./mouse-drag-tracker";
type MarkerDomRecord = {x: HTMLElement, y: HTMLElement}; type MarkerDomRecord = {x: HTMLElement, y: HTMLElement};
@ -7,6 +8,7 @@ class TableResizer {
protected editor: LexicalEditor; protected editor: LexicalEditor;
protected editArea: HTMLElement; protected editArea: HTMLElement;
protected markerDom: MarkerDomRecord|null = null; protected markerDom: MarkerDomRecord|null = null;
protected mouseTracker: MouseDragTracker|null = null;
constructor(editor: LexicalEditor, editArea: HTMLElement) { constructor(editor: LexicalEditor, editArea: HTMLElement) {
this.editor = editor; this.editor = editor;
@ -49,14 +51,27 @@ class TableResizer {
getMarkers(): MarkerDomRecord { getMarkers(): MarkerDomRecord {
if (!this.markerDom) { if (!this.markerDom) {
this.markerDom = { this.markerDom = {
x: el('div', {class: 'editor-table-marker-column'}), x: el('div', {class: 'editor-table-marker editor-table-marker-column'}),
y: el('div', {class: 'editor-table-marker-row'}), y: el('div', {class: 'editor-table-marker editor-table-marker-row'}),
} }
this.editArea.after(this.markerDom.x, this.markerDom.y); const wrapper = el('div', {
class: 'editor-table-marker-wrap',
}, [this.markerDom.x, this.markerDom.y]);
this.editArea.after(wrapper);
this.watchMarkerMouseDrags(wrapper);
} }
return this.markerDom; return this.markerDom;
} }
watchMarkerMouseDrags(wrapper: HTMLElement) {
this.mouseTracker = new MouseDragTracker(wrapper, '.editor-table-marker', {
up(event: MouseEvent, marker: HTMLElement, distance: MouseDragTrackerDistance) {
console.log('up', distance, marker);
// TODO - Update row/column for distance
}
});
}
} }