|
@@ -1900,28 +1900,8 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|
|
) => {
|
|
|
event.persist();
|
|
|
|
|
|
- // 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);
|
|
|
- }
|
|
|
-
|
|
|
- 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);
|
|
|
- }
|
|
|
+ this.maybeOpenContextMenuAfterPointerDownOnTouchDevices(event);
|
|
|
+ this.maybeCleanupAfterMissingPointerUp(event);
|
|
|
|
|
|
if (isPanning) {
|
|
|
return;
|
|
@@ -1933,89 +1913,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|
|
});
|
|
|
this.savePointer(event.clientX, event.clientY, "down");
|
|
|
|
|
|
- // pan canvas on wheel button drag or space+drag
|
|
|
- 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);
|
|
|
+ if (this.handleCanvasPanUsingWheelOrSpaceDrag(event)) {
|
|
|
return;
|
|
|
}
|
|
|
|
|
@@ -2027,18 +1925,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
- 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()),
|
|
|
- );
|
|
|
- }
|
|
|
+ this.updateGestureOnPointerDown(event);
|
|
|
|
|
|
// fixes pointermove causing selection of UI texts #32
|
|
|
event.preventDefault();
|
|
@@ -2055,10 +1942,15 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|
|
}
|
|
|
|
|
|
// Handle scrollbars dragging
|
|
|
+ const isOverScrollBarsNow = isOverScrollBars(
|
|
|
+ currentScrollBars,
|
|
|
+ event.clientX,
|
|
|
+ event.clientY,
|
|
|
+ );
|
|
|
const {
|
|
|
isOverHorizontalScrollBar,
|
|
|
isOverVerticalScrollBar,
|
|
|
- } = isOverScrollBars(currentScrollBars, event.clientX, event.clientY);
|
|
|
+ } = isOverScrollBarsNow;
|
|
|
|
|
|
const { x, y } = viewportCoordsToSceneCoords(
|
|
|
event,
|
|
@@ -2066,58 +1958,9 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|
|
this.canvas,
|
|
|
window.devicePixelRatio,
|
|
|
);
|
|
|
- let lastX = x;
|
|
|
- let lastY = y;
|
|
|
-
|
|
|
- if (
|
|
|
- (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;
|
|
|
- }
|
|
|
+ const lastCoords = { x, y };
|
|
|
|
|
|
- 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);
|
|
|
+ if (this.handleDraggingScrollBar(event, lastCoords, isOverScrollBarsNow)) {
|
|
|
return;
|
|
|
}
|
|
|
|
|
@@ -2142,6 +1985,14 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|
|
let hitElement: ExcalidrawElement | null = null;
|
|
|
let hitElementWasAddedToSelection = false;
|
|
|
|
|
|
+ if (this.state.elementType !== "selection") {
|
|
|
+ this.setState({
|
|
|
+ selectedElementIds: {},
|
|
|
+ selectedGroupIds: {},
|
|
|
+ editingGroupId: null,
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
if (this.state.elementType === "selection") {
|
|
|
const elements = globalSceneState.getElements();
|
|
|
const selectedElements = getSelectedElements(elements, this.state);
|
|
@@ -2154,17 +2005,9 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|
|
this.state.zoom,
|
|
|
event.pointerType,
|
|
|
);
|
|
|
- if (elementWithResizeHandler) {
|
|
|
- this.setState({
|
|
|
- resizingElement: elementWithResizeHandler
|
|
|
- ? elementWithResizeHandler.element
|
|
|
- : null,
|
|
|
- });
|
|
|
+ if (elementWithResizeHandler != null) {
|
|
|
+ this.setState({ resizingElement: elementWithResizeHandler.element });
|
|
|
resizeHandle = elementWithResizeHandler.resizeHandle;
|
|
|
- document.documentElement.style.cursor = getCursorForResizingElement(
|
|
|
- elementWithResizeHandler,
|
|
|
- );
|
|
|
- isResizingElements = true;
|
|
|
}
|
|
|
} else if (selectedElements.length > 1) {
|
|
|
resizeHandle = getResizeHandlerFromCoords(
|
|
@@ -2174,14 +2017,12 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|
|
this.state.zoom,
|
|
|
event.pointerType,
|
|
|
);
|
|
|
- if (resizeHandle) {
|
|
|
- document.documentElement.style.cursor = getCursorForResizingElement({
|
|
|
- resizeHandle,
|
|
|
- });
|
|
|
- isResizingElements = true;
|
|
|
- }
|
|
|
}
|
|
|
- if (isResizingElements) {
|
|
|
+ if (resizeHandle) {
|
|
|
+ document.documentElement.style.cursor = getCursorForResizingElement({
|
|
|
+ resizeHandle,
|
|
|
+ });
|
|
|
+ isResizingElements = true;
|
|
|
resizeOffsetXY = getResizeOffsetXY(
|
|
|
resizeHandle,
|
|
|
selectedElements,
|
|
@@ -2198,8 +2039,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|
|
selectedElements[0],
|
|
|
);
|
|
|
}
|
|
|
- }
|
|
|
- if (!isResizingElements) {
|
|
|
+ } else {
|
|
|
if (this.state.editingLinearElement) {
|
|
|
const ret = LinearElementEditor.handlePointerDown(
|
|
|
event,
|
|
@@ -2222,27 +2062,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|
|
hitElement ||
|
|
|
getElementAtPosition(elements, this.state, x, y, this.state.zoom);
|
|
|
|
|
|
- // clear selection if shift is not clicked
|
|
|
- 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,
|
|
|
- });
|
|
|
- }
|
|
|
+ this.maybeClearSelectionWhenHittingElement(event, hitElement);
|
|
|
|
|
|
// If we click on something
|
|
|
if (hitElement) {
|
|
@@ -2289,12 +2109,6 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|
|
previousSelectedElementIds: selectedElementIds,
|
|
|
});
|
|
|
}
|
|
|
- } else {
|
|
|
- this.setState({
|
|
|
- selectedElementIds: {},
|
|
|
- selectedGroupIds: {},
|
|
|
- editingGroupId: null,
|
|
|
- });
|
|
|
}
|
|
|
|
|
|
if (this.state.elementType === "text") {
|
|
@@ -2457,21 +2271,21 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|
|
|
|
|
if (isOverHorizontalScrollBar) {
|
|
|
const x = event.clientX;
|
|
|
- const dx = x - lastX;
|
|
|
+ const dx = x - lastCoords.x;
|
|
|
this.setState({
|
|
|
scrollX: normalizeScroll(this.state.scrollX - dx / this.state.zoom),
|
|
|
});
|
|
|
- lastX = x;
|
|
|
+ lastCoords.x = x;
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
if (isOverVerticalScrollBar) {
|
|
|
const y = event.clientY;
|
|
|
- const dy = y - lastY;
|
|
|
+ const dy = y - lastCoords.y;
|
|
|
this.setState({
|
|
|
scrollY: normalizeScroll(this.state.scrollY - dy / this.state.zoom),
|
|
|
});
|
|
|
- lastY = y;
|
|
|
+ lastCoords.y = y;
|
|
|
return;
|
|
|
}
|
|
|
|
|
@@ -2534,13 +2348,13 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|
|
(appState) => this.setState(appState),
|
|
|
x,
|
|
|
y,
|
|
|
- lastX,
|
|
|
- lastY,
|
|
|
+ lastCoords.x,
|
|
|
+ lastCoords.y,
|
|
|
);
|
|
|
|
|
|
if (didDrag) {
|
|
|
- lastX = x;
|
|
|
- lastY = y;
|
|
|
+ lastCoords.x = x;
|
|
|
+ lastCoords.y = y;
|
|
|
return;
|
|
|
}
|
|
|
}
|
|
@@ -2905,6 +2719,241 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|
|
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) => {
|
|
|
// canvas is null when unmounting
|
|
|
if (canvas !== null) {
|