Refactoring in pointer down event handler (#1880)

* Refactor: Move context menu touch device handling

* Refactor: Move more stuff out of pointer down

* Refactor: Move last coords into an object

* Refactor: Move scrollbar handling out of pointer down

* Refactor: simplify resizing in pointer down

* Refactor: further simplify resizing in pointer down

* Refactor: clarify clearing selection code

* Refactor: move out clearing selection from pointer down

* Refactor: further simplify deselection in pointer down
This commit is contained in:
Michal Srb 2020-07-08 22:07:51 -07:00 committed by GitHub
parent d5e7d08586
commit 5d7020cce6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 278 additions and 226 deletions

View File

@ -1900,28 +1900,8 @@ class App extends React.Component<ExcalidrawProps, AppState> {
) => { ) => {
event.persist(); event.persist();
// deal with opening context menu on touch devices this.maybeOpenContextMenuAfterPointerDownOnTouchDevices(event);
if (event.pointerType === "touch") { this.maybeCleanupAfterMissingPointerUp(event);
touchMoving = false;
// open the context menu with the first touch's clientX and clientY
// if the touch is not moving
touchTimeout = window.setTimeout(() => {
if (!touchMoving) {
this.openContextMenu({
clientX: event.clientX,
clientY: event.clientY,
});
}
}, TOUCH_CTX_MENU_TIMEOUT);
}
if (lastPointerUp !== null) {
// Unfortunately, sometimes we don't get a pointerup after a pointerdown,
// this can happen when a contextual menu or alert is triggered. In order to avoid
// being in a weird state, we clean up on the next pointerdown
lastPointerUp(event);
}
if (isPanning) { if (isPanning) {
return; return;
@ -1933,89 +1913,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
}); });
this.savePointer(event.clientX, event.clientY, "down"); this.savePointer(event.clientX, event.clientY, "down");
// pan canvas on wheel button drag or space+drag if (this.handleCanvasPanUsingWheelOrSpaceDrag(event)) {
if (
gesture.pointers.size === 0 &&
(event.button === POINTER_BUTTON.WHEEL ||
(event.button === POINTER_BUTTON.MAIN && isHoldingSpace))
) {
isPanning = true;
let nextPastePrevented = false;
const isLinux = /Linux/.test(window.navigator.platform);
document.documentElement.style.cursor = CURSOR_TYPE.GRABBING;
let { clientX: lastX, clientY: lastY } = event;
const onPointerMove = withBatchedUpdates((event: PointerEvent) => {
const deltaX = lastX - event.clientX;
const deltaY = lastY - event.clientY;
lastX = event.clientX;
lastY = event.clientY;
/*
* Prevent paste event if we move while middle clicking on Linux.
* See issue #1383.
*/
if (
isLinux &&
!nextPastePrevented &&
(Math.abs(deltaX) > 1 || Math.abs(deltaY) > 1)
) {
nextPastePrevented = true;
/* Prevent the next paste event */
const preventNextPaste = (event: ClipboardEvent) => {
document.body.removeEventListener(EVENT.PASTE, preventNextPaste);
event.stopPropagation();
};
/*
* Reenable next paste in case of disabled middle click paste for
* any reason:
* - rigth click paste
* - empty clipboard
*/
const enableNextPaste = () => {
setTimeout(() => {
document.body.removeEventListener(EVENT.PASTE, preventNextPaste);
window.removeEventListener(EVENT.POINTER_UP, enableNextPaste);
}, 100);
};
document.body.addEventListener(EVENT.PASTE, preventNextPaste);
window.addEventListener(EVENT.POINTER_UP, enableNextPaste);
}
this.setState({
scrollX: normalizeScroll(
this.state.scrollX - deltaX / this.state.zoom,
),
scrollY: normalizeScroll(
this.state.scrollY - deltaY / this.state.zoom,
),
});
});
const teardown = withBatchedUpdates(
(lastPointerUp = () => {
lastPointerUp = null;
isPanning = false;
if (!isHoldingSpace) {
setCursorForShape(this.state.elementType);
}
this.setState({
cursorButton: "up",
});
this.savePointer(event.clientX, event.clientY, "up");
window.removeEventListener(EVENT.POINTER_MOVE, onPointerMove);
window.removeEventListener(EVENT.POINTER_UP, teardown);
window.removeEventListener(EVENT.BLUR, teardown);
}),
);
window.addEventListener(EVENT.BLUR, teardown);
window.addEventListener(EVENT.POINTER_MOVE, onPointerMove, {
passive: true,
});
window.addEventListener(EVENT.POINTER_UP, teardown);
return; return;
} }
@ -2027,18 +1925,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
return; return;
} }
gesture.pointers.set(event.pointerId, { this.updateGestureOnPointerDown(event);
x: event.clientX,
y: event.clientY,
});
if (gesture.pointers.size === 2) {
gesture.lastCenter = getCenter(gesture.pointers);
gesture.initialScale = this.state.zoom;
gesture.initialDistance = getDistance(
Array.from(gesture.pointers.values()),
);
}
// fixes pointermove causing selection of UI texts #32 // fixes pointermove causing selection of UI texts #32
event.preventDefault(); event.preventDefault();
@ -2055,10 +1942,15 @@ class App extends React.Component<ExcalidrawProps, AppState> {
} }
// Handle scrollbars dragging // Handle scrollbars dragging
const isOverScrollBarsNow = isOverScrollBars(
currentScrollBars,
event.clientX,
event.clientY,
);
const { const {
isOverHorizontalScrollBar, isOverHorizontalScrollBar,
isOverVerticalScrollBar, isOverVerticalScrollBar,
} = isOverScrollBars(currentScrollBars, event.clientX, event.clientY); } = isOverScrollBarsNow;
const { x, y } = viewportCoordsToSceneCoords( const { x, y } = viewportCoordsToSceneCoords(
event, event,
@ -2066,58 +1958,9 @@ class App extends React.Component<ExcalidrawProps, AppState> {
this.canvas, this.canvas,
window.devicePixelRatio, window.devicePixelRatio,
); );
let lastX = x; const lastCoords = { x, y };
let lastY = y;
if ( if (this.handleDraggingScrollBar(event, lastCoords, isOverScrollBarsNow)) {
(isOverHorizontalScrollBar || isOverVerticalScrollBar) &&
!this.state.multiElement
) {
isDraggingScrollBar = true;
lastX = event.clientX;
lastY = event.clientY;
const onPointerMove = withBatchedUpdates((event: PointerEvent) => {
const target = event.target;
if (!(target instanceof HTMLElement)) {
return;
}
if (isOverHorizontalScrollBar) {
const x = event.clientX;
const dx = x - lastX;
this.setState({
scrollX: normalizeScroll(this.state.scrollX - dx / this.state.zoom),
});
lastX = x;
return;
}
if (isOverVerticalScrollBar) {
const y = event.clientY;
const dy = y - lastY;
this.setState({
scrollY: normalizeScroll(this.state.scrollY - dy / this.state.zoom),
});
lastY = y;
}
});
const onPointerUp = withBatchedUpdates(() => {
isDraggingScrollBar = false;
setCursorForShape(this.state.elementType);
lastPointerUp = null;
this.setState({
cursorButton: "up",
});
this.savePointer(event.clientX, event.clientY, "up");
window.removeEventListener(EVENT.POINTER_MOVE, onPointerMove);
window.removeEventListener(EVENT.POINTER_UP, onPointerUp);
});
lastPointerUp = onPointerUp;
window.addEventListener(EVENT.POINTER_MOVE, onPointerMove);
window.addEventListener(EVENT.POINTER_UP, onPointerUp);
return; return;
} }
@ -2142,6 +1985,14 @@ class App extends React.Component<ExcalidrawProps, AppState> {
let hitElement: ExcalidrawElement | null = null; let hitElement: ExcalidrawElement | null = null;
let hitElementWasAddedToSelection = false; let hitElementWasAddedToSelection = false;
if (this.state.elementType !== "selection") {
this.setState({
selectedElementIds: {},
selectedGroupIds: {},
editingGroupId: null,
});
}
if (this.state.elementType === "selection") { if (this.state.elementType === "selection") {
const elements = globalSceneState.getElements(); const elements = globalSceneState.getElements();
const selectedElements = getSelectedElements(elements, this.state); const selectedElements = getSelectedElements(elements, this.state);
@ -2154,17 +2005,9 @@ class App extends React.Component<ExcalidrawProps, AppState> {
this.state.zoom, this.state.zoom,
event.pointerType, event.pointerType,
); );
if (elementWithResizeHandler) { if (elementWithResizeHandler != null) {
this.setState({ this.setState({ resizingElement: elementWithResizeHandler.element });
resizingElement: elementWithResizeHandler
? elementWithResizeHandler.element
: null,
});
resizeHandle = elementWithResizeHandler.resizeHandle; resizeHandle = elementWithResizeHandler.resizeHandle;
document.documentElement.style.cursor = getCursorForResizingElement(
elementWithResizeHandler,
);
isResizingElements = true;
} }
} else if (selectedElements.length > 1) { } else if (selectedElements.length > 1) {
resizeHandle = getResizeHandlerFromCoords( resizeHandle = getResizeHandlerFromCoords(
@ -2174,14 +2017,12 @@ class App extends React.Component<ExcalidrawProps, AppState> {
this.state.zoom, this.state.zoom,
event.pointerType, event.pointerType,
); );
}
if (resizeHandle) { if (resizeHandle) {
document.documentElement.style.cursor = getCursorForResizingElement({ document.documentElement.style.cursor = getCursorForResizingElement({
resizeHandle, resizeHandle,
}); });
isResizingElements = true; isResizingElements = true;
}
}
if (isResizingElements) {
resizeOffsetXY = getResizeOffsetXY( resizeOffsetXY = getResizeOffsetXY(
resizeHandle, resizeHandle,
selectedElements, selectedElements,
@ -2198,8 +2039,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
selectedElements[0], selectedElements[0],
); );
} }
} } else {
if (!isResizingElements) {
if (this.state.editingLinearElement) { if (this.state.editingLinearElement) {
const ret = LinearElementEditor.handlePointerDown( const ret = LinearElementEditor.handlePointerDown(
event, event,
@ -2222,27 +2062,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
hitElement || hitElement ||
getElementAtPosition(elements, this.state, x, y, this.state.zoom); getElementAtPosition(elements, this.state, x, y, this.state.zoom);
// clear selection if shift is not clicked this.maybeClearSelectionWhenHittingElement(event, hitElement);
if (
!(hitElement && this.state.selectedElementIds[hitElement.id]) &&
!event.shiftKey
) {
this.setState((prevState) => ({
selectedElementIds: {},
selectedGroupIds: {},
editingGroupId:
prevState.editingGroupId &&
hitElement &&
isElementInGroup(hitElement, prevState.editingGroupId)
? prevState.editingGroupId
: null,
}));
const { selectedElementIds } = this.state;
this.setState({
selectedElementIds: {},
previousSelectedElementIds: selectedElementIds,
});
}
// If we click on something // If we click on something
if (hitElement) { if (hitElement) {
@ -2289,12 +2109,6 @@ class App extends React.Component<ExcalidrawProps, AppState> {
previousSelectedElementIds: selectedElementIds, previousSelectedElementIds: selectedElementIds,
}); });
} }
} else {
this.setState({
selectedElementIds: {},
selectedGroupIds: {},
editingGroupId: null,
});
} }
if (this.state.elementType === "text") { if (this.state.elementType === "text") {
@ -2457,21 +2271,21 @@ class App extends React.Component<ExcalidrawProps, AppState> {
if (isOverHorizontalScrollBar) { if (isOverHorizontalScrollBar) {
const x = event.clientX; const x = event.clientX;
const dx = x - lastX; const dx = x - lastCoords.x;
this.setState({ this.setState({
scrollX: normalizeScroll(this.state.scrollX - dx / this.state.zoom), scrollX: normalizeScroll(this.state.scrollX - dx / this.state.zoom),
}); });
lastX = x; lastCoords.x = x;
return; return;
} }
if (isOverVerticalScrollBar) { if (isOverVerticalScrollBar) {
const y = event.clientY; const y = event.clientY;
const dy = y - lastY; const dy = y - lastCoords.y;
this.setState({ this.setState({
scrollY: normalizeScroll(this.state.scrollY - dy / this.state.zoom), scrollY: normalizeScroll(this.state.scrollY - dy / this.state.zoom),
}); });
lastY = y; lastCoords.y = y;
return; return;
} }
@ -2534,13 +2348,13 @@ class App extends React.Component<ExcalidrawProps, AppState> {
(appState) => this.setState(appState), (appState) => this.setState(appState),
x, x,
y, y,
lastX, lastCoords.x,
lastY, lastCoords.y,
); );
if (didDrag) { if (didDrag) {
lastX = x; lastCoords.x = x;
lastY = y; lastCoords.y = y;
return; return;
} }
} }
@ -2905,6 +2719,241 @@ class App extends React.Component<ExcalidrawProps, AppState> {
window.addEventListener(EVENT.POINTER_UP, onPointerUp); window.addEventListener(EVENT.POINTER_UP, onPointerUp);
}; };
private maybeOpenContextMenuAfterPointerDownOnTouchDevices = (
event: React.PointerEvent<HTMLCanvasElement>,
): void => {
// deal with opening context menu on touch devices
if (event.pointerType === "touch") {
touchMoving = false;
// open the context menu with the first touch's clientX and clientY
// if the touch is not moving
touchTimeout = window.setTimeout(() => {
if (!touchMoving) {
this.openContextMenu({
clientX: event.clientX,
clientY: event.clientY,
});
}
}, TOUCH_CTX_MENU_TIMEOUT);
}
};
private maybeCleanupAfterMissingPointerUp(
event: React.PointerEvent<HTMLCanvasElement>,
): void {
if (lastPointerUp !== null) {
// Unfortunately, sometimes we don't get a pointerup after a pointerdown,
// this can happen when a contextual menu or alert is triggered. In order to avoid
// being in a weird state, we clean up on the next pointerdown
lastPointerUp(event);
}
}
// Returns whether the event is a panning
private handleCanvasPanUsingWheelOrSpaceDrag = (
event: React.PointerEvent<HTMLCanvasElement>,
): boolean => {
if (
!(
gesture.pointers.size === 0 &&
(event.button === POINTER_BUTTON.WHEEL ||
(event.button === POINTER_BUTTON.MAIN && isHoldingSpace))
)
) {
return false;
}
isPanning = true;
let nextPastePrevented = false;
const isLinux = /Linux/.test(window.navigator.platform);
document.documentElement.style.cursor = CURSOR_TYPE.GRABBING;
let { clientX: lastX, clientY: lastY } = event;
const onPointerMove = withBatchedUpdates((event: PointerEvent) => {
const deltaX = lastX - event.clientX;
const deltaY = lastY - event.clientY;
lastX = event.clientX;
lastY = event.clientY;
/*
* Prevent paste event if we move while middle clicking on Linux.
* See issue #1383.
*/
if (
isLinux &&
!nextPastePrevented &&
(Math.abs(deltaX) > 1 || Math.abs(deltaY) > 1)
) {
nextPastePrevented = true;
/* Prevent the next paste event */
const preventNextPaste = (event: ClipboardEvent) => {
document.body.removeEventListener(EVENT.PASTE, preventNextPaste);
event.stopPropagation();
};
/*
* Reenable next paste in case of disabled middle click paste for
* any reason:
* - rigth click paste
* - empty clipboard
*/
const enableNextPaste = () => {
setTimeout(() => {
document.body.removeEventListener(EVENT.PASTE, preventNextPaste);
window.removeEventListener(EVENT.POINTER_UP, enableNextPaste);
}, 100);
};
document.body.addEventListener(EVENT.PASTE, preventNextPaste);
window.addEventListener(EVENT.POINTER_UP, enableNextPaste);
}
this.setState({
scrollX: normalizeScroll(this.state.scrollX - deltaX / this.state.zoom),
scrollY: normalizeScroll(this.state.scrollY - deltaY / this.state.zoom),
});
});
const teardown = withBatchedUpdates(
(lastPointerUp = () => {
lastPointerUp = null;
isPanning = false;
if (!isHoldingSpace) {
setCursorForShape(this.state.elementType);
}
this.setState({
cursorButton: "up",
});
this.savePointer(event.clientX, event.clientY, "up");
window.removeEventListener(EVENT.POINTER_MOVE, onPointerMove);
window.removeEventListener(EVENT.POINTER_UP, teardown);
window.removeEventListener(EVENT.BLUR, teardown);
}),
);
window.addEventListener(EVENT.BLUR, teardown);
window.addEventListener(EVENT.POINTER_MOVE, onPointerMove, {
passive: true,
});
window.addEventListener(EVENT.POINTER_UP, teardown);
return true;
};
private updateGestureOnPointerDown(
event: React.PointerEvent<HTMLCanvasElement>,
): void {
gesture.pointers.set(event.pointerId, {
x: event.clientX,
y: event.clientY,
});
if (gesture.pointers.size === 2) {
gesture.lastCenter = getCenter(gesture.pointers);
gesture.initialScale = this.state.zoom;
gesture.initialDistance = getDistance(
Array.from(gesture.pointers.values()),
);
}
}
// Returns whether the event is a dragging a scrollbar
private handleDraggingScrollBar(
event: React.PointerEvent<HTMLCanvasElement>,
lastCoords: { x: number; y: number },
{
isOverHorizontalScrollBar,
isOverVerticalScrollBar,
}: {
isOverHorizontalScrollBar: boolean;
isOverVerticalScrollBar: boolean;
},
): boolean {
if (
!(
(isOverHorizontalScrollBar || isOverVerticalScrollBar) &&
!this.state.multiElement
)
) {
return false;
}
isDraggingScrollBar = true;
lastCoords.x = event.clientX;
lastCoords.y = event.clientY;
const onPointerMove = withBatchedUpdates((event: PointerEvent) => {
const target = event.target;
if (!(target instanceof HTMLElement)) {
return;
}
if (isOverHorizontalScrollBar) {
const x = event.clientX;
const dx = x - lastCoords.x;
this.setState({
scrollX: normalizeScroll(this.state.scrollX - dx / this.state.zoom),
});
lastCoords.x = x;
return;
}
if (isOverVerticalScrollBar) {
const y = event.clientY;
const dy = y - lastCoords.y;
this.setState({
scrollY: normalizeScroll(this.state.scrollY - dy / this.state.zoom),
});
lastCoords.y = y;
}
});
const onPointerUp = withBatchedUpdates(() => {
isDraggingScrollBar = false;
setCursorForShape(this.state.elementType);
lastPointerUp = null;
this.setState({
cursorButton: "up",
});
this.savePointer(event.clientX, event.clientY, "up");
window.removeEventListener(EVENT.POINTER_MOVE, onPointerMove);
window.removeEventListener(EVENT.POINTER_UP, onPointerUp);
});
lastPointerUp = onPointerUp;
window.addEventListener(EVENT.POINTER_MOVE, onPointerMove);
window.addEventListener(EVENT.POINTER_UP, onPointerUp);
return true;
}
private maybeClearSelectionWhenHittingElement(
event: React.PointerEvent<HTMLCanvasElement>,
hitElement: ExcalidrawElement | null,
): void {
const isHittingASelectedElement =
hitElement != null && this.state.selectedElementIds[hitElement.id];
// clear selection if shift is not clicked
if (isHittingASelectedElement || event.shiftKey) {
return;
}
this.setState((prevState) => ({
selectedElementIds: {},
selectedGroupIds: {},
// Continue editing the same group if the user selected a different
// element from it
editingGroupId:
prevState.editingGroupId &&
hitElement != null &&
isElementInGroup(hitElement, prevState.editingGroupId)
? prevState.editingGroupId
: null,
}));
const { selectedElementIds } = this.state;
this.setState({
selectedElementIds: {},
previousSelectedElementIds: selectedElementIds,
});
}
private handleCanvasRef = (canvas: HTMLCanvasElement) => { private handleCanvasRef = (canvas: HTMLCanvasElement) => {
// canvas is null when unmounting // canvas is null when unmounting
if (canvas !== null) { if (canvas !== null) {

View File

@ -81,7 +81,7 @@ export const getElementWithResizeHandler = (
pointerType, pointerType,
); );
return resizeHandle ? { element, resizeHandle } : null; return resizeHandle ? { element, resizeHandle } : null;
}, null as { element: NonDeletedExcalidrawElement; resizeHandle: ReturnType<typeof resizeTest> } | null); }, null as { element: NonDeletedExcalidrawElement; resizeHandle: HandlerRectanglesRet } | null);
}; };
export const getResizeHandlerFromCoords = ( export const getResizeHandlerFromCoords = (

View File

@ -106,13 +106,16 @@ export const isOverScrollBars = (
scrollBars: ScrollBars, scrollBars: ScrollBars,
x: number, x: number,
y: number, y: number,
) => { ): {
isOverHorizontalScrollBar: boolean;
isOverVerticalScrollBar: boolean;
} => {
const [isOverHorizontalScrollBar, isOverVerticalScrollBar] = [ const [isOverHorizontalScrollBar, isOverVerticalScrollBar] = [
scrollBars.horizontal, scrollBars.horizontal,
scrollBars.vertical, scrollBars.vertical,
].map((scrollBar) => { ].map((scrollBar) => {
return ( return (
scrollBar && scrollBar != null &&
scrollBar.x <= x && scrollBar.x <= x &&
x <= scrollBar.x + scrollBar.width && x <= scrollBar.x + scrollBar.width &&
scrollBar.y <= y && scrollBar.y <= y &&