2020-03-07 10:20:38 -05:00
|
|
|
import React from "react";
|
|
|
|
|
|
|
|
import rough from "roughjs/bin/rough";
|
|
|
|
import { RoughCanvas } from "roughjs/bin/canvas";
|
2020-05-12 20:10:11 +01:00
|
|
|
import { simplify, Point } from "points-on-curve";
|
2020-07-27 17:18:49 +05:30
|
|
|
import { SocketUpdateData } from "../types";
|
2020-03-07 10:20:38 -05:00
|
|
|
|
|
|
|
import {
|
|
|
|
newElement,
|
|
|
|
newTextElement,
|
|
|
|
duplicateElement,
|
|
|
|
isInvisiblySmallElement,
|
|
|
|
isTextElement,
|
|
|
|
textWysiwyg,
|
|
|
|
getCommonBounds,
|
|
|
|
getCursorForResizingElement,
|
|
|
|
getPerfectElementSize,
|
2020-05-28 11:41:34 +02:00
|
|
|
getNormalizedDimensions,
|
2020-03-14 20:46:57 -07:00
|
|
|
getElementMap,
|
|
|
|
getDrawingVersion,
|
|
|
|
getSyncableElements,
|
2020-03-17 20:55:40 +01:00
|
|
|
newLinearElement,
|
2020-04-07 17:49:59 +09:00
|
|
|
resizeElements,
|
2020-08-10 14:16:39 +02:00
|
|
|
getElementWithTransformHandleType,
|
2020-05-09 17:57:00 +09:00
|
|
|
getResizeOffsetXY,
|
2020-05-11 00:41:36 +09:00
|
|
|
getResizeArrowDirection,
|
2020-08-10 14:16:39 +02:00
|
|
|
getTransformHandleTypeFromCoords,
|
2020-04-11 17:10:56 +01:00
|
|
|
isNonDeletedElement,
|
2020-06-25 21:21:27 +02:00
|
|
|
updateTextElement,
|
2020-06-24 00:24:52 +09:00
|
|
|
dragSelectedElements,
|
|
|
|
getDragOffsetXY,
|
|
|
|
dragNewElement,
|
2020-08-08 21:04:15 -07:00
|
|
|
hitTest,
|
2020-08-26 17:37:44 +01:00
|
|
|
isHittingElementBoundingBoxWithoutHittingElement,
|
2020-03-07 10:20:38 -05:00
|
|
|
} from "../element";
|
|
|
|
import {
|
|
|
|
getElementsWithinSelection,
|
|
|
|
isOverScrollBars,
|
2020-08-26 17:37:44 +01:00
|
|
|
getElementsAtPosition,
|
2020-03-07 10:20:38 -05:00
|
|
|
getElementContainingPosition,
|
|
|
|
getNormalizedZoom,
|
|
|
|
getSelectedElements,
|
|
|
|
isSomeElementSelected,
|
2020-04-03 21:22:26 +02:00
|
|
|
calculateScrollCenter,
|
2020-03-07 10:20:38 -05:00
|
|
|
} from "../scene";
|
basic Socket.io implementation of collaborative editing (#879)
* Enable collaborative syncing for elements
* Don't fall back to local storage if using a room, as that is confusing
* Use remote socket server
* Send updates to new users when they join
* ~
* add mouse tracking
* enable collaboration, rooms, and mouse tracking
* fix syncing bugs and add a button to start syncing mid session
* enable collaboration, rooms, and mouse tracking
* fix syncing bugs and add a button to start syncing mid session
* Add Live button and app state to support tracking collaborator counts
* Enable collaborative syncing for elements
* add mouse tracking
* enable collaboration, rooms, and mouse tracking
* fix syncing bugs and add a button to start syncing mid session
* fix syncing bugs and add a button to start syncing mid session
* Add Live button and app state to support tracking collaborator counts
* prettier
* Fix bug with remote pointers not changing on scroll
* Enable collaborative syncing for elements
* add mouse tracking
* enable collaboration, rooms, and mouse tracking
* fix syncing bugs and add a button to start syncing mid session
* enable collaboration, rooms, and mouse tracking
* fix syncing bugs and add a button to start syncing mid session
* Add Live button and app state to support tracking collaborator counts
* enable collaboration, rooms, and mouse tracking
* fix syncing bugs and add a button to start syncing mid session
* fix syncing bugs and add a button to start syncing mid session
* Fix bug with remote pointers not changing on scroll
* remove UI for collaboration
* remove link
* clean up lingering unused UI
* set random IV passed per encrypted message, reduce room id length, refactored socket broadcasting API, rename room_id to room, removed throttling of pointer movement
* fix package.json conflict
2020-03-09 08:48:25 -07:00
|
|
|
import {
|
|
|
|
decryptAESGEM,
|
|
|
|
loadScene,
|
|
|
|
loadFromBlob,
|
|
|
|
SOCKET_SERVER,
|
2020-03-14 21:25:07 +01:00
|
|
|
SocketUpdateDataSource,
|
2020-03-14 22:53:18 +01:00
|
|
|
exportCanvas,
|
basic Socket.io implementation of collaborative editing (#879)
* Enable collaborative syncing for elements
* Don't fall back to local storage if using a room, as that is confusing
* Use remote socket server
* Send updates to new users when they join
* ~
* add mouse tracking
* enable collaboration, rooms, and mouse tracking
* fix syncing bugs and add a button to start syncing mid session
* enable collaboration, rooms, and mouse tracking
* fix syncing bugs and add a button to start syncing mid session
* Add Live button and app state to support tracking collaborator counts
* Enable collaborative syncing for elements
* add mouse tracking
* enable collaboration, rooms, and mouse tracking
* fix syncing bugs and add a button to start syncing mid session
* fix syncing bugs and add a button to start syncing mid session
* Add Live button and app state to support tracking collaborator counts
* prettier
* Fix bug with remote pointers not changing on scroll
* Enable collaborative syncing for elements
* add mouse tracking
* enable collaboration, rooms, and mouse tracking
* fix syncing bugs and add a button to start syncing mid session
* enable collaboration, rooms, and mouse tracking
* fix syncing bugs and add a button to start syncing mid session
* Add Live button and app state to support tracking collaborator counts
* enable collaboration, rooms, and mouse tracking
* fix syncing bugs and add a button to start syncing mid session
* fix syncing bugs and add a button to start syncing mid session
* Fix bug with remote pointers not changing on scroll
* remove UI for collaboration
* remove link
* clean up lingering unused UI
* set random IV passed per encrypted message, reduce room id length, refactored socket broadcasting API, rename room_id to room, removed throttling of pointer movement
* fix package.json conflict
2020-03-09 08:48:25 -07:00
|
|
|
} from "../data";
|
2020-04-12 16:24:52 +05:30
|
|
|
import Portal from "./Portal";
|
2020-03-07 10:20:38 -05:00
|
|
|
|
|
|
|
import { renderScene } from "../renderer";
|
2020-07-11 18:43:20 +05:30
|
|
|
import { AppState, GestureEvent, Gesture, ExcalidrawProps } from "../types";
|
2020-06-25 21:21:27 +02:00
|
|
|
import {
|
|
|
|
ExcalidrawElement,
|
|
|
|
ExcalidrawTextElement,
|
|
|
|
NonDeleted,
|
2020-07-09 09:30:38 -07:00
|
|
|
ExcalidrawGenericElement,
|
2020-08-08 21:04:15 -07:00
|
|
|
ExcalidrawLinearElement,
|
|
|
|
ExcalidrawBindableElement,
|
2020-06-25 21:21:27 +02:00
|
|
|
} from "../element/types";
|
2020-03-07 10:20:38 -05:00
|
|
|
|
2020-06-24 00:24:52 +09:00
|
|
|
import { distance2d, isPathALoop, getGridPoint } from "../math";
|
2020-04-09 01:46:47 -07:00
|
|
|
|
2020-03-07 10:20:38 -05:00
|
|
|
import {
|
|
|
|
isWritableElement,
|
|
|
|
isInputLike,
|
|
|
|
isToolIcon,
|
|
|
|
debounce,
|
|
|
|
distance,
|
|
|
|
resetCursor,
|
|
|
|
viewportCoordsToSceneCoords,
|
|
|
|
sceneCoordsToViewportCoords,
|
2020-04-06 22:26:54 +02:00
|
|
|
setCursorForShape,
|
2020-07-09 09:30:38 -07:00
|
|
|
tupleToCoors,
|
2020-03-07 10:20:38 -05:00
|
|
|
} from "../utils";
|
2020-04-22 16:57:17 +01:00
|
|
|
import {
|
|
|
|
KEYS,
|
|
|
|
isArrowKey,
|
|
|
|
getResizeCenterPointKey,
|
|
|
|
getResizeWithSidesSameLengthKey,
|
2020-06-24 00:24:52 +09:00
|
|
|
getRotateWithDiscreteAngleKey,
|
2020-04-22 16:57:17 +01:00
|
|
|
} from "../keys";
|
2020-03-07 10:20:38 -05:00
|
|
|
|
2020-07-24 15:47:46 +02:00
|
|
|
import { findShapeByKey } from "../shapes";
|
2020-03-18 20:44:05 +01:00
|
|
|
import { createHistory, SceneHistory } from "../history";
|
2020-03-07 10:20:38 -05:00
|
|
|
|
|
|
|
import ContextMenu from "./ContextMenu";
|
|
|
|
|
|
|
|
import { ActionManager } from "../actions/manager";
|
|
|
|
import "../actions";
|
|
|
|
import { actions } from "../actions/register";
|
|
|
|
|
|
|
|
import { ActionResult } from "../actions/types";
|
|
|
|
import { getDefaultAppState } from "../appState";
|
2020-05-27 16:46:11 +02:00
|
|
|
import { t, getLanguage } from "../i18n";
|
2020-03-07 10:20:38 -05:00
|
|
|
|
2020-03-14 22:53:18 +01:00
|
|
|
import {
|
|
|
|
copyToAppClipboard,
|
|
|
|
getClipboardContent,
|
|
|
|
probablySupportsClipboardBlob,
|
2020-04-05 16:13:17 -07:00
|
|
|
probablySupportsClipboardWriteText,
|
2020-03-14 22:53:18 +01:00
|
|
|
} from "../clipboard";
|
2020-03-07 10:20:38 -05:00
|
|
|
import { normalizeScroll } from "../scene";
|
|
|
|
import { getCenter, getDistance } from "../gesture";
|
|
|
|
import { createUndoAction, createRedoAction } from "../actions/actionHistory";
|
2020-04-12 06:12:02 +05:30
|
|
|
|
2020-03-07 10:20:38 -05:00
|
|
|
import {
|
|
|
|
CURSOR_TYPE,
|
|
|
|
ELEMENT_SHIFT_TRANSLATE_AMOUNT,
|
|
|
|
ELEMENT_TRANSLATE_AMOUNT,
|
|
|
|
POINTER_BUTTON,
|
|
|
|
DRAGGING_THRESHOLD,
|
|
|
|
TEXT_TO_CENTER_SNAP_THRESHOLD,
|
2020-04-09 01:46:47 -07:00
|
|
|
LINE_CONFIRM_THRESHOLD,
|
2020-04-12 06:12:02 +05:30
|
|
|
SCENE,
|
|
|
|
EVENT,
|
|
|
|
ENV,
|
2020-05-30 18:56:17 +05:30
|
|
|
CANVAS_ONLY_ACTIONS,
|
2020-06-25 21:21:27 +02:00
|
|
|
DEFAULT_VERTICAL_ALIGN,
|
2020-06-24 00:24:52 +09:00
|
|
|
GRID_SIZE,
|
2020-07-08 22:55:26 +02:00
|
|
|
LOCAL_STORAGE_KEY_COLLAB_FORCE_FLAG,
|
2020-03-07 10:20:38 -05:00
|
|
|
} from "../constants";
|
2020-04-12 06:12:02 +05:30
|
|
|
import {
|
2020-07-11 18:43:20 +05:30
|
|
|
INITIAL_SCENE_UPDATE_TIMEOUT,
|
2020-04-12 06:12:02 +05:30
|
|
|
TAP_TWICE_TIMEOUT,
|
2020-05-30 18:56:17 +05:30
|
|
|
SYNC_FULL_SCENE_INTERVAL_MS,
|
2020-07-02 22:12:56 +01:00
|
|
|
TOUCH_CTX_MENU_TIMEOUT,
|
2020-04-12 06:12:02 +05:30
|
|
|
} from "../time_constants";
|
|
|
|
|
2020-04-18 01:54:19 +05:30
|
|
|
import LayerUI from "./LayerUI";
|
2020-04-04 16:02:16 +02:00
|
|
|
import { ScrollBars, SceneState } from "../scene/types";
|
basic Socket.io implementation of collaborative editing (#879)
* Enable collaborative syncing for elements
* Don't fall back to local storage if using a room, as that is confusing
* Use remote socket server
* Send updates to new users when they join
* ~
* add mouse tracking
* enable collaboration, rooms, and mouse tracking
* fix syncing bugs and add a button to start syncing mid session
* enable collaboration, rooms, and mouse tracking
* fix syncing bugs and add a button to start syncing mid session
* Add Live button and app state to support tracking collaborator counts
* Enable collaborative syncing for elements
* add mouse tracking
* enable collaboration, rooms, and mouse tracking
* fix syncing bugs and add a button to start syncing mid session
* fix syncing bugs and add a button to start syncing mid session
* Add Live button and app state to support tracking collaborator counts
* prettier
* Fix bug with remote pointers not changing on scroll
* Enable collaborative syncing for elements
* add mouse tracking
* enable collaboration, rooms, and mouse tracking
* fix syncing bugs and add a button to start syncing mid session
* enable collaboration, rooms, and mouse tracking
* fix syncing bugs and add a button to start syncing mid session
* Add Live button and app state to support tracking collaborator counts
* enable collaboration, rooms, and mouse tracking
* fix syncing bugs and add a button to start syncing mid session
* fix syncing bugs and add a button to start syncing mid session
* Fix bug with remote pointers not changing on scroll
* remove UI for collaboration
* remove link
* clean up lingering unused UI
* set random IV passed per encrypted message, reduce room id length, refactored socket broadcasting API, rename room_id to room, removed throttling of pointer movement
* fix package.json conflict
2020-03-09 08:48:25 -07:00
|
|
|
import { generateCollaborationLink, getCollaborationLinkData } from "../data";
|
2020-03-10 20:11:02 -07:00
|
|
|
import { mutateElement, newElementWith } from "../element/mutateElement";
|
2020-03-14 21:48:51 -07:00
|
|
|
import { invalidateShapeForElement } from "../renderer/renderElement";
|
2020-03-16 19:07:47 -07:00
|
|
|
import { unstable_batchedUpdates } from "react-dom";
|
2020-08-08 21:04:15 -07:00
|
|
|
import {
|
|
|
|
isLinearElement,
|
|
|
|
isLinearElementType,
|
|
|
|
isBindingElement,
|
|
|
|
isBindingElementType,
|
|
|
|
isBindableElement,
|
|
|
|
} from "../element/typeChecks";
|
2020-05-30 22:48:57 +02:00
|
|
|
import { actionFinalize, actionDeleteSelected } from "../actions";
|
2020-08-26 16:15:54 +05:30
|
|
|
import { loadLibrary } from "../data/localStorage";
|
2020-03-16 19:07:47 -07:00
|
|
|
|
2020-05-07 14:13:18 -07:00
|
|
|
import throttle from "lodash.throttle";
|
2020-06-01 11:35:44 +02:00
|
|
|
import { LinearElementEditor } from "../element/linearElementEditor";
|
2020-05-26 13:07:46 -07:00
|
|
|
import {
|
|
|
|
getSelectedGroupIds,
|
2020-08-26 17:37:44 +01:00
|
|
|
isSelectedViaGroup,
|
2020-05-26 13:07:46 -07:00
|
|
|
selectGroupsForSelectedElements,
|
|
|
|
isElementInGroup,
|
|
|
|
getSelectedGroupIdForElement,
|
2020-08-26 17:37:44 +01:00
|
|
|
getElementsInGroup,
|
2020-05-26 13:07:46 -07:00
|
|
|
} from "../groups";
|
2020-07-27 15:29:19 +03:00
|
|
|
import { Library } from "../data/library";
|
2020-07-30 14:50:59 +05:30
|
|
|
import Scene from "../scene/Scene";
|
2020-08-08 21:04:15 -07:00
|
|
|
import {
|
|
|
|
getHoveredElementForBinding,
|
|
|
|
maybeBindLinearElement,
|
|
|
|
getEligibleElementsForBinding,
|
|
|
|
bindOrUnbindSelectedElements,
|
|
|
|
unbindLinearElements,
|
|
|
|
fixBindingsAfterDuplication,
|
|
|
|
maybeBindBindableElement,
|
|
|
|
getElligibleElementForBindingElementAtCoors,
|
|
|
|
fixBindingsAfterDeletion,
|
|
|
|
isLinearElementSimpleAndAlreadyBound,
|
|
|
|
isBindingEnabled,
|
|
|
|
} from "../element/binding";
|
2020-08-10 14:16:39 +02:00
|
|
|
import { MaybeTransformHandleType } from "../element/transformHandles";
|
2020-05-07 14:13:18 -07:00
|
|
|
|
2020-03-19 14:51:05 +01:00
|
|
|
/**
|
|
|
|
* @param func handler taking at most single parameter (event).
|
|
|
|
*/
|
2020-05-20 16:21:37 +03:00
|
|
|
const withBatchedUpdates = <
|
2020-03-16 19:07:47 -07:00
|
|
|
TFunction extends ((event: any) => void) | (() => void)
|
2020-05-20 16:21:37 +03:00
|
|
|
>(
|
|
|
|
func: Parameters<TFunction>["length"] extends 0 | 1 ? TFunction : never,
|
|
|
|
) =>
|
|
|
|
((event) => {
|
2020-04-14 12:30:58 +03:00
|
|
|
unstable_batchedUpdates(func as TFunction, event);
|
|
|
|
}) as TFunction;
|
2020-03-07 10:20:38 -05:00
|
|
|
|
|
|
|
const { history } = createHistory();
|
|
|
|
|
2020-04-04 14:55:36 +02:00
|
|
|
let didTapTwice: boolean = false;
|
|
|
|
let tappedTwiceTimer = 0;
|
2020-03-07 10:20:38 -05:00
|
|
|
let cursorX = 0;
|
|
|
|
let cursorY = 0;
|
|
|
|
let isHoldingSpace: boolean = false;
|
|
|
|
let isPanning: boolean = false;
|
|
|
|
let isDraggingScrollBar: boolean = false;
|
|
|
|
let currentScrollBars: ScrollBars = { horizontal: null, vertical: null };
|
2020-07-02 22:12:56 +01:00
|
|
|
let touchTimeout = 0;
|
|
|
|
let touchMoving = false;
|
2020-03-07 10:20:38 -05:00
|
|
|
|
|
|
|
let lastPointerUp: ((event: any) => void) | null = null;
|
|
|
|
const gesture: Gesture = {
|
2020-03-08 19:25:16 -07:00
|
|
|
pointers: new Map(),
|
2020-03-07 10:20:38 -05:00
|
|
|
lastCenter: null,
|
|
|
|
initialDistance: null,
|
|
|
|
initialScale: null,
|
|
|
|
};
|
|
|
|
|
2020-07-09 09:30:38 -07:00
|
|
|
type PointerDownState = Readonly<{
|
|
|
|
// The first position at which pointerDown happened
|
|
|
|
origin: Readonly<{ x: number; y: number }>;
|
2020-07-09 14:15:42 -07:00
|
|
|
// Same as "origin" but snapped to the grid, if grid is on
|
|
|
|
originInGrid: Readonly<{ x: number; y: number }>;
|
|
|
|
// Scrollbar checks
|
|
|
|
scrollbars: ReturnType<typeof isOverScrollBars>;
|
2020-07-09 09:30:38 -07:00
|
|
|
// The previous pointer position
|
|
|
|
lastCoords: { x: number; y: number };
|
|
|
|
resize: {
|
|
|
|
// Handle when resizing, might change during the pointer interaction
|
2020-08-10 14:16:39 +02:00
|
|
|
handleType: MaybeTransformHandleType;
|
2020-07-09 09:30:38 -07:00
|
|
|
// This is determined on the initial pointer down event
|
|
|
|
isResizing: boolean;
|
|
|
|
// This is determined on the initial pointer down event
|
|
|
|
offset: { x: number; y: number };
|
|
|
|
// This is determined on the initial pointer down event
|
|
|
|
arrowDirection: "origin" | "end";
|
2020-07-26 19:21:38 +09:00
|
|
|
// This is a center point of selected elements determined on the initial pointer down event (for rotation only)
|
|
|
|
center: { x: number; y: number };
|
|
|
|
// This is a list of selected elements determined on the initial pointer down event (for rotation only)
|
|
|
|
originalElements: readonly NonDeleted<ExcalidrawElement>[];
|
2020-07-09 09:30:38 -07:00
|
|
|
};
|
|
|
|
hit: {
|
|
|
|
// The element the pointer is "hitting", is determined on the initial
|
|
|
|
// pointer down event
|
2020-08-26 17:37:44 +01:00
|
|
|
element: NonDeleted<ExcalidrawElement> | null;
|
|
|
|
// The elements the pointer is "hitting", is determined on the initial
|
|
|
|
// pointer down event
|
|
|
|
allHitElements: NonDeleted<ExcalidrawElement>[];
|
2020-07-09 09:30:38 -07:00
|
|
|
// This is determined on the initial pointer down event
|
|
|
|
wasAddedToSelection: boolean;
|
2020-07-09 14:15:42 -07:00
|
|
|
// Whether selected element(s) were duplicated, might change during the
|
2020-08-26 17:37:44 +01:00
|
|
|
// pointer interaction
|
2020-07-09 14:15:42 -07:00
|
|
|
hasBeenDuplicated: boolean;
|
2020-08-26 17:37:44 +01:00
|
|
|
hasHitCommonBoundingBoxOfSelectedElements: boolean;
|
2020-07-09 14:15:42 -07:00
|
|
|
};
|
|
|
|
drag: {
|
|
|
|
// Might change during the pointer interation
|
|
|
|
hasOccurred: boolean;
|
|
|
|
// Might change during the pointer interation
|
|
|
|
offset: { x: number; y: number } | null;
|
|
|
|
};
|
|
|
|
// We need to have these in the state so that we can unsubscribe them
|
|
|
|
eventListeners: {
|
|
|
|
// It's defined on the initial pointer down event
|
|
|
|
onMove: null | ((event: PointerEvent) => void);
|
|
|
|
// It's defined on the initial pointer down event
|
|
|
|
onUp: null | ((event: PointerEvent) => void);
|
2020-07-09 09:30:38 -07:00
|
|
|
};
|
|
|
|
}>;
|
|
|
|
|
2020-07-07 20:40:39 +05:30
|
|
|
class App extends React.Component<ExcalidrawProps, AppState> {
|
2020-04-07 15:29:43 -07:00
|
|
|
canvas: HTMLCanvasElement | null = null;
|
|
|
|
rc: RoughCanvas | null = null;
|
2020-04-27 10:56:08 -07:00
|
|
|
portal: Portal = new Portal(this);
|
2020-03-14 20:46:57 -07:00
|
|
|
lastBroadcastedOrReceivedSceneVersion: number = -1;
|
2020-05-07 14:13:18 -07:00
|
|
|
broadcastedElementVersions: Map<string, number> = new Map();
|
2020-05-30 18:56:17 +05:30
|
|
|
unmounted: boolean = false;
|
2020-03-07 10:20:38 -05:00
|
|
|
actionManager: ActionManager;
|
2020-07-27 17:18:49 +05:30
|
|
|
private excalidrawRef: any;
|
2020-03-26 18:28:26 +01:00
|
|
|
|
2020-07-07 20:40:39 +05:30
|
|
|
public static defaultProps: Partial<ExcalidrawProps> = {
|
|
|
|
width: window.innerWidth,
|
|
|
|
height: window.innerHeight,
|
2020-03-26 18:28:26 +01:00
|
|
|
};
|
2020-07-30 14:50:59 +05:30
|
|
|
private scene: Scene;
|
2020-03-26 18:28:26 +01:00
|
|
|
|
2020-07-11 18:43:20 +05:30
|
|
|
constructor(props: ExcalidrawProps) {
|
2020-03-07 10:20:38 -05:00
|
|
|
super(props);
|
2020-07-07 20:40:39 +05:30
|
|
|
const defaultAppState = getDefaultAppState();
|
|
|
|
|
2020-08-20 16:45:20 +02:00
|
|
|
const { width, height, user } = props;
|
2020-07-07 20:40:39 +05:30
|
|
|
this.state = {
|
|
|
|
...defaultAppState,
|
|
|
|
isLoading: true,
|
|
|
|
width,
|
|
|
|
height,
|
2020-08-20 16:45:20 +02:00
|
|
|
username: user?.name || "",
|
2020-07-27 17:18:49 +05:30
|
|
|
...this.getCanvasOffsets(),
|
2020-07-07 20:40:39 +05:30
|
|
|
};
|
|
|
|
|
2020-07-30 14:50:59 +05:30
|
|
|
this.scene = new Scene();
|
2020-07-27 17:18:49 +05:30
|
|
|
this.excalidrawRef = React.createRef();
|
2020-03-07 10:20:38 -05:00
|
|
|
this.actionManager = new ActionManager(
|
|
|
|
this.syncActionResult,
|
|
|
|
() => this.state,
|
2020-07-30 14:50:59 +05:30
|
|
|
() => this.scene.getElementsIncludingDeleted(),
|
2020-03-07 10:20:38 -05:00
|
|
|
);
|
|
|
|
this.actionManager.registerAll(actions);
|
|
|
|
|
|
|
|
this.actionManager.registerAction(createUndoAction(history));
|
|
|
|
this.actionManager.registerAction(createRedoAction(history));
|
|
|
|
}
|
|
|
|
|
2020-03-22 10:24:50 -07:00
|
|
|
public render() {
|
2020-07-07 20:40:39 +05:30
|
|
|
const {
|
|
|
|
zenModeEnabled,
|
|
|
|
width: canvasDOMWidth,
|
|
|
|
height: canvasDOMHeight,
|
2020-07-27 17:18:49 +05:30
|
|
|
offsetTop,
|
|
|
|
offsetLeft,
|
2020-07-07 20:40:39 +05:30
|
|
|
} = this.state;
|
2020-03-22 10:24:50 -07:00
|
|
|
|
2020-08-26 16:15:54 +05:30
|
|
|
const { onUsernameChange } = this.props;
|
2020-03-22 10:24:50 -07:00
|
|
|
const canvasScale = window.devicePixelRatio;
|
|
|
|
|
|
|
|
const canvasWidth = canvasDOMWidth * canvasScale;
|
|
|
|
const canvasHeight = canvasDOMHeight * canvasScale;
|
|
|
|
|
|
|
|
return (
|
2020-07-27 17:18:49 +05:30
|
|
|
<div
|
|
|
|
className="excalidraw"
|
|
|
|
ref={this.excalidrawRef}
|
|
|
|
style={{
|
|
|
|
width: canvasDOMWidth,
|
|
|
|
height: canvasDOMHeight,
|
|
|
|
top: offsetTop,
|
|
|
|
left: offsetLeft,
|
|
|
|
}}
|
|
|
|
>
|
2020-03-22 10:24:50 -07:00
|
|
|
<LayerUI
|
|
|
|
canvas={this.canvas}
|
|
|
|
appState={this.state}
|
|
|
|
setAppState={this.setAppState}
|
|
|
|
actionManager={this.actionManager}
|
2020-07-30 14:50:59 +05:30
|
|
|
elements={this.scene.getElements()}
|
2020-04-07 15:29:43 -07:00
|
|
|
onRoomCreate={this.openPortal}
|
|
|
|
onRoomDestroy={this.closePortal}
|
2020-04-11 17:13:10 +01:00
|
|
|
onUsernameChange={(username) => {
|
2020-08-26 16:15:54 +05:30
|
|
|
onUsernameChange && onUsernameChange(username);
|
|
|
|
this.setState({ username });
|
2020-04-11 17:13:10 +01:00
|
|
|
}}
|
2020-03-24 19:51:49 +09:00
|
|
|
onLockToggle={this.toggleLock}
|
2020-07-10 02:20:23 -07:00
|
|
|
onInsertShape={(elements) =>
|
|
|
|
this.addElementsFromPasteOrLibrary(elements)
|
|
|
|
}
|
2020-04-25 18:43:02 +05:30
|
|
|
zenModeEnabled={zenModeEnabled}
|
|
|
|
toggleZenMode={this.toggleZenMode}
|
2020-05-27 16:46:11 +02:00
|
|
|
lng={getLanguage().lng}
|
2020-03-22 10:24:50 -07:00
|
|
|
/>
|
|
|
|
<main>
|
|
|
|
<canvas
|
|
|
|
id="canvas"
|
|
|
|
style={{
|
|
|
|
width: canvasDOMWidth,
|
|
|
|
height: canvasDOMHeight,
|
|
|
|
}}
|
|
|
|
width={canvasWidth}
|
|
|
|
height={canvasHeight}
|
2020-04-04 14:55:36 +02:00
|
|
|
ref={this.handleCanvasRef}
|
2020-03-22 10:24:50 -07:00
|
|
|
onContextMenu={this.handleCanvasContextMenu}
|
|
|
|
onPointerDown={this.handleCanvasPointerDown}
|
|
|
|
onDoubleClick={this.handleCanvasDoubleClick}
|
|
|
|
onPointerMove={this.handleCanvasPointerMove}
|
|
|
|
onPointerUp={this.removePointer}
|
|
|
|
onPointerCancel={this.removePointer}
|
2020-07-02 22:12:56 +01:00
|
|
|
onTouchMove={this.handleTouchMove}
|
2020-04-04 15:27:53 +02:00
|
|
|
onDrop={this.handleCanvasOnDrop}
|
2020-03-22 10:24:50 -07:00
|
|
|
>
|
|
|
|
{t("labels.drawingCanvas")}
|
|
|
|
</canvas>
|
|
|
|
</main>
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2020-07-07 13:53:44 +02:00
|
|
|
private syncActionResult = withBatchedUpdates(
|
|
|
|
(actionResult: ActionResult) => {
|
|
|
|
if (this.unmounted || actionResult === false) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
let editingElement: AppState["editingElement"] | null = null;
|
|
|
|
if (actionResult.elements) {
|
|
|
|
actionResult.elements.forEach((element) => {
|
|
|
|
if (
|
|
|
|
this.state.editingElement?.id === element.id &&
|
|
|
|
this.state.editingElement !== element &&
|
|
|
|
isNonDeletedElement(element)
|
|
|
|
) {
|
|
|
|
editingElement = element;
|
|
|
|
}
|
|
|
|
});
|
2020-07-30 14:50:59 +05:30
|
|
|
this.scene.replaceAllElements(actionResult.elements);
|
2020-07-07 13:53:44 +02:00
|
|
|
if (actionResult.commitToHistory) {
|
|
|
|
history.resumeRecording();
|
2020-04-11 17:10:56 +01:00
|
|
|
}
|
2020-03-07 10:20:38 -05:00
|
|
|
}
|
|
|
|
|
2020-07-07 13:53:44 +02:00
|
|
|
if (actionResult.appState || editingElement) {
|
|
|
|
if (actionResult.commitToHistory) {
|
|
|
|
history.resumeRecording();
|
|
|
|
}
|
|
|
|
this.setState(
|
|
|
|
(state) => ({
|
|
|
|
...actionResult.appState,
|
|
|
|
editingElement:
|
|
|
|
editingElement || actionResult.appState?.editingElement || null,
|
|
|
|
isCollaborating: state.isCollaborating,
|
|
|
|
collaborators: state.collaborators,
|
2020-07-27 17:18:49 +05:30
|
|
|
width: state.width,
|
|
|
|
height: state.height,
|
|
|
|
offsetTop: state.offsetTop,
|
|
|
|
offsetLeft: state.offsetLeft,
|
2020-07-07 13:53:44 +02:00
|
|
|
}),
|
|
|
|
() => {
|
|
|
|
if (actionResult.syncHistory) {
|
|
|
|
history.setCurrentState(
|
|
|
|
this.state,
|
2020-07-30 14:50:59 +05:30
|
|
|
this.scene.getElementsIncludingDeleted(),
|
2020-07-07 13:53:44 +02:00
|
|
|
);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
);
|
2020-03-07 10:20:38 -05:00
|
|
|
}
|
2020-07-07 13:53:44 +02:00
|
|
|
},
|
|
|
|
);
|
2020-03-07 10:20:38 -05:00
|
|
|
|
2020-03-22 10:24:50 -07:00
|
|
|
// Lifecycle
|
|
|
|
|
2020-03-26 18:28:26 +01:00
|
|
|
private onBlur = withBatchedUpdates(() => {
|
2020-03-22 10:24:50 -07:00
|
|
|
isHoldingSpace = false;
|
2020-08-08 21:04:15 -07:00
|
|
|
this.setState({ isBindingEnabled: true });
|
2020-03-22 10:24:50 -07:00
|
|
|
});
|
|
|
|
|
2020-03-26 18:28:26 +01:00
|
|
|
private onUnload = () => {
|
|
|
|
this.destroySocketClient();
|
|
|
|
this.onBlur();
|
|
|
|
};
|
|
|
|
|
2020-03-23 13:05:07 +02:00
|
|
|
private disableEvent: EventHandlerNonNull = (event) => {
|
2020-03-22 10:24:50 -07:00
|
|
|
event.preventDefault();
|
|
|
|
};
|
2020-03-26 18:28:26 +01:00
|
|
|
|
2020-05-08 10:42:51 +02:00
|
|
|
private onFontLoaded = () => {
|
2020-07-30 14:50:59 +05:30
|
|
|
this.scene.getElementsIncludingDeleted().forEach((element) => {
|
2020-05-08 10:42:51 +02:00
|
|
|
if (isTextElement(element)) {
|
|
|
|
invalidateShapeForElement(element);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
this.onSceneUpdated();
|
|
|
|
};
|
|
|
|
|
2020-07-08 22:55:26 +02:00
|
|
|
private shouldForceLoadScene(
|
|
|
|
scene: ResolutionType<typeof loadScene>,
|
|
|
|
): boolean {
|
|
|
|
if (!scene.elements.length) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
const roomMatch = getCollaborationLinkData(window.location.href);
|
|
|
|
|
|
|
|
if (!roomMatch) {
|
|
|
|
return false;
|
|
|
|
}
|
2020-07-17 18:39:23 +02:00
|
|
|
|
|
|
|
let collabForceLoadFlag;
|
|
|
|
try {
|
|
|
|
collabForceLoadFlag = localStorage?.getItem(
|
|
|
|
LOCAL_STORAGE_KEY_COLLAB_FORCE_FLAG,
|
|
|
|
);
|
|
|
|
} catch {}
|
|
|
|
|
2020-07-08 22:55:26 +02:00
|
|
|
if (collabForceLoadFlag) {
|
|
|
|
try {
|
|
|
|
const {
|
|
|
|
room: previousRoom,
|
|
|
|
timestamp,
|
|
|
|
}: { room: string; timestamp: number } = JSON.parse(
|
|
|
|
collabForceLoadFlag,
|
|
|
|
);
|
|
|
|
// if loading same room as the one previously unloaded within 15sec
|
|
|
|
// force reload without prompting
|
|
|
|
if (previousRoom === roomMatch[1] && Date.now() - timestamp < 15000) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
} catch {}
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2020-03-26 18:28:26 +01:00
|
|
|
private initializeScene = async () => {
|
|
|
|
const searchParams = new URLSearchParams(window.location.search);
|
|
|
|
const id = searchParams.get("id");
|
|
|
|
const jsonMatch = window.location.hash.match(
|
|
|
|
/^#json=([0-9]+),([a-zA-Z0-9_-]+)$/,
|
|
|
|
);
|
|
|
|
|
2020-07-10 07:16:28 +02:00
|
|
|
if (!this.state.isLoading) {
|
|
|
|
this.setState({ isLoading: true });
|
|
|
|
}
|
|
|
|
|
2020-08-20 16:45:20 +02:00
|
|
|
let scene = await loadScene(null, null, this.props.initialData);
|
2020-07-08 22:55:26 +02:00
|
|
|
|
|
|
|
let isCollaborationScene = !!getCollaborationLinkData(window.location.href);
|
|
|
|
const isExternalScene = !!(id || jsonMatch || isCollaborationScene);
|
|
|
|
|
2020-08-26 16:15:54 +05:30
|
|
|
if (isExternalScene) {
|
2020-07-08 22:55:26 +02:00
|
|
|
if (
|
|
|
|
this.shouldForceLoadScene(scene) ||
|
|
|
|
window.confirm(t("alerts.loadSceneOverridePrompt"))
|
|
|
|
) {
|
|
|
|
// Backwards compatibility with legacy url format
|
|
|
|
if (id) {
|
|
|
|
scene = await loadScene(id);
|
|
|
|
} else if (jsonMatch) {
|
|
|
|
scene = await loadScene(jsonMatch[1], jsonMatch[2]);
|
|
|
|
}
|
|
|
|
if (!isCollaborationScene) {
|
|
|
|
window.history.replaceState({}, "Excalidraw", window.location.origin);
|
|
|
|
}
|
2020-03-26 18:28:26 +01:00
|
|
|
} else {
|
2020-07-20 12:53:53 +02:00
|
|
|
// https://github.com/excalidraw/excalidraw/issues/1919
|
|
|
|
if (document.hidden) {
|
|
|
|
window.addEventListener("focus", () => this.initializeScene(), {
|
|
|
|
once: true,
|
|
|
|
});
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2020-07-08 22:55:26 +02:00
|
|
|
isCollaborationScene = false;
|
|
|
|
window.history.replaceState({}, "Excalidraw", window.location.origin);
|
2020-03-26 18:28:26 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (this.state.isLoading) {
|
|
|
|
this.setState({ isLoading: false });
|
|
|
|
}
|
|
|
|
|
|
|
|
if (isCollaborationScene) {
|
|
|
|
this.initializeSocketClient({ showLoadingState: true });
|
2020-07-08 22:55:26 +02:00
|
|
|
} else if (scene) {
|
2020-07-27 17:18:49 +05:30
|
|
|
if (scene.appState) {
|
|
|
|
scene.appState = {
|
|
|
|
...scene.appState,
|
|
|
|
...calculateScrollCenter(
|
|
|
|
scene.elements,
|
|
|
|
{
|
|
|
|
...scene.appState,
|
|
|
|
offsetTop: this.state.offsetTop,
|
|
|
|
offsetLeft: this.state.offsetLeft,
|
|
|
|
},
|
|
|
|
null,
|
|
|
|
),
|
|
|
|
};
|
|
|
|
}
|
2020-07-08 22:55:26 +02:00
|
|
|
this.syncActionResult(scene);
|
2020-03-26 18:28:26 +01:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2020-03-22 10:24:50 -07:00
|
|
|
public async componentDidMount() {
|
|
|
|
if (
|
2020-05-30 18:56:17 +05:30
|
|
|
process.env.NODE_ENV === ENV.TEST ||
|
|
|
|
process.env.NODE_ENV === ENV.DEVELOPMENT
|
2020-03-22 10:24:50 -07:00
|
|
|
) {
|
|
|
|
const setState = this.setState.bind(this);
|
|
|
|
Object.defineProperties(window.h, {
|
|
|
|
state: {
|
|
|
|
configurable: true,
|
|
|
|
get: () => {
|
|
|
|
return this.state;
|
|
|
|
},
|
|
|
|
},
|
|
|
|
setState: {
|
|
|
|
configurable: true,
|
|
|
|
value: (...args: Parameters<typeof setState>) => {
|
|
|
|
return this.setState(...args);
|
|
|
|
},
|
|
|
|
},
|
2020-03-26 08:28:50 +01:00
|
|
|
app: {
|
|
|
|
configurable: true,
|
|
|
|
value: this,
|
|
|
|
},
|
2020-03-22 10:24:50 -07:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2020-07-30 14:50:59 +05:30
|
|
|
this.scene.addCallback(this.onSceneUpdated);
|
2020-03-22 10:24:50 -07:00
|
|
|
|
2020-05-30 18:56:17 +05:30
|
|
|
this.addEventListeners();
|
2020-07-27 17:18:49 +05:30
|
|
|
this.setState(this.getCanvasOffsets(), () => {
|
|
|
|
this.initializeScene();
|
|
|
|
});
|
2020-03-22 10:24:50 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
public componentWillUnmount() {
|
|
|
|
this.unmounted = true;
|
2020-05-30 18:56:17 +05:30
|
|
|
this.removeEventListeners();
|
2020-07-30 14:50:59 +05:30
|
|
|
this.scene.destroy();
|
2020-07-02 22:12:56 +01:00
|
|
|
clearTimeout(touchTimeout);
|
2020-05-30 18:56:17 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
private onResize = withBatchedUpdates(() => {
|
2020-07-30 14:50:59 +05:30
|
|
|
this.scene
|
2020-05-30 18:56:17 +05:30
|
|
|
.getElementsIncludingDeleted()
|
|
|
|
.forEach((element) => invalidateShapeForElement(element));
|
|
|
|
this.setState({});
|
|
|
|
});
|
2020-03-22 10:24:50 -07:00
|
|
|
|
2020-07-10 07:16:28 +02:00
|
|
|
private onHashChange = (event: HashChangeEvent) => {
|
|
|
|
if (window.location.hash.length > 1) {
|
|
|
|
this.initializeScene();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2020-05-30 18:56:17 +05:30
|
|
|
private removeEventListeners() {
|
2020-04-12 06:12:02 +05:30
|
|
|
document.removeEventListener(EVENT.COPY, this.onCopy);
|
|
|
|
document.removeEventListener(EVENT.PASTE, this.pasteFromClipboard);
|
|
|
|
document.removeEventListener(EVENT.CUT, this.onCut);
|
2020-03-22 10:24:50 -07:00
|
|
|
|
2020-04-12 06:12:02 +05:30
|
|
|
document.removeEventListener(EVENT.KEYDOWN, this.onKeyDown, false);
|
2020-03-22 10:24:50 -07:00
|
|
|
document.removeEventListener(
|
2020-04-12 06:12:02 +05:30
|
|
|
EVENT.MOUSE_MOVE,
|
2020-03-22 10:24:50 -07:00
|
|
|
this.updateCurrentCursorPosition,
|
|
|
|
false,
|
|
|
|
);
|
2020-04-12 06:12:02 +05:30
|
|
|
document.removeEventListener(EVENT.KEYUP, this.onKeyUp);
|
|
|
|
window.removeEventListener(EVENT.RESIZE, this.onResize, false);
|
|
|
|
window.removeEventListener(EVENT.UNLOAD, this.onUnload, false);
|
|
|
|
window.removeEventListener(EVENT.BLUR, this.onBlur, false);
|
|
|
|
window.removeEventListener(EVENT.DRAG_OVER, this.disableEvent, false);
|
|
|
|
window.removeEventListener(EVENT.DROP, this.disableEvent, false);
|
2020-07-10 07:16:28 +02:00
|
|
|
window.removeEventListener(EVENT.HASHCHANGE, this.onHashChange, false);
|
2020-03-22 10:24:50 -07:00
|
|
|
|
|
|
|
document.removeEventListener(
|
2020-04-12 06:12:02 +05:30
|
|
|
EVENT.GESTURE_START,
|
2020-03-22 10:24:50 -07:00
|
|
|
this.onGestureStart as any,
|
|
|
|
false,
|
|
|
|
);
|
|
|
|
document.removeEventListener(
|
2020-04-12 06:12:02 +05:30
|
|
|
EVENT.GESTURE_CHANGE,
|
2020-03-22 10:24:50 -07:00
|
|
|
this.onGestureChange as any,
|
|
|
|
false,
|
|
|
|
);
|
2020-04-12 06:12:02 +05:30
|
|
|
document.removeEventListener(
|
|
|
|
EVENT.GESTURE_END,
|
|
|
|
this.onGestureEnd as any,
|
|
|
|
false,
|
|
|
|
);
|
|
|
|
window.removeEventListener(EVENT.BEFORE_UNLOAD, this.beforeUnload);
|
2020-03-22 10:24:50 -07:00
|
|
|
}
|
2020-05-30 18:56:17 +05:30
|
|
|
|
|
|
|
private addEventListeners() {
|
|
|
|
document.addEventListener(EVENT.COPY, this.onCopy);
|
|
|
|
document.addEventListener(EVENT.PASTE, this.pasteFromClipboard);
|
|
|
|
document.addEventListener(EVENT.CUT, this.onCut);
|
|
|
|
|
|
|
|
document.addEventListener(EVENT.KEYDOWN, this.onKeyDown, false);
|
|
|
|
document.addEventListener(EVENT.KEYUP, this.onKeyUp, { passive: true });
|
|
|
|
document.addEventListener(
|
|
|
|
EVENT.MOUSE_MOVE,
|
|
|
|
this.updateCurrentCursorPosition,
|
|
|
|
);
|
|
|
|
window.addEventListener(EVENT.RESIZE, this.onResize, false);
|
|
|
|
window.addEventListener(EVENT.UNLOAD, this.onUnload, false);
|
|
|
|
window.addEventListener(EVENT.BLUR, this.onBlur, false);
|
|
|
|
window.addEventListener(EVENT.DRAG_OVER, this.disableEvent, false);
|
|
|
|
window.addEventListener(EVENT.DROP, this.disableEvent, false);
|
2020-07-10 07:16:28 +02:00
|
|
|
window.addEventListener(EVENT.HASHCHANGE, this.onHashChange, false);
|
2020-05-30 18:56:17 +05:30
|
|
|
|
|
|
|
// rerender text elements on font load to fix #637 && #1553
|
|
|
|
document.fonts?.addEventListener?.("loadingdone", this.onFontLoaded);
|
|
|
|
|
|
|
|
// Safari-only desktop pinch zoom
|
|
|
|
document.addEventListener(
|
|
|
|
EVENT.GESTURE_START,
|
|
|
|
this.onGestureStart as any,
|
|
|
|
false,
|
|
|
|
);
|
|
|
|
document.addEventListener(
|
|
|
|
EVENT.GESTURE_CHANGE,
|
|
|
|
this.onGestureChange as any,
|
|
|
|
false,
|
|
|
|
);
|
|
|
|
document.addEventListener(
|
|
|
|
EVENT.GESTURE_END,
|
|
|
|
this.onGestureEnd as any,
|
|
|
|
false,
|
|
|
|
);
|
|
|
|
window.addEventListener(EVENT.BEFORE_UNLOAD, this.beforeUnload);
|
|
|
|
}
|
2020-03-22 10:24:50 -07:00
|
|
|
|
|
|
|
private beforeUnload = withBatchedUpdates((event: BeforeUnloadEvent) => {
|
2020-07-08 22:55:26 +02:00
|
|
|
if (this.state.isCollaborating && this.portal.roomID) {
|
2020-07-17 18:39:23 +02:00
|
|
|
try {
|
|
|
|
localStorage?.setItem(
|
|
|
|
LOCAL_STORAGE_KEY_COLLAB_FORCE_FLAG,
|
|
|
|
JSON.stringify({
|
|
|
|
timestamp: Date.now(),
|
|
|
|
room: this.portal.roomID,
|
|
|
|
}),
|
|
|
|
);
|
|
|
|
} catch {}
|
2020-07-08 22:55:26 +02:00
|
|
|
}
|
2020-07-30 14:50:59 +05:30
|
|
|
if (this.state.isCollaborating && this.scene.getElements().length > 0) {
|
2020-03-22 10:24:50 -07:00
|
|
|
event.preventDefault();
|
|
|
|
// NOTE: modern browsers no longer allow showing a custom message here
|
|
|
|
event.returnValue = "";
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2020-05-07 14:13:18 -07:00
|
|
|
queueBroadcastAllElements = throttle(() => {
|
|
|
|
this.broadcastScene(SCENE.UPDATE, /* syncAll */ true);
|
|
|
|
}, SYNC_FULL_SCENE_INTERVAL_MS);
|
|
|
|
|
2020-08-08 21:04:15 -07:00
|
|
|
componentDidUpdate(prevProps: ExcalidrawProps, prevState: AppState) {
|
2020-07-07 20:40:39 +05:30
|
|
|
const { width: prevWidth, height: prevHeight } = prevProps;
|
2020-08-20 16:45:20 +02:00
|
|
|
const { width: currentWidth, height: currentHeight, onChange } = this.props;
|
2020-07-07 20:40:39 +05:30
|
|
|
if (prevWidth !== currentWidth || prevHeight !== currentHeight) {
|
|
|
|
this.setState({
|
|
|
|
width: currentWidth,
|
|
|
|
height: currentHeight,
|
2020-07-27 17:18:49 +05:30
|
|
|
...this.getCanvasOffsets(),
|
2020-07-07 20:40:39 +05:30
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2020-08-20 13:20:13 +02:00
|
|
|
document.documentElement.classList.toggle(
|
2020-08-13 04:35:31 -07:00
|
|
|
"Appearance_dark",
|
|
|
|
this.state.appearance === "dark",
|
|
|
|
);
|
|
|
|
|
2020-04-08 10:18:56 -07:00
|
|
|
if (this.state.isCollaborating && !this.portal.socket) {
|
2020-03-26 18:28:26 +01:00
|
|
|
this.initializeSocketClient({ showLoadingState: true });
|
2020-03-22 10:24:50 -07:00
|
|
|
}
|
2020-04-04 16:12:19 +01:00
|
|
|
|
2020-06-01 11:35:44 +02:00
|
|
|
if (
|
|
|
|
this.state.editingLinearElement &&
|
|
|
|
!this.state.selectedElementIds[this.state.editingLinearElement.elementId]
|
|
|
|
) {
|
|
|
|
// defer so that the commitToHistory flag isn't reset via current update
|
|
|
|
setTimeout(() => {
|
|
|
|
this.actionManager.executeAction(actionFinalize);
|
|
|
|
});
|
|
|
|
}
|
2020-08-08 21:04:15 -07:00
|
|
|
const { multiElement } = prevState;
|
|
|
|
if (
|
|
|
|
prevState.elementType !== this.state.elementType &&
|
|
|
|
multiElement != null &&
|
|
|
|
isBindingEnabled(this.state) &&
|
|
|
|
isBindingElement(multiElement)
|
|
|
|
) {
|
|
|
|
maybeBindLinearElement(
|
|
|
|
multiElement,
|
|
|
|
this.state,
|
|
|
|
this.scene,
|
|
|
|
tupleToCoors(
|
|
|
|
LinearElementEditor.getPointAtIndexGlobalCoordinates(
|
|
|
|
multiElement,
|
|
|
|
-1,
|
|
|
|
),
|
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|
2020-06-01 11:35:44 +02:00
|
|
|
|
2020-04-04 16:12:19 +01:00
|
|
|
const cursorButton: {
|
|
|
|
[id: string]: string | undefined;
|
|
|
|
} = {};
|
2020-04-04 16:02:16 +02:00
|
|
|
const pointerViewportCoords: SceneState["remotePointerViewportCoords"] = {};
|
|
|
|
const remoteSelectedElementIds: SceneState["remoteSelectedElementIds"] = {};
|
2020-04-07 14:02:42 +01:00
|
|
|
const pointerUsernames: { [id: string]: string } = {};
|
2020-08-14 20:14:22 +02:00
|
|
|
this.state.collaborators.forEach((user, socketId) => {
|
2020-04-04 16:02:16 +02:00
|
|
|
if (user.selectedElementIds) {
|
|
|
|
for (const id of Object.keys(user.selectedElementIds)) {
|
|
|
|
if (!(id in remoteSelectedElementIds)) {
|
|
|
|
remoteSelectedElementIds[id] = [];
|
|
|
|
}
|
2020-08-14 20:14:22 +02:00
|
|
|
remoteSelectedElementIds[id].push(socketId);
|
2020-04-04 16:02:16 +02:00
|
|
|
}
|
|
|
|
}
|
2020-03-22 10:24:50 -07:00
|
|
|
if (!user.pointer) {
|
|
|
|
return;
|
|
|
|
}
|
2020-04-07 14:02:42 +01:00
|
|
|
if (user.username) {
|
2020-08-14 20:14:22 +02:00
|
|
|
pointerUsernames[socketId] = user.username;
|
2020-04-07 14:02:42 +01:00
|
|
|
}
|
2020-08-14 20:14:22 +02:00
|
|
|
pointerViewportCoords[socketId] = sceneCoordsToViewportCoords(
|
2020-03-22 10:24:50 -07:00
|
|
|
{
|
|
|
|
sceneX: user.pointer.x,
|
|
|
|
sceneY: user.pointer.y,
|
|
|
|
},
|
|
|
|
this.state,
|
|
|
|
this.canvas,
|
|
|
|
window.devicePixelRatio,
|
|
|
|
);
|
2020-08-14 20:14:22 +02:00
|
|
|
cursorButton[socketId] = user.button;
|
2020-03-22 10:24:50 -07:00
|
|
|
});
|
2020-07-30 14:50:59 +05:30
|
|
|
const elements = this.scene.getElements();
|
2020-03-22 10:24:50 -07:00
|
|
|
const { atLeastOneVisibleElement, scrollBars } = renderScene(
|
2020-04-08 09:49:52 -07:00
|
|
|
elements.filter((element) => {
|
2020-04-03 14:16:14 +02:00
|
|
|
// don't render text element that's being currently edited (it's
|
|
|
|
// rendered on remote only)
|
|
|
|
return (
|
|
|
|
!this.state.editingElement ||
|
|
|
|
this.state.editingElement.type !== "text" ||
|
|
|
|
element.id !== this.state.editingElement.id
|
|
|
|
);
|
|
|
|
}),
|
2020-03-22 10:24:50 -07:00
|
|
|
this.state,
|
|
|
|
this.state.selectionElement,
|
|
|
|
window.devicePixelRatio,
|
|
|
|
this.rc!,
|
|
|
|
this.canvas!,
|
|
|
|
{
|
|
|
|
scrollX: this.state.scrollX,
|
|
|
|
scrollY: this.state.scrollY,
|
|
|
|
viewBackgroundColor: this.state.viewBackgroundColor,
|
|
|
|
zoom: this.state.zoom,
|
|
|
|
remotePointerViewportCoords: pointerViewportCoords,
|
2020-04-04 16:12:19 +01:00
|
|
|
remotePointerButton: cursorButton,
|
2020-04-04 16:02:16 +02:00
|
|
|
remoteSelectedElementIds: remoteSelectedElementIds,
|
2020-04-07 14:02:42 +01:00
|
|
|
remotePointerUsernames: pointerUsernames,
|
2020-03-28 16:59:36 -07:00
|
|
|
shouldCacheIgnoreZoom: this.state.shouldCacheIgnoreZoom,
|
2020-03-22 10:24:50 -07:00
|
|
|
},
|
|
|
|
{
|
|
|
|
renderOptimizations: true,
|
|
|
|
},
|
|
|
|
);
|
|
|
|
if (scrollBars) {
|
|
|
|
currentScrollBars = scrollBars;
|
|
|
|
}
|
2020-06-25 21:21:27 +02:00
|
|
|
const scrolledOutside =
|
|
|
|
// hide when editing text
|
|
|
|
this.state.editingElement?.type === "text"
|
|
|
|
? false
|
|
|
|
: !atLeastOneVisibleElement && elements.length > 0;
|
2020-03-22 10:24:50 -07:00
|
|
|
if (this.state.scrolledOutside !== scrolledOutside) {
|
|
|
|
this.setState({ scrolledOutside: scrolledOutside });
|
|
|
|
}
|
|
|
|
|
|
|
|
if (
|
2020-07-30 14:50:59 +05:30
|
|
|
getDrawingVersion(this.scene.getElementsIncludingDeleted()) >
|
2020-03-22 10:24:50 -07:00
|
|
|
this.lastBroadcastedOrReceivedSceneVersion
|
|
|
|
) {
|
2020-05-07 14:13:18 -07:00
|
|
|
this.broadcastScene(SCENE.UPDATE, /* syncAll */ false);
|
|
|
|
this.queueBroadcastAllElements();
|
2020-03-22 10:24:50 -07:00
|
|
|
}
|
|
|
|
|
2020-07-30 14:50:59 +05:30
|
|
|
history.record(this.state, this.scene.getElementsIncludingDeleted());
|
2020-08-20 16:45:20 +02:00
|
|
|
|
|
|
|
if (onChange) {
|
|
|
|
onChange(this.scene.getElementsIncludingDeleted(), this.state);
|
|
|
|
}
|
2020-03-22 10:24:50 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
// Copy/paste
|
|
|
|
|
|
|
|
private onCut = withBatchedUpdates((event: ClipboardEvent) => {
|
|
|
|
if (isWritableElement(event.target)) {
|
|
|
|
return;
|
|
|
|
}
|
2020-03-27 15:18:14 -07:00
|
|
|
this.copyAll();
|
2020-05-30 22:48:57 +02:00
|
|
|
this.actionManager.executeAction(actionDeleteSelected);
|
2020-03-22 10:24:50 -07:00
|
|
|
event.preventDefault();
|
|
|
|
});
|
|
|
|
|
|
|
|
private onCopy = withBatchedUpdates((event: ClipboardEvent) => {
|
|
|
|
if (isWritableElement(event.target)) {
|
|
|
|
return;
|
|
|
|
}
|
2020-03-27 15:18:14 -07:00
|
|
|
this.copyAll();
|
2020-03-22 10:24:50 -07:00
|
|
|
event.preventDefault();
|
|
|
|
});
|
2020-03-27 15:18:14 -07:00
|
|
|
|
|
|
|
private copyAll = () => {
|
2020-07-30 14:50:59 +05:30
|
|
|
copyToAppClipboard(this.scene.getElements(), this.state);
|
2020-03-22 10:24:50 -07:00
|
|
|
};
|
|
|
|
|
|
|
|
private copyToClipboardAsPng = () => {
|
2020-07-30 14:50:59 +05:30
|
|
|
const elements = this.scene.getElements();
|
2020-04-08 09:49:52 -07:00
|
|
|
|
|
|
|
const selectedElements = getSelectedElements(elements, this.state);
|
2020-03-22 10:24:50 -07:00
|
|
|
exportCanvas(
|
|
|
|
"clipboard",
|
2020-04-08 09:49:52 -07:00
|
|
|
selectedElements.length ? selectedElements : elements,
|
2020-03-22 10:24:50 -07:00
|
|
|
this.state,
|
|
|
|
this.canvas!,
|
|
|
|
this.state,
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
2020-04-05 16:13:17 -07:00
|
|
|
private copyToClipboardAsSvg = () => {
|
|
|
|
const selectedElements = getSelectedElements(
|
2020-07-30 14:50:59 +05:30
|
|
|
this.scene.getElements(),
|
2020-04-05 16:13:17 -07:00
|
|
|
this.state,
|
|
|
|
);
|
|
|
|
exportCanvas(
|
|
|
|
"clipboard-svg",
|
2020-07-30 14:50:59 +05:30
|
|
|
selectedElements.length ? selectedElements : this.scene.getElements(),
|
2020-04-05 16:13:17 -07:00
|
|
|
this.state,
|
|
|
|
this.canvas!,
|
|
|
|
this.state,
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
2020-05-30 18:56:17 +05:30
|
|
|
private static resetTapTwice() {
|
2020-04-12 06:12:02 +05:30
|
|
|
didTapTwice = false;
|
|
|
|
}
|
|
|
|
|
2020-04-04 14:55:36 +02:00
|
|
|
private onTapStart = (event: TouchEvent) => {
|
|
|
|
if (!didTapTwice) {
|
|
|
|
didTapTwice = true;
|
|
|
|
clearTimeout(tappedTwiceTimer);
|
2020-04-12 06:12:02 +05:30
|
|
|
tappedTwiceTimer = window.setTimeout(
|
2020-05-30 18:56:17 +05:30
|
|
|
App.resetTapTwice,
|
2020-04-12 06:12:02 +05:30
|
|
|
TAP_TWICE_TIMEOUT,
|
|
|
|
);
|
2020-04-04 14:55:36 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
// insert text only if we tapped twice with a single finger
|
|
|
|
// event.touches.length === 1 will also prevent inserting text when user's zooming
|
|
|
|
if (didTapTwice && event.touches.length === 1) {
|
|
|
|
const [touch] = event.touches;
|
|
|
|
// @ts-ignore
|
|
|
|
this.handleCanvasDoubleClick({
|
|
|
|
clientX: touch.clientX,
|
|
|
|
clientY: touch.clientY,
|
|
|
|
});
|
|
|
|
didTapTwice = false;
|
|
|
|
clearTimeout(tappedTwiceTimer);
|
|
|
|
}
|
|
|
|
event.preventDefault();
|
2020-06-02 18:41:40 +02:00
|
|
|
if (event.touches.length === 2) {
|
|
|
|
this.setState({
|
|
|
|
selectedElementIds: {},
|
|
|
|
});
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
private onTapEnd = (event: TouchEvent) => {
|
|
|
|
event.preventDefault();
|
|
|
|
if (event.touches.length > 0) {
|
|
|
|
const { previousSelectedElementIds } = this.state;
|
|
|
|
this.setState({
|
|
|
|
previousSelectedElementIds: {},
|
|
|
|
selectedElementIds: previousSelectedElementIds,
|
|
|
|
});
|
|
|
|
}
|
2020-04-04 14:55:36 +02:00
|
|
|
};
|
|
|
|
|
2020-03-22 10:24:50 -07:00
|
|
|
private pasteFromClipboard = withBatchedUpdates(
|
|
|
|
async (event: ClipboardEvent | null) => {
|
|
|
|
// #686
|
|
|
|
const target = document.activeElement;
|
|
|
|
const elementUnderCursor = document.elementFromPoint(cursorX, cursorY);
|
|
|
|
if (
|
|
|
|
// if no ClipboardEvent supplied, assume we're pasting via contextMenu
|
|
|
|
// thus these checks don't make sense
|
2020-03-28 15:43:09 -07:00
|
|
|
event &&
|
|
|
|
(!(elementUnderCursor instanceof HTMLCanvasElement) ||
|
|
|
|
isWritableElement(target))
|
2020-03-22 10:24:50 -07:00
|
|
|
) {
|
2020-03-28 15:43:09 -07:00
|
|
|
return;
|
|
|
|
}
|
2020-06-06 13:09:04 -07:00
|
|
|
const data = await getClipboardContent(
|
|
|
|
this.state,
|
|
|
|
cursorX,
|
|
|
|
cursorY,
|
|
|
|
event,
|
|
|
|
);
|
|
|
|
if (data.error) {
|
|
|
|
alert(data.error);
|
|
|
|
} else if (data.elements) {
|
2020-07-10 02:20:23 -07:00
|
|
|
this.addElementsFromPasteOrLibrary(data.elements);
|
2020-03-28 15:43:09 -07:00
|
|
|
} else if (data.text) {
|
|
|
|
this.addTextFromPaste(data.text);
|
2020-03-22 10:24:50 -07:00
|
|
|
}
|
2020-03-28 15:43:09 -07:00
|
|
|
this.selectShapeTool("selection");
|
|
|
|
event?.preventDefault();
|
2020-03-22 10:24:50 -07:00
|
|
|
},
|
|
|
|
);
|
|
|
|
|
2020-07-10 02:20:23 -07:00
|
|
|
private addElementsFromPasteOrLibrary = (
|
2020-03-22 10:24:50 -07:00
|
|
|
clipboardElements: readonly ExcalidrawElement[],
|
2020-07-10 02:20:23 -07:00
|
|
|
clientX = cursorX,
|
|
|
|
clientY = cursorY,
|
2020-03-22 10:24:50 -07:00
|
|
|
) => {
|
|
|
|
const [minX, minY, maxX, maxY] = getCommonBounds(clipboardElements);
|
|
|
|
|
|
|
|
const elementsCenterX = distance(minX, maxX) / 2;
|
|
|
|
const elementsCenterY = distance(minY, maxY) / 2;
|
|
|
|
|
|
|
|
const { x, y } = viewportCoordsToSceneCoords(
|
2020-07-10 02:20:23 -07:00
|
|
|
{ clientX, clientY },
|
2020-03-22 10:24:50 -07:00
|
|
|
this.state,
|
|
|
|
this.canvas,
|
|
|
|
window.devicePixelRatio,
|
|
|
|
);
|
|
|
|
|
|
|
|
const dx = x - elementsCenterX;
|
|
|
|
const dy = y - elementsCenterY;
|
2020-05-26 13:07:46 -07:00
|
|
|
const groupIdMap = new Map();
|
2020-03-22 10:24:50 -07:00
|
|
|
|
2020-08-08 21:04:15 -07:00
|
|
|
const oldIdToDuplicatedId = new Map();
|
2020-07-30 14:50:59 +05:30
|
|
|
const newElements = clipboardElements.map((element) => {
|
2020-08-08 21:04:15 -07:00
|
|
|
const newElement = duplicateElement(
|
|
|
|
this.state.editingGroupId,
|
|
|
|
groupIdMap,
|
|
|
|
element,
|
|
|
|
{
|
|
|
|
x: element.x + dx - minX,
|
|
|
|
y: element.y + dy - minY,
|
|
|
|
},
|
|
|
|
);
|
|
|
|
oldIdToDuplicatedId.set(element.id, newElement.id);
|
|
|
|
return newElement;
|
2020-07-30 14:50:59 +05:30
|
|
|
});
|
2020-08-08 21:04:15 -07:00
|
|
|
const nextElements = [
|
2020-07-30 14:50:59 +05:30
|
|
|
...this.scene.getElementsIncludingDeleted(),
|
2020-03-22 10:24:50 -07:00
|
|
|
...newElements,
|
2020-08-08 21:04:15 -07:00
|
|
|
];
|
|
|
|
fixBindingsAfterDuplication(
|
|
|
|
nextElements,
|
|
|
|
clipboardElements,
|
|
|
|
oldIdToDuplicatedId,
|
|
|
|
);
|
|
|
|
|
|
|
|
this.scene.replaceAllElements(nextElements);
|
2020-03-22 10:24:50 -07:00
|
|
|
history.resumeRecording();
|
2020-08-08 22:35:34 +02:00
|
|
|
this.setState(
|
|
|
|
selectGroupsForSelectedElements(
|
|
|
|
{
|
|
|
|
...this.state,
|
|
|
|
isLibraryOpen: false,
|
|
|
|
selectedElementIds: newElements.reduce((map, element) => {
|
|
|
|
map[element.id] = true;
|
|
|
|
return map;
|
|
|
|
}, {} as any),
|
|
|
|
selectedGroupIds: {},
|
|
|
|
},
|
|
|
|
this.scene.getElements(),
|
|
|
|
),
|
|
|
|
);
|
2020-03-22 10:24:50 -07:00
|
|
|
};
|
|
|
|
|
2020-03-28 15:43:09 -07:00
|
|
|
private addTextFromPaste(text: any) {
|
|
|
|
const { x, y } = viewportCoordsToSceneCoords(
|
|
|
|
{ clientX: cursorX, clientY: cursorY },
|
|
|
|
this.state,
|
|
|
|
this.canvas,
|
|
|
|
window.devicePixelRatio,
|
|
|
|
);
|
|
|
|
|
|
|
|
const element = newTextElement({
|
|
|
|
x: x,
|
|
|
|
y: y,
|
|
|
|
strokeColor: this.state.currentItemStrokeColor,
|
|
|
|
backgroundColor: this.state.currentItemBackgroundColor,
|
|
|
|
fillStyle: this.state.currentItemFillStyle,
|
|
|
|
strokeWidth: this.state.currentItemStrokeWidth,
|
2020-05-14 17:04:33 +02:00
|
|
|
strokeStyle: this.state.currentItemStrokeStyle,
|
2020-03-28 15:43:09 -07:00
|
|
|
roughness: this.state.currentItemRoughness,
|
|
|
|
opacity: this.state.currentItemOpacity,
|
2020-08-15 00:59:43 +09:00
|
|
|
strokeSharpness: this.state.currentItemStrokeSharpness,
|
2020-03-28 15:43:09 -07:00
|
|
|
text: text,
|
2020-05-27 15:14:50 +02:00
|
|
|
fontSize: this.state.currentItemFontSize,
|
|
|
|
fontFamily: this.state.currentItemFontFamily,
|
2020-04-08 21:00:27 +01:00
|
|
|
textAlign: this.state.currentItemTextAlign,
|
2020-06-25 21:21:27 +02:00
|
|
|
verticalAlign: DEFAULT_VERTICAL_ALIGN,
|
2020-03-28 15:43:09 -07:00
|
|
|
});
|
|
|
|
|
2020-07-30 14:50:59 +05:30
|
|
|
this.scene.replaceAllElements([
|
|
|
|
...this.scene.getElementsIncludingDeleted(),
|
2020-03-28 15:43:09 -07:00
|
|
|
element,
|
|
|
|
]);
|
|
|
|
this.setState({ selectedElementIds: { [element.id]: true } });
|
|
|
|
history.resumeRecording();
|
|
|
|
}
|
|
|
|
|
2020-03-22 10:24:50 -07:00
|
|
|
// Collaboration
|
|
|
|
|
|
|
|
setAppState = (obj: any) => {
|
|
|
|
this.setState(obj);
|
|
|
|
};
|
|
|
|
|
|
|
|
removePointer = (event: React.PointerEvent<HTMLElement>) => {
|
2020-07-02 22:12:56 +01:00
|
|
|
// remove touch handler for context menu on touch devices
|
|
|
|
if (event.pointerType === "touch" && touchTimeout) {
|
|
|
|
clearTimeout(touchTimeout);
|
|
|
|
touchMoving = false;
|
|
|
|
}
|
|
|
|
|
2020-03-22 10:24:50 -07:00
|
|
|
gesture.pointers.delete(event.pointerId);
|
|
|
|
};
|
|
|
|
|
2020-04-07 15:29:43 -07:00
|
|
|
openPortal = async () => {
|
2020-03-22 10:24:50 -07:00
|
|
|
window.history.pushState(
|
|
|
|
{},
|
|
|
|
"Excalidraw",
|
|
|
|
await generateCollaborationLink(),
|
2020-03-08 14:10:42 -07:00
|
|
|
);
|
2020-03-26 18:28:26 +01:00
|
|
|
this.initializeSocketClient({ showLoadingState: false });
|
2020-03-22 10:24:50 -07:00
|
|
|
};
|
2020-03-07 10:20:38 -05:00
|
|
|
|
2020-04-07 15:29:43 -07:00
|
|
|
closePortal = () => {
|
2020-03-22 10:24:50 -07:00
|
|
|
window.history.pushState({}, "Excalidraw", window.location.origin);
|
|
|
|
this.destroySocketClient();
|
|
|
|
};
|
2020-03-07 10:20:38 -05:00
|
|
|
|
2020-03-22 10:24:50 -07:00
|
|
|
toggleLock = () => {
|
2020-03-23 13:05:07 +02:00
|
|
|
this.setState((prevState) => ({
|
2020-03-22 10:24:50 -07:00
|
|
|
elementLocked: !prevState.elementLocked,
|
|
|
|
elementType: prevState.elementLocked
|
|
|
|
? "selection"
|
|
|
|
: prevState.elementType,
|
|
|
|
}));
|
2020-03-07 10:20:38 -05:00
|
|
|
};
|
|
|
|
|
2020-04-25 18:43:02 +05:30
|
|
|
toggleZenMode = () => {
|
|
|
|
this.setState({
|
|
|
|
zenModeEnabled: !this.state.zenModeEnabled,
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2020-06-24 00:24:52 +09:00
|
|
|
toggleGridMode = () => {
|
|
|
|
this.setState({
|
|
|
|
gridSize: this.state.gridSize ? null : GRID_SIZE,
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2020-03-11 19:42:18 +01:00
|
|
|
private destroySocketClient = () => {
|
|
|
|
this.setState({
|
|
|
|
isCollaborating: false,
|
2020-03-12 12:19:56 +01:00
|
|
|
collaborators: new Map(),
|
2020-03-11 19:42:18 +01:00
|
|
|
});
|
2020-04-08 10:18:56 -07:00
|
|
|
this.portal.close();
|
2020-03-11 19:42:18 +01:00
|
|
|
};
|
|
|
|
|
2020-05-27 00:21:03 +05:30
|
|
|
private initializeSocketClient = async (opts: {
|
|
|
|
showLoadingState: boolean;
|
|
|
|
}) => {
|
2020-04-08 10:18:56 -07:00
|
|
|
if (this.portal.socket) {
|
basic Socket.io implementation of collaborative editing (#879)
* Enable collaborative syncing for elements
* Don't fall back to local storage if using a room, as that is confusing
* Use remote socket server
* Send updates to new users when they join
* ~
* add mouse tracking
* enable collaboration, rooms, and mouse tracking
* fix syncing bugs and add a button to start syncing mid session
* enable collaboration, rooms, and mouse tracking
* fix syncing bugs and add a button to start syncing mid session
* Add Live button and app state to support tracking collaborator counts
* Enable collaborative syncing for elements
* add mouse tracking
* enable collaboration, rooms, and mouse tracking
* fix syncing bugs and add a button to start syncing mid session
* fix syncing bugs and add a button to start syncing mid session
* Add Live button and app state to support tracking collaborator counts
* prettier
* Fix bug with remote pointers not changing on scroll
* Enable collaborative syncing for elements
* add mouse tracking
* enable collaboration, rooms, and mouse tracking
* fix syncing bugs and add a button to start syncing mid session
* enable collaboration, rooms, and mouse tracking
* fix syncing bugs and add a button to start syncing mid session
* Add Live button and app state to support tracking collaborator counts
* enable collaboration, rooms, and mouse tracking
* fix syncing bugs and add a button to start syncing mid session
* fix syncing bugs and add a button to start syncing mid session
* Fix bug with remote pointers not changing on scroll
* remove UI for collaboration
* remove link
* clean up lingering unused UI
* set random IV passed per encrypted message, reduce room id length, refactored socket broadcasting API, rename room_id to room, removed throttling of pointer movement
* fix package.json conflict
2020-03-09 08:48:25 -07:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
const roomMatch = getCollaborationLinkData(window.location.href);
|
|
|
|
if (roomMatch) {
|
2020-03-26 18:28:26 +01:00
|
|
|
const initialize = () => {
|
2020-04-08 10:18:56 -07:00
|
|
|
this.portal.socketInitialized = true;
|
2020-03-26 18:28:26 +01:00
|
|
|
clearTimeout(initializationTimer);
|
|
|
|
if (this.state.isLoading && !this.unmounted) {
|
|
|
|
this.setState({ isLoading: false });
|
|
|
|
}
|
|
|
|
};
|
|
|
|
// fallback in case you're not alone in the room but still don't receive
|
|
|
|
// initial SCENE_UPDATE message
|
2020-04-12 06:12:02 +05:30
|
|
|
const initializationTimer = setTimeout(
|
|
|
|
initialize,
|
2020-07-11 18:43:20 +05:30
|
|
|
INITIAL_SCENE_UPDATE_TIMEOUT,
|
2020-04-12 06:12:02 +05:30
|
|
|
);
|
2020-03-26 18:28:26 +01:00
|
|
|
|
2020-03-29 11:35:56 +09:00
|
|
|
const updateScene = (
|
2020-04-12 06:12:02 +05:30
|
|
|
decryptedData: SocketUpdateDataSource[SCENE.INIT | SCENE.UPDATE],
|
2020-04-03 21:22:26 +02:00
|
|
|
{ scrollToContent = false }: { scrollToContent?: boolean } = {},
|
2020-03-29 11:35:56 +09:00
|
|
|
) => {
|
|
|
|
const { elements: remoteElements } = decryptedData.payload;
|
2020-04-03 14:16:14 +02:00
|
|
|
|
2020-04-03 21:22:26 +02:00
|
|
|
if (scrollToContent) {
|
|
|
|
this.setState({
|
|
|
|
...this.state,
|
|
|
|
...calculateScrollCenter(
|
2020-04-12 06:12:02 +05:30
|
|
|
remoteElements.filter((element: { isDeleted: boolean }) => {
|
2020-04-03 21:22:26 +02:00
|
|
|
return !element.isDeleted;
|
|
|
|
}),
|
2020-05-30 17:32:32 +05:30
|
|
|
this.state,
|
|
|
|
this.canvas,
|
2020-04-03 21:22:26 +02:00
|
|
|
),
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2020-03-29 11:35:56 +09:00
|
|
|
// Perform reconciliation - in collaboration, if we encounter
|
|
|
|
// elements with more staler versions than ours, ignore them
|
|
|
|
// and keep ours.
|
|
|
|
if (
|
2020-07-30 14:50:59 +05:30
|
|
|
this.scene.getElementsIncludingDeleted() == null ||
|
|
|
|
this.scene.getElementsIncludingDeleted().length === 0
|
2020-03-29 11:35:56 +09:00
|
|
|
) {
|
2020-07-30 14:50:59 +05:30
|
|
|
this.scene.replaceAllElements(remoteElements);
|
2020-03-29 11:35:56 +09:00
|
|
|
} else {
|
|
|
|
// create a map of ids so we don't have to iterate
|
|
|
|
// over the array more than once.
|
|
|
|
const localElementMap = getElementMap(
|
2020-07-30 14:50:59 +05:30
|
|
|
this.scene.getElementsIncludingDeleted(),
|
2020-03-29 11:35:56 +09:00
|
|
|
);
|
|
|
|
|
|
|
|
// Reconcile
|
2020-04-03 14:16:14 +02:00
|
|
|
const newElements = remoteElements
|
2020-03-28 21:25:40 -07:00
|
|
|
.reduce((elements, element) => {
|
|
|
|
// if the remote element references one that's currently
|
|
|
|
// edited on local, skip it (it'll be added in the next
|
|
|
|
// step)
|
|
|
|
if (
|
|
|
|
element.id === this.state.editingElement?.id ||
|
|
|
|
element.id === this.state.resizingElement?.id ||
|
|
|
|
element.id === this.state.draggingElement?.id
|
|
|
|
) {
|
|
|
|
return elements;
|
|
|
|
}
|
2020-03-29 11:35:56 +09:00
|
|
|
|
2020-03-28 21:25:40 -07:00
|
|
|
if (
|
|
|
|
localElementMap.hasOwnProperty(element.id) &&
|
|
|
|
localElementMap[element.id].version > element.version
|
|
|
|
) {
|
|
|
|
elements.push(localElementMap[element.id]);
|
|
|
|
delete localElementMap[element.id];
|
|
|
|
} else if (
|
|
|
|
localElementMap.hasOwnProperty(element.id) &&
|
|
|
|
localElementMap[element.id].version === element.version &&
|
|
|
|
localElementMap[element.id].versionNonce !==
|
|
|
|
element.versionNonce
|
|
|
|
) {
|
|
|
|
// resolve conflicting edits deterministically by taking the one with the lowest versionNonce
|
2020-03-29 11:35:56 +09:00
|
|
|
if (
|
2020-03-28 21:25:40 -07:00
|
|
|
localElementMap[element.id].versionNonce <
|
|
|
|
element.versionNonce
|
2020-03-29 11:35:56 +09:00
|
|
|
) {
|
|
|
|
elements.push(localElementMap[element.id]);
|
|
|
|
} else {
|
2020-03-28 21:25:40 -07:00
|
|
|
// it should be highly unlikely that the two versionNonces are the same. if we are
|
|
|
|
// really worried about this, we can replace the versionNonce with the socket id.
|
2020-03-29 11:35:56 +09:00
|
|
|
elements.push(element);
|
|
|
|
}
|
2020-03-28 21:25:40 -07:00
|
|
|
delete localElementMap[element.id];
|
|
|
|
} else {
|
|
|
|
elements.push(element);
|
|
|
|
delete localElementMap[element.id];
|
|
|
|
}
|
2020-03-29 11:35:56 +09:00
|
|
|
|
2020-03-28 21:25:40 -07:00
|
|
|
return elements;
|
2020-04-03 14:16:14 +02:00
|
|
|
}, [] as Mutable<typeof remoteElements>)
|
2020-03-28 21:25:40 -07:00
|
|
|
// add local elements that weren't deleted or on remote
|
|
|
|
.concat(...Object.values(localElementMap));
|
|
|
|
|
|
|
|
// Avoid broadcasting to the rest of the collaborators the scene
|
|
|
|
// we just received!
|
|
|
|
// Note: this needs to be set before replaceAllElements as it
|
|
|
|
// syncronously calls render.
|
|
|
|
this.lastBroadcastedOrReceivedSceneVersion = getDrawingVersion(
|
|
|
|
newElements,
|
2020-03-29 11:35:56 +09:00
|
|
|
);
|
2020-03-28 21:25:40 -07:00
|
|
|
|
2020-07-30 14:50:59 +05:30
|
|
|
this.scene.replaceAllElements(newElements);
|
2020-03-29 11:35:56 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
// We haven't yet implemented multiplayer undo functionality, so we clear the undo stack
|
|
|
|
// when we receive any messages from another peer. This UX can be pretty rough -- if you
|
|
|
|
// undo, a user makes a change, and then try to redo, your element(s) will be lost. However,
|
|
|
|
// right now we think this is the right tradeoff.
|
|
|
|
history.clear();
|
2020-05-30 18:56:17 +05:30
|
|
|
if (!this.portal.socketInitialized) {
|
2020-03-29 11:35:56 +09:00
|
|
|
initialize();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2020-05-27 00:21:03 +05:30
|
|
|
const { default: socketIOClient }: any = await import(
|
|
|
|
/* webpackChunkName: "socketIoClient" */ "socket.io-client"
|
|
|
|
);
|
|
|
|
|
2020-04-08 10:18:56 -07:00
|
|
|
this.portal.open(
|
|
|
|
socketIOClient(SOCKET_SERVER),
|
|
|
|
roomMatch[1],
|
|
|
|
roomMatch[2],
|
|
|
|
);
|
2020-04-07 15:29:43 -07:00
|
|
|
|
2020-04-27 10:56:08 -07:00
|
|
|
// All socket listeners are moving to Portal
|
2020-04-08 10:18:56 -07:00
|
|
|
this.portal.socket!.on(
|
basic Socket.io implementation of collaborative editing (#879)
* Enable collaborative syncing for elements
* Don't fall back to local storage if using a room, as that is confusing
* Use remote socket server
* Send updates to new users when they join
* ~
* add mouse tracking
* enable collaboration, rooms, and mouse tracking
* fix syncing bugs and add a button to start syncing mid session
* enable collaboration, rooms, and mouse tracking
* fix syncing bugs and add a button to start syncing mid session
* Add Live button and app state to support tracking collaborator counts
* Enable collaborative syncing for elements
* add mouse tracking
* enable collaboration, rooms, and mouse tracking
* fix syncing bugs and add a button to start syncing mid session
* fix syncing bugs and add a button to start syncing mid session
* Add Live button and app state to support tracking collaborator counts
* prettier
* Fix bug with remote pointers not changing on scroll
* Enable collaborative syncing for elements
* add mouse tracking
* enable collaboration, rooms, and mouse tracking
* fix syncing bugs and add a button to start syncing mid session
* enable collaboration, rooms, and mouse tracking
* fix syncing bugs and add a button to start syncing mid session
* Add Live button and app state to support tracking collaborator counts
* enable collaboration, rooms, and mouse tracking
* fix syncing bugs and add a button to start syncing mid session
* fix syncing bugs and add a button to start syncing mid session
* Fix bug with remote pointers not changing on scroll
* remove UI for collaboration
* remove link
* clean up lingering unused UI
* set random IV passed per encrypted message, reduce room id length, refactored socket broadcasting API, rename room_id to room, removed throttling of pointer movement
* fix package.json conflict
2020-03-09 08:48:25 -07:00
|
|
|
"client-broadcast",
|
|
|
|
async (encryptedData: ArrayBuffer, iv: Uint8Array) => {
|
2020-04-08 10:18:56 -07:00
|
|
|
if (!this.portal.roomKey) {
|
basic Socket.io implementation of collaborative editing (#879)
* Enable collaborative syncing for elements
* Don't fall back to local storage if using a room, as that is confusing
* Use remote socket server
* Send updates to new users when they join
* ~
* add mouse tracking
* enable collaboration, rooms, and mouse tracking
* fix syncing bugs and add a button to start syncing mid session
* enable collaboration, rooms, and mouse tracking
* fix syncing bugs and add a button to start syncing mid session
* Add Live button and app state to support tracking collaborator counts
* Enable collaborative syncing for elements
* add mouse tracking
* enable collaboration, rooms, and mouse tracking
* fix syncing bugs and add a button to start syncing mid session
* fix syncing bugs and add a button to start syncing mid session
* Add Live button and app state to support tracking collaborator counts
* prettier
* Fix bug with remote pointers not changing on scroll
* Enable collaborative syncing for elements
* add mouse tracking
* enable collaboration, rooms, and mouse tracking
* fix syncing bugs and add a button to start syncing mid session
* enable collaboration, rooms, and mouse tracking
* fix syncing bugs and add a button to start syncing mid session
* Add Live button and app state to support tracking collaborator counts
* enable collaboration, rooms, and mouse tracking
* fix syncing bugs and add a button to start syncing mid session
* fix syncing bugs and add a button to start syncing mid session
* Fix bug with remote pointers not changing on scroll
* remove UI for collaboration
* remove link
* clean up lingering unused UI
* set random IV passed per encrypted message, reduce room id length, refactored socket broadcasting API, rename room_id to room, removed throttling of pointer movement
* fix package.json conflict
2020-03-09 08:48:25 -07:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
const decryptedData = await decryptAESGEM(
|
|
|
|
encryptedData,
|
2020-04-08 10:18:56 -07:00
|
|
|
this.portal.roomKey,
|
basic Socket.io implementation of collaborative editing (#879)
* Enable collaborative syncing for elements
* Don't fall back to local storage if using a room, as that is confusing
* Use remote socket server
* Send updates to new users when they join
* ~
* add mouse tracking
* enable collaboration, rooms, and mouse tracking
* fix syncing bugs and add a button to start syncing mid session
* enable collaboration, rooms, and mouse tracking
* fix syncing bugs and add a button to start syncing mid session
* Add Live button and app state to support tracking collaborator counts
* Enable collaborative syncing for elements
* add mouse tracking
* enable collaboration, rooms, and mouse tracking
* fix syncing bugs and add a button to start syncing mid session
* fix syncing bugs and add a button to start syncing mid session
* Add Live button and app state to support tracking collaborator counts
* prettier
* Fix bug with remote pointers not changing on scroll
* Enable collaborative syncing for elements
* add mouse tracking
* enable collaboration, rooms, and mouse tracking
* fix syncing bugs and add a button to start syncing mid session
* enable collaboration, rooms, and mouse tracking
* fix syncing bugs and add a button to start syncing mid session
* Add Live button and app state to support tracking collaborator counts
* enable collaboration, rooms, and mouse tracking
* fix syncing bugs and add a button to start syncing mid session
* fix syncing bugs and add a button to start syncing mid session
* Fix bug with remote pointers not changing on scroll
* remove UI for collaboration
* remove link
* clean up lingering unused UI
* set random IV passed per encrypted message, reduce room id length, refactored socket broadcasting API, rename room_id to room, removed throttling of pointer movement
* fix package.json conflict
2020-03-09 08:48:25 -07:00
|
|
|
iv,
|
|
|
|
);
|
|
|
|
|
|
|
|
switch (decryptedData.type) {
|
|
|
|
case "INVALID_RESPONSE":
|
|
|
|
return;
|
2020-04-12 06:12:02 +05:30
|
|
|
case SCENE.INIT: {
|
2020-04-08 10:18:56 -07:00
|
|
|
if (!this.portal.socketInitialized) {
|
2020-04-03 21:22:26 +02:00
|
|
|
updateScene(decryptedData, { scrollToContent: true });
|
basic Socket.io implementation of collaborative editing (#879)
* Enable collaborative syncing for elements
* Don't fall back to local storage if using a room, as that is confusing
* Use remote socket server
* Send updates to new users when they join
* ~
* add mouse tracking
* enable collaboration, rooms, and mouse tracking
* fix syncing bugs and add a button to start syncing mid session
* enable collaboration, rooms, and mouse tracking
* fix syncing bugs and add a button to start syncing mid session
* Add Live button and app state to support tracking collaborator counts
* Enable collaborative syncing for elements
* add mouse tracking
* enable collaboration, rooms, and mouse tracking
* fix syncing bugs and add a button to start syncing mid session
* fix syncing bugs and add a button to start syncing mid session
* Add Live button and app state to support tracking collaborator counts
* prettier
* Fix bug with remote pointers not changing on scroll
* Enable collaborative syncing for elements
* add mouse tracking
* enable collaboration, rooms, and mouse tracking
* fix syncing bugs and add a button to start syncing mid session
* enable collaboration, rooms, and mouse tracking
* fix syncing bugs and add a button to start syncing mid session
* Add Live button and app state to support tracking collaborator counts
* enable collaboration, rooms, and mouse tracking
* fix syncing bugs and add a button to start syncing mid session
* fix syncing bugs and add a button to start syncing mid session
* Fix bug with remote pointers not changing on scroll
* remove UI for collaboration
* remove link
* clean up lingering unused UI
* set random IV passed per encrypted message, reduce room id length, refactored socket broadcasting API, rename room_id to room, removed throttling of pointer movement
* fix package.json conflict
2020-03-09 08:48:25 -07:00
|
|
|
}
|
|
|
|
break;
|
2020-03-26 18:28:26 +01:00
|
|
|
}
|
2020-04-12 06:12:02 +05:30
|
|
|
case SCENE.UPDATE:
|
2020-03-29 11:35:56 +09:00
|
|
|
updateScene(decryptedData);
|
|
|
|
break;
|
2020-03-26 18:28:26 +01:00
|
|
|
case "MOUSE_LOCATION": {
|
2020-04-04 16:02:16 +02:00
|
|
|
const {
|
2020-08-14 20:14:22 +02:00
|
|
|
socketId,
|
|
|
|
pointer,
|
2020-04-04 16:12:19 +01:00
|
|
|
button,
|
2020-04-07 14:02:42 +01:00
|
|
|
username,
|
2020-04-04 16:02:16 +02:00
|
|
|
selectedElementIds,
|
|
|
|
} = decryptedData.payload;
|
2020-08-13 17:20:38 +02:00
|
|
|
// NOTE purposefully mutating collaborators map in case of
|
|
|
|
// pointer updates so as not to trigger LayerUI rerender
|
2020-03-23 13:05:07 +02:00
|
|
|
this.setState((state) => {
|
2020-08-14 20:14:22 +02:00
|
|
|
if (!state.collaborators.has(socketId)) {
|
|
|
|
state.collaborators.set(socketId, {});
|
2020-03-16 00:38:37 -07:00
|
|
|
}
|
2020-08-14 20:14:22 +02:00
|
|
|
const user = state.collaborators.get(socketId)!;
|
|
|
|
user.pointer = pointer;
|
2020-04-04 16:12:19 +01:00
|
|
|
user.button = button;
|
2020-04-04 16:02:16 +02:00
|
|
|
user.selectedElementIds = selectedElementIds;
|
2020-04-07 14:02:42 +01:00
|
|
|
user.username = username;
|
2020-08-14 20:14:22 +02:00
|
|
|
state.collaborators.set(socketId, user);
|
2020-03-14 20:46:57 -07:00
|
|
|
return state;
|
basic Socket.io implementation of collaborative editing (#879)
* Enable collaborative syncing for elements
* Don't fall back to local storage if using a room, as that is confusing
* Use remote socket server
* Send updates to new users when they join
* ~
* add mouse tracking
* enable collaboration, rooms, and mouse tracking
* fix syncing bugs and add a button to start syncing mid session
* enable collaboration, rooms, and mouse tracking
* fix syncing bugs and add a button to start syncing mid session
* Add Live button and app state to support tracking collaborator counts
* Enable collaborative syncing for elements
* add mouse tracking
* enable collaboration, rooms, and mouse tracking
* fix syncing bugs and add a button to start syncing mid session
* fix syncing bugs and add a button to start syncing mid session
* Add Live button and app state to support tracking collaborator counts
* prettier
* Fix bug with remote pointers not changing on scroll
* Enable collaborative syncing for elements
* add mouse tracking
* enable collaboration, rooms, and mouse tracking
* fix syncing bugs and add a button to start syncing mid session
* enable collaboration, rooms, and mouse tracking
* fix syncing bugs and add a button to start syncing mid session
* Add Live button and app state to support tracking collaborator counts
* enable collaboration, rooms, and mouse tracking
* fix syncing bugs and add a button to start syncing mid session
* fix syncing bugs and add a button to start syncing mid session
* Fix bug with remote pointers not changing on scroll
* remove UI for collaboration
* remove link
* clean up lingering unused UI
* set random IV passed per encrypted message, reduce room id length, refactored socket broadcasting API, rename room_id to room, removed throttling of pointer movement
* fix package.json conflict
2020-03-09 08:48:25 -07:00
|
|
|
});
|
|
|
|
break;
|
2020-03-26 18:28:26 +01:00
|
|
|
}
|
basic Socket.io implementation of collaborative editing (#879)
* Enable collaborative syncing for elements
* Don't fall back to local storage if using a room, as that is confusing
* Use remote socket server
* Send updates to new users when they join
* ~
* add mouse tracking
* enable collaboration, rooms, and mouse tracking
* fix syncing bugs and add a button to start syncing mid session
* enable collaboration, rooms, and mouse tracking
* fix syncing bugs and add a button to start syncing mid session
* Add Live button and app state to support tracking collaborator counts
* Enable collaborative syncing for elements
* add mouse tracking
* enable collaboration, rooms, and mouse tracking
* fix syncing bugs and add a button to start syncing mid session
* fix syncing bugs and add a button to start syncing mid session
* Add Live button and app state to support tracking collaborator counts
* prettier
* Fix bug with remote pointers not changing on scroll
* Enable collaborative syncing for elements
* add mouse tracking
* enable collaboration, rooms, and mouse tracking
* fix syncing bugs and add a button to start syncing mid session
* enable collaboration, rooms, and mouse tracking
* fix syncing bugs and add a button to start syncing mid session
* Add Live button and app state to support tracking collaborator counts
* enable collaboration, rooms, and mouse tracking
* fix syncing bugs and add a button to start syncing mid session
* fix syncing bugs and add a button to start syncing mid session
* Fix bug with remote pointers not changing on scroll
* remove UI for collaboration
* remove link
* clean up lingering unused UI
* set random IV passed per encrypted message, reduce room id length, refactored socket broadcasting API, rename room_id to room, removed throttling of pointer movement
* fix package.json conflict
2020-03-09 08:48:25 -07:00
|
|
|
}
|
|
|
|
},
|
|
|
|
);
|
2020-04-08 10:18:56 -07:00
|
|
|
this.portal.socket!.on("first-in-room", () => {
|
|
|
|
if (this.portal.socket) {
|
|
|
|
this.portal.socket.off("first-in-room");
|
basic Socket.io implementation of collaborative editing (#879)
* Enable collaborative syncing for elements
* Don't fall back to local storage if using a room, as that is confusing
* Use remote socket server
* Send updates to new users when they join
* ~
* add mouse tracking
* enable collaboration, rooms, and mouse tracking
* fix syncing bugs and add a button to start syncing mid session
* enable collaboration, rooms, and mouse tracking
* fix syncing bugs and add a button to start syncing mid session
* Add Live button and app state to support tracking collaborator counts
* Enable collaborative syncing for elements
* add mouse tracking
* enable collaboration, rooms, and mouse tracking
* fix syncing bugs and add a button to start syncing mid session
* fix syncing bugs and add a button to start syncing mid session
* Add Live button and app state to support tracking collaborator counts
* prettier
* Fix bug with remote pointers not changing on scroll
* Enable collaborative syncing for elements
* add mouse tracking
* enable collaboration, rooms, and mouse tracking
* fix syncing bugs and add a button to start syncing mid session
* enable collaboration, rooms, and mouse tracking
* fix syncing bugs and add a button to start syncing mid session
* Add Live button and app state to support tracking collaborator counts
* enable collaboration, rooms, and mouse tracking
* fix syncing bugs and add a button to start syncing mid session
* fix syncing bugs and add a button to start syncing mid session
* Fix bug with remote pointers not changing on scroll
* remove UI for collaboration
* remove link
* clean up lingering unused UI
* set random IV passed per encrypted message, reduce room id length, refactored socket broadcasting API, rename room_id to room, removed throttling of pointer movement
* fix package.json conflict
2020-03-09 08:48:25 -07:00
|
|
|
}
|
2020-03-26 18:28:26 +01:00
|
|
|
initialize();
|
basic Socket.io implementation of collaborative editing (#879)
* Enable collaborative syncing for elements
* Don't fall back to local storage if using a room, as that is confusing
* Use remote socket server
* Send updates to new users when they join
* ~
* add mouse tracking
* enable collaboration, rooms, and mouse tracking
* fix syncing bugs and add a button to start syncing mid session
* enable collaboration, rooms, and mouse tracking
* fix syncing bugs and add a button to start syncing mid session
* Add Live button and app state to support tracking collaborator counts
* Enable collaborative syncing for elements
* add mouse tracking
* enable collaboration, rooms, and mouse tracking
* fix syncing bugs and add a button to start syncing mid session
* fix syncing bugs and add a button to start syncing mid session
* Add Live button and app state to support tracking collaborator counts
* prettier
* Fix bug with remote pointers not changing on scroll
* Enable collaborative syncing for elements
* add mouse tracking
* enable collaboration, rooms, and mouse tracking
* fix syncing bugs and add a button to start syncing mid session
* enable collaboration, rooms, and mouse tracking
* fix syncing bugs and add a button to start syncing mid session
* Add Live button and app state to support tracking collaborator counts
* enable collaboration, rooms, and mouse tracking
* fix syncing bugs and add a button to start syncing mid session
* fix syncing bugs and add a button to start syncing mid session
* Fix bug with remote pointers not changing on scroll
* remove UI for collaboration
* remove link
* clean up lingering unused UI
* set random IV passed per encrypted message, reduce room id length, refactored socket broadcasting API, rename room_id to room, removed throttling of pointer movement
* fix package.json conflict
2020-03-09 08:48:25 -07:00
|
|
|
});
|
2020-03-26 18:28:26 +01:00
|
|
|
|
|
|
|
this.setState({
|
|
|
|
isCollaborating: true,
|
|
|
|
isLoading: opts.showLoadingState ? true : this.state.isLoading,
|
|
|
|
});
|
basic Socket.io implementation of collaborative editing (#879)
* Enable collaborative syncing for elements
* Don't fall back to local storage if using a room, as that is confusing
* Use remote socket server
* Send updates to new users when they join
* ~
* add mouse tracking
* enable collaboration, rooms, and mouse tracking
* fix syncing bugs and add a button to start syncing mid session
* enable collaboration, rooms, and mouse tracking
* fix syncing bugs and add a button to start syncing mid session
* Add Live button and app state to support tracking collaborator counts
* Enable collaborative syncing for elements
* add mouse tracking
* enable collaboration, rooms, and mouse tracking
* fix syncing bugs and add a button to start syncing mid session
* fix syncing bugs and add a button to start syncing mid session
* Add Live button and app state to support tracking collaborator counts
* prettier
* Fix bug with remote pointers not changing on scroll
* Enable collaborative syncing for elements
* add mouse tracking
* enable collaboration, rooms, and mouse tracking
* fix syncing bugs and add a button to start syncing mid session
* enable collaboration, rooms, and mouse tracking
* fix syncing bugs and add a button to start syncing mid session
* Add Live button and app state to support tracking collaborator counts
* enable collaboration, rooms, and mouse tracking
* fix syncing bugs and add a button to start syncing mid session
* fix syncing bugs and add a button to start syncing mid session
* Fix bug with remote pointers not changing on scroll
* remove UI for collaboration
* remove link
* clean up lingering unused UI
* set random IV passed per encrypted message, reduce room id length, refactored socket broadcasting API, rename room_id to room, removed throttling of pointer movement
* fix package.json conflict
2020-03-09 08:48:25 -07:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2020-04-28 09:49:00 -07:00
|
|
|
// Portal-only
|
|
|
|
setCollaborators(sockets: string[]) {
|
|
|
|
this.setState((state) => {
|
|
|
|
const collaborators: typeof state.collaborators = new Map();
|
2020-08-14 20:14:22 +02:00
|
|
|
for (const socketId of sockets) {
|
|
|
|
if (state.collaborators.has(socketId)) {
|
|
|
|
collaborators.set(socketId, state.collaborators.get(socketId)!);
|
2020-04-28 09:49:00 -07:00
|
|
|
} else {
|
2020-08-14 20:14:22 +02:00
|
|
|
collaborators.set(socketId, {});
|
2020-04-28 09:49:00 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return {
|
|
|
|
...state,
|
|
|
|
collaborators,
|
|
|
|
};
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2020-03-14 21:25:07 +01:00
|
|
|
private broadcastMouseLocation = (payload: {
|
2020-08-14 20:14:22 +02:00
|
|
|
pointer: SocketUpdateDataSource["MOUSE_LOCATION"]["payload"]["pointer"];
|
2020-04-04 16:12:19 +01:00
|
|
|
button: SocketUpdateDataSource["MOUSE_LOCATION"]["payload"]["button"];
|
2020-03-14 21:25:07 +01:00
|
|
|
}) => {
|
2020-04-08 10:18:56 -07:00
|
|
|
if (this.portal.socket?.id) {
|
2020-03-14 21:25:07 +01:00
|
|
|
const data: SocketUpdateDataSource["MOUSE_LOCATION"] = {
|
|
|
|
type: "MOUSE_LOCATION",
|
|
|
|
payload: {
|
2020-08-14 20:14:22 +02:00
|
|
|
socketId: this.portal.socket.id,
|
|
|
|
pointer: payload.pointer,
|
2020-04-04 16:12:19 +01:00
|
|
|
button: payload.button || "up",
|
2020-04-04 16:02:16 +02:00
|
|
|
selectedElementIds: this.state.selectedElementIds,
|
2020-04-07 14:02:42 +01:00
|
|
|
username: this.state.username,
|
2020-03-14 21:25:07 +01:00
|
|
|
},
|
|
|
|
};
|
2020-04-08 10:18:56 -07:00
|
|
|
return this.portal._broadcastSocketData(
|
2020-04-12 16:24:52 +05:30
|
|
|
data as SocketUpdateData,
|
2020-04-09 02:13:32 -07:00
|
|
|
true, // volatile
|
2020-03-14 21:25:07 +01:00
|
|
|
);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2020-04-27 10:56:08 -07:00
|
|
|
// maybe should move to Portal
|
2020-05-07 14:13:18 -07:00
|
|
|
broadcastScene = (sceneType: SCENE.INIT | SCENE.UPDATE, syncAll: boolean) => {
|
|
|
|
if (sceneType === SCENE.INIT && !syncAll) {
|
|
|
|
throw new Error("syncAll must be true when sending SCENE.INIT");
|
|
|
|
}
|
|
|
|
|
|
|
|
let syncableElements = getSyncableElements(
|
2020-07-30 14:50:59 +05:30
|
|
|
this.scene.getElementsIncludingDeleted(),
|
2020-05-07 14:13:18 -07:00
|
|
|
);
|
|
|
|
|
|
|
|
if (!syncAll) {
|
|
|
|
// sync out only the elements we think we need to to save bandwidth.
|
|
|
|
// periodically we'll resync the whole thing to make sure no one diverges
|
|
|
|
// due to a dropped message (server goes down etc).
|
|
|
|
syncableElements = syncableElements.filter(
|
|
|
|
(syncableElement) =>
|
|
|
|
!this.broadcastedElementVersions.has(syncableElement.id) ||
|
|
|
|
syncableElement.version >
|
|
|
|
this.broadcastedElementVersions.get(syncableElement.id)!,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2020-03-29 11:35:56 +09:00
|
|
|
const data: SocketUpdateDataSource[typeof sceneType] = {
|
|
|
|
type: sceneType,
|
2020-03-14 21:25:07 +01:00
|
|
|
payload: {
|
2020-05-07 14:13:18 -07:00
|
|
|
elements: syncableElements,
|
2020-03-14 21:25:07 +01:00
|
|
|
},
|
|
|
|
};
|
2020-03-14 20:46:57 -07:00
|
|
|
this.lastBroadcastedOrReceivedSceneVersion = Math.max(
|
|
|
|
this.lastBroadcastedOrReceivedSceneVersion,
|
2020-07-30 14:50:59 +05:30
|
|
|
getDrawingVersion(this.scene.getElementsIncludingDeleted()),
|
2020-03-14 20:46:57 -07:00
|
|
|
);
|
2020-05-07 14:13:18 -07:00
|
|
|
for (const syncableElement of syncableElements) {
|
|
|
|
this.broadcastedElementVersions.set(
|
|
|
|
syncableElement.id,
|
|
|
|
syncableElement.version,
|
|
|
|
);
|
|
|
|
}
|
2020-04-12 16:24:52 +05:30
|
|
|
return this.portal._broadcastSocketData(data as SocketUpdateData);
|
2020-03-14 21:25:07 +01:00
|
|
|
};
|
|
|
|
|
2020-03-16 19:07:47 -07:00
|
|
|
private onSceneUpdated = () => {
|
2020-03-15 10:06:41 -07:00
|
|
|
this.setState({});
|
|
|
|
};
|
|
|
|
|
2020-03-16 19:07:47 -07:00
|
|
|
private updateCurrentCursorPosition = withBatchedUpdates(
|
|
|
|
(event: MouseEvent) => {
|
|
|
|
cursorX = event.x;
|
|
|
|
cursorY = event.y;
|
|
|
|
},
|
|
|
|
);
|
2020-03-07 10:20:38 -05:00
|
|
|
|
2020-03-22 10:24:50 -07:00
|
|
|
// Input handling
|
|
|
|
|
2020-03-16 19:07:47 -07:00
|
|
|
private onKeyDown = withBatchedUpdates((event: KeyboardEvent) => {
|
2020-04-17 00:18:45 +02:00
|
|
|
// ensures we don't prevent devTools select-element feature
|
|
|
|
if (event[KEYS.CTRL_OR_CMD] && event.shiftKey && event.key === "C") {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2020-03-07 10:20:38 -05:00
|
|
|
if (
|
|
|
|
(isWritableElement(event.target) && event.key !== KEYS.ESCAPE) ||
|
|
|
|
// case: using arrows to move between buttons
|
|
|
|
(isArrowKey(event.key) && isInputLike(event.target))
|
|
|
|
) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2020-04-05 15:58:00 +03:00
|
|
|
if (event.key === KEYS.QUESTION_MARK) {
|
|
|
|
this.setState({
|
|
|
|
showShortcutsDialog: true,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2020-04-25 18:43:02 +05:30
|
|
|
if (
|
|
|
|
!event[KEYS.CTRL_OR_CMD] &&
|
|
|
|
event.altKey &&
|
|
|
|
event.keyCode === KEYS.Z_KEY_CODE
|
|
|
|
) {
|
|
|
|
this.toggleZenMode();
|
|
|
|
}
|
|
|
|
|
2020-06-24 00:24:52 +09:00
|
|
|
if (event[KEYS.CTRL_OR_CMD] && event.keyCode === KEYS.GRID_KEY_CODE) {
|
|
|
|
this.toggleGridMode();
|
|
|
|
}
|
2020-08-08 21:04:15 -07:00
|
|
|
if (event[KEYS.CTRL_OR_CMD]) {
|
|
|
|
this.setState({ isBindingEnabled: false });
|
|
|
|
}
|
2020-06-24 00:24:52 +09:00
|
|
|
|
2020-03-14 22:53:18 +01:00
|
|
|
if (event.code === "KeyC" && event.altKey && event.shiftKey) {
|
|
|
|
this.copyToClipboardAsPng();
|
|
|
|
event.preventDefault();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2020-03-07 10:20:38 -05:00
|
|
|
if (this.actionManager.handleKeyDown(event)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2020-07-10 02:20:23 -07:00
|
|
|
if (event.code === "Digit9") {
|
|
|
|
this.setState({ isLibraryOpen: !this.state.isLibraryOpen });
|
|
|
|
}
|
|
|
|
|
2020-03-07 10:20:38 -05:00
|
|
|
if (isArrowKey(event.key)) {
|
2020-06-24 00:24:52 +09:00
|
|
|
const step =
|
|
|
|
(this.state.gridSize &&
|
|
|
|
(event.shiftKey ? ELEMENT_TRANSLATE_AMOUNT : this.state.gridSize)) ||
|
|
|
|
(event.shiftKey
|
|
|
|
? ELEMENT_SHIFT_TRANSLATE_AMOUNT
|
|
|
|
: ELEMENT_TRANSLATE_AMOUNT);
|
2020-07-30 14:50:59 +05:30
|
|
|
this.scene.replaceAllElements(
|
|
|
|
this.scene.getElementsIncludingDeleted().map((el) => {
|
2020-03-14 21:48:51 -07:00
|
|
|
if (this.state.selectedElementIds[el.id]) {
|
|
|
|
const update: { x?: number; y?: number } = {};
|
|
|
|
if (event.key === KEYS.ARROW_LEFT) {
|
|
|
|
update.x = el.x - step;
|
|
|
|
} else if (event.key === KEYS.ARROW_RIGHT) {
|
|
|
|
update.x = el.x + step;
|
|
|
|
} else if (event.key === KEYS.ARROW_UP) {
|
|
|
|
update.y = el.y - step;
|
|
|
|
} else if (event.key === KEYS.ARROW_DOWN) {
|
|
|
|
update.y = el.y + step;
|
|
|
|
}
|
|
|
|
return newElementWith(el, update);
|
2020-03-07 10:20:38 -05:00
|
|
|
}
|
2020-03-14 21:48:51 -07:00
|
|
|
return el;
|
|
|
|
}),
|
|
|
|
);
|
2020-03-07 10:20:38 -05:00
|
|
|
event.preventDefault();
|
2020-03-26 01:12:51 +09:00
|
|
|
} else if (event.key === KEYS.ENTER) {
|
|
|
|
const selectedElements = getSelectedElements(
|
2020-07-30 14:50:59 +05:30
|
|
|
this.scene.getElements(),
|
2020-03-26 01:12:51 +09:00
|
|
|
this.state,
|
|
|
|
);
|
|
|
|
|
|
|
|
if (
|
2020-06-01 11:35:44 +02:00
|
|
|
selectedElements.length === 1 &&
|
|
|
|
isLinearElement(selectedElements[0])
|
|
|
|
) {
|
|
|
|
if (
|
|
|
|
!this.state.editingLinearElement ||
|
|
|
|
this.state.editingLinearElement.elementId !== selectedElements[0].id
|
|
|
|
) {
|
|
|
|
history.resumeRecording();
|
|
|
|
this.setState({
|
2020-07-30 14:50:59 +05:30
|
|
|
editingLinearElement: new LinearElementEditor(
|
|
|
|
selectedElements[0],
|
|
|
|
this.scene,
|
|
|
|
),
|
2020-06-01 11:35:44 +02:00
|
|
|
});
|
|
|
|
}
|
|
|
|
} else if (
|
2020-03-26 01:12:51 +09:00
|
|
|
selectedElements.length === 1 &&
|
|
|
|
!isLinearElement(selectedElements[0])
|
|
|
|
) {
|
|
|
|
const selectedElement = selectedElements[0];
|
|
|
|
this.startTextEditing({
|
2020-06-25 21:21:27 +02:00
|
|
|
sceneX: selectedElement.x + selectedElement.width / 2,
|
|
|
|
sceneY: selectedElement.y + selectedElement.height / 2,
|
2020-03-26 01:12:51 +09:00
|
|
|
});
|
|
|
|
event.preventDefault();
|
|
|
|
return;
|
|
|
|
}
|
2020-03-07 10:20:38 -05:00
|
|
|
} else if (
|
|
|
|
!event.ctrlKey &&
|
|
|
|
!event.altKey &&
|
2020-03-22 10:24:50 -07:00
|
|
|
!event.metaKey &&
|
|
|
|
this.state.draggingElement === null
|
|
|
|
) {
|
2020-07-24 15:47:46 +02:00
|
|
|
const shape = findShapeByKey(event.key);
|
|
|
|
if (shape) {
|
2020-03-22 10:24:50 -07:00
|
|
|
this.selectShapeTool(shape);
|
|
|
|
} else if (event.key === "q") {
|
|
|
|
this.toggleLock();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (event.key === KEYS.SPACE && gesture.pointers.size === 0) {
|
|
|
|
isHoldingSpace = true;
|
|
|
|
document.documentElement.style.cursor = CURSOR_TYPE.GRABBING;
|
|
|
|
}
|
|
|
|
});
|
2020-03-07 10:20:38 -05:00
|
|
|
|
2020-03-22 10:24:50 -07:00
|
|
|
private onKeyUp = withBatchedUpdates((event: KeyboardEvent) => {
|
|
|
|
if (event.key === KEYS.SPACE) {
|
|
|
|
if (this.state.elementType === "selection") {
|
|
|
|
resetCursor();
|
|
|
|
} else {
|
2020-04-06 22:26:54 +02:00
|
|
|
setCursorForShape(this.state.elementType);
|
2020-05-26 13:07:46 -07:00
|
|
|
this.setState({
|
|
|
|
selectedElementIds: {},
|
|
|
|
selectedGroupIds: {},
|
|
|
|
editingGroupId: null,
|
|
|
|
});
|
2020-03-07 10:20:38 -05:00
|
|
|
}
|
2020-03-22 10:24:50 -07:00
|
|
|
isHoldingSpace = false;
|
|
|
|
}
|
2020-08-08 21:04:15 -07:00
|
|
|
if (!event[KEYS.CTRL_OR_CMD] && !this.state.isBindingEnabled) {
|
|
|
|
this.setState({ isBindingEnabled: true });
|
|
|
|
}
|
2020-03-22 10:24:50 -07:00
|
|
|
});
|
2020-03-07 10:20:38 -05:00
|
|
|
|
|
|
|
private selectShapeTool(elementType: AppState["elementType"]) {
|
|
|
|
if (!isHoldingSpace) {
|
|
|
|
setCursorForShape(elementType);
|
|
|
|
}
|
|
|
|
if (isToolIcon(document.activeElement)) {
|
|
|
|
document.activeElement.blur();
|
|
|
|
}
|
2020-08-08 21:04:15 -07:00
|
|
|
if (!isLinearElementType(elementType)) {
|
|
|
|
this.setState({ suggestedBindings: [] });
|
|
|
|
}
|
2020-03-07 10:20:38 -05:00
|
|
|
if (elementType !== "selection") {
|
2020-05-26 13:07:46 -07:00
|
|
|
this.setState({
|
|
|
|
elementType,
|
|
|
|
selectedElementIds: {},
|
|
|
|
selectedGroupIds: {},
|
|
|
|
editingGroupId: null,
|
|
|
|
});
|
2020-03-08 10:20:55 -07:00
|
|
|
} else {
|
|
|
|
this.setState({ elementType });
|
2020-03-07 10:20:38 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-03-16 19:07:47 -07:00
|
|
|
private onGestureStart = withBatchedUpdates((event: GestureEvent) => {
|
2020-03-07 10:20:38 -05:00
|
|
|
event.preventDefault();
|
2020-06-02 18:41:40 +02:00
|
|
|
this.setState({
|
|
|
|
selectedElementIds: {},
|
|
|
|
});
|
2020-03-07 10:20:38 -05:00
|
|
|
gesture.initialScale = this.state.zoom;
|
2020-03-16 19:07:47 -07:00
|
|
|
});
|
|
|
|
|
|
|
|
private onGestureChange = withBatchedUpdates((event: GestureEvent) => {
|
2020-03-07 10:20:38 -05:00
|
|
|
event.preventDefault();
|
|
|
|
|
|
|
|
this.setState({
|
|
|
|
zoom: getNormalizedZoom(gesture.initialScale! * event.scale),
|
|
|
|
});
|
2020-03-16 19:07:47 -07:00
|
|
|
});
|
|
|
|
|
|
|
|
private onGestureEnd = withBatchedUpdates((event: GestureEvent) => {
|
2020-03-07 10:20:38 -05:00
|
|
|
event.preventDefault();
|
2020-06-02 18:41:40 +02:00
|
|
|
const { previousSelectedElementIds } = this.state;
|
|
|
|
this.setState({
|
|
|
|
previousSelectedElementIds: {},
|
|
|
|
selectedElementIds: previousSelectedElementIds,
|
|
|
|
});
|
2020-03-07 10:20:38 -05:00
|
|
|
gesture.initialScale = null;
|
2020-03-16 19:07:47 -07:00
|
|
|
});
|
2020-03-07 10:20:38 -05:00
|
|
|
|
2020-04-03 14:16:14 +02:00
|
|
|
private handleTextWysiwyg(
|
|
|
|
element: ExcalidrawTextElement,
|
|
|
|
{
|
|
|
|
isExistingElement = false,
|
2020-06-25 21:21:27 +02:00
|
|
|
}: {
|
|
|
|
isExistingElement?: boolean;
|
|
|
|
},
|
2020-04-03 14:16:14 +02:00
|
|
|
) {
|
2020-07-14 14:56:45 +03:00
|
|
|
const updateElement = (text: string, isDeleted = false) => {
|
2020-07-30 14:50:59 +05:30
|
|
|
this.scene.replaceAllElements([
|
|
|
|
...this.scene.getElementsIncludingDeleted().map((_element) => {
|
2020-06-25 21:21:27 +02:00
|
|
|
if (_element.id === element.id && isTextElement(_element)) {
|
|
|
|
return updateTextElement(_element, {
|
2020-04-03 14:16:14 +02:00
|
|
|
text,
|
2020-07-14 14:56:45 +03:00
|
|
|
isDeleted,
|
2020-04-03 14:16:14 +02:00
|
|
|
});
|
|
|
|
}
|
|
|
|
return _element;
|
|
|
|
}),
|
|
|
|
]);
|
|
|
|
};
|
|
|
|
|
2020-04-12 15:57:57 +02:00
|
|
|
textWysiwyg({
|
|
|
|
id: element.id,
|
2020-07-27 17:18:49 +05:30
|
|
|
appState: this.state,
|
2020-06-25 21:21:27 +02:00
|
|
|
getViewportCoords: (x, y) => {
|
|
|
|
const { x: viewportX, y: viewportY } = sceneCoordsToViewportCoords(
|
2020-07-27 17:18:49 +05:30
|
|
|
{
|
|
|
|
sceneX: x,
|
|
|
|
sceneY: y,
|
|
|
|
},
|
2020-06-25 21:21:27 +02:00
|
|
|
this.state,
|
|
|
|
this.canvas,
|
|
|
|
window.devicePixelRatio,
|
|
|
|
);
|
|
|
|
return [viewportX, viewportY];
|
|
|
|
},
|
2020-04-03 14:16:14 +02:00
|
|
|
onChange: withBatchedUpdates((text) => {
|
2020-06-25 21:21:27 +02:00
|
|
|
updateElement(text);
|
2020-04-03 14:16:14 +02:00
|
|
|
}),
|
|
|
|
onSubmit: withBatchedUpdates((text) => {
|
2020-07-14 14:56:45 +03:00
|
|
|
const isDeleted = !text.trim();
|
|
|
|
updateElement(text, isDeleted);
|
|
|
|
if (!isDeleted) {
|
|
|
|
this.setState((prevState) => ({
|
|
|
|
selectedElementIds: {
|
|
|
|
...prevState.selectedElementIds,
|
|
|
|
[element.id]: true,
|
|
|
|
},
|
|
|
|
}));
|
2020-08-08 21:04:15 -07:00
|
|
|
} else {
|
|
|
|
fixBindingsAfterDeletion(this.scene.getElements(), [element]);
|
2020-04-03 14:16:14 +02:00
|
|
|
}
|
2020-07-14 14:56:45 +03:00
|
|
|
if (!isDeleted || isExistingElement) {
|
2020-04-03 14:16:14 +02:00
|
|
|
history.resumeRecording();
|
|
|
|
}
|
2020-07-14 14:56:45 +03:00
|
|
|
|
|
|
|
this.setState({
|
|
|
|
draggingElement: null,
|
|
|
|
editingElement: null,
|
|
|
|
});
|
|
|
|
if (this.state.elementLocked) {
|
|
|
|
setCursorForShape(this.state.elementType);
|
|
|
|
}
|
2020-04-03 14:16:14 +02:00
|
|
|
}),
|
2020-07-30 14:50:59 +05:30
|
|
|
element,
|
2020-04-03 14:16:14 +02:00
|
|
|
});
|
2020-04-11 17:10:56 +01:00
|
|
|
// deselect all other elements when inserting text
|
2020-05-26 13:07:46 -07:00
|
|
|
this.setState({
|
|
|
|
selectedElementIds: {},
|
|
|
|
selectedGroupIds: {},
|
|
|
|
editingGroupId: null,
|
|
|
|
});
|
2020-04-03 14:16:14 +02:00
|
|
|
|
|
|
|
// do an initial update to re-initialize element position since we were
|
|
|
|
// modifying element's x/y for sake of editor (case: syncing to remote)
|
|
|
|
updateElement(element.text);
|
|
|
|
}
|
|
|
|
|
2020-06-25 21:21:27 +02:00
|
|
|
private getTextElementAtPosition(
|
|
|
|
x: number,
|
|
|
|
y: number,
|
|
|
|
): NonDeleted<ExcalidrawTextElement> | null {
|
2020-08-08 21:04:15 -07:00
|
|
|
const element = this.getElementAtPosition(x, y);
|
2020-03-07 10:20:38 -05:00
|
|
|
|
2020-06-25 21:21:27 +02:00
|
|
|
if (element && isTextElement(element) && !element.isDeleted) {
|
|
|
|
return element;
|
|
|
|
}
|
|
|
|
return null;
|
|
|
|
}
|
2020-03-07 10:20:38 -05:00
|
|
|
|
2020-08-08 21:04:15 -07:00
|
|
|
private getElementAtPosition(
|
|
|
|
x: number,
|
|
|
|
y: number,
|
|
|
|
): NonDeleted<ExcalidrawElement> | null {
|
2020-08-26 17:37:44 +01:00
|
|
|
const allHitElements = this.getElementsAtPosition(x, y);
|
|
|
|
if (allHitElements.length > 1) {
|
|
|
|
const elementWithHighestZIndex =
|
|
|
|
allHitElements[allHitElements.length - 1];
|
|
|
|
// If we're hitting element with highest z-index only on its bounding box
|
|
|
|
// while also hitting other element figure, the latter should be considered.
|
|
|
|
return isHittingElementBoundingBoxWithoutHittingElement(
|
|
|
|
elementWithHighestZIndex,
|
|
|
|
this.state,
|
|
|
|
x,
|
|
|
|
y,
|
|
|
|
)
|
|
|
|
? allHitElements[allHitElements.length - 2]
|
|
|
|
: elementWithHighestZIndex;
|
|
|
|
}
|
|
|
|
if (allHitElements.length === 1) {
|
|
|
|
return allHitElements[0];
|
|
|
|
}
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
private getElementsAtPosition(
|
|
|
|
x: number,
|
|
|
|
y: number,
|
|
|
|
): NonDeleted<ExcalidrawElement>[] {
|
|
|
|
return getElementsAtPosition(this.scene.getElements(), (element) =>
|
2020-08-08 21:04:15 -07:00
|
|
|
hitTest(element, this.state, x, y),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2020-06-25 21:21:27 +02:00
|
|
|
private startTextEditing = ({
|
|
|
|
sceneX,
|
|
|
|
sceneY,
|
|
|
|
insertAtParentCenter = true,
|
|
|
|
}: {
|
|
|
|
/** X position to insert text at */
|
|
|
|
sceneX: number;
|
|
|
|
/** Y position to insert text at */
|
|
|
|
sceneY: number;
|
|
|
|
/** whether to attempt to insert at element center if applicable */
|
|
|
|
insertAtParentCenter?: boolean;
|
|
|
|
}) => {
|
|
|
|
const existingTextElement = this.getTextElementAtPosition(sceneX, sceneY);
|
2020-03-07 10:20:38 -05:00
|
|
|
|
2020-06-25 21:21:27 +02:00
|
|
|
const parentCenterPosition =
|
|
|
|
insertAtParentCenter &&
|
|
|
|
this.getTextWysiwygSnappedToCenterPosition(
|
|
|
|
sceneX,
|
|
|
|
sceneY,
|
2020-03-08 18:09:45 -07:00
|
|
|
this.state,
|
|
|
|
this.canvas,
|
2020-03-15 12:25:18 -07:00
|
|
|
window.devicePixelRatio,
|
2020-03-08 18:09:45 -07:00
|
|
|
);
|
2020-03-07 10:20:38 -05:00
|
|
|
|
2020-06-25 21:21:27 +02:00
|
|
|
const element = existingTextElement
|
|
|
|
? existingTextElement
|
|
|
|
: newTextElement({
|
|
|
|
x: parentCenterPosition
|
|
|
|
? parentCenterPosition.elementCenterX
|
|
|
|
: sceneX,
|
|
|
|
y: parentCenterPosition
|
|
|
|
? parentCenterPosition.elementCenterY
|
|
|
|
: sceneY,
|
|
|
|
strokeColor: this.state.currentItemStrokeColor,
|
|
|
|
backgroundColor: this.state.currentItemBackgroundColor,
|
|
|
|
fillStyle: this.state.currentItemFillStyle,
|
|
|
|
strokeWidth: this.state.currentItemStrokeWidth,
|
|
|
|
strokeStyle: this.state.currentItemStrokeStyle,
|
|
|
|
roughness: this.state.currentItemRoughness,
|
|
|
|
opacity: this.state.currentItemOpacity,
|
2020-08-15 00:59:43 +09:00
|
|
|
strokeSharpness: this.state.currentItemStrokeSharpness,
|
2020-06-25 21:21:27 +02:00
|
|
|
text: "",
|
|
|
|
fontSize: this.state.currentItemFontSize,
|
|
|
|
fontFamily: this.state.currentItemFontFamily,
|
|
|
|
textAlign: parentCenterPosition
|
|
|
|
? "center"
|
|
|
|
: this.state.currentItemTextAlign,
|
|
|
|
verticalAlign: parentCenterPosition
|
|
|
|
? "middle"
|
|
|
|
: DEFAULT_VERTICAL_ALIGN,
|
|
|
|
});
|
2020-03-07 10:20:38 -05:00
|
|
|
|
2020-06-25 21:21:27 +02:00
|
|
|
this.setState({ editingElement: element });
|
|
|
|
|
|
|
|
if (existingTextElement) {
|
|
|
|
// if text element is no longer centered to a container, reset
|
|
|
|
// verticalAlign to default because it's currently internal-only
|
|
|
|
if (!parentCenterPosition || element.textAlign !== "center") {
|
|
|
|
mutateElement(element, { verticalAlign: DEFAULT_VERTICAL_ALIGN });
|
|
|
|
}
|
2020-04-03 14:16:14 +02:00
|
|
|
} else {
|
2020-07-30 14:50:59 +05:30
|
|
|
this.scene.replaceAllElements([
|
|
|
|
...this.scene.getElementsIncludingDeleted(),
|
2020-04-03 14:16:14 +02:00
|
|
|
element,
|
|
|
|
]);
|
2020-03-07 10:20:38 -05:00
|
|
|
|
2020-06-25 21:21:27 +02:00
|
|
|
// case: creating new text not centered to parent elemenent → offset Y
|
|
|
|
// so that the text is centered to cursor position
|
|
|
|
if (!parentCenterPosition) {
|
|
|
|
mutateElement(element, {
|
|
|
|
y: element.y - element.baseline / 2,
|
|
|
|
});
|
2020-03-08 18:09:45 -07:00
|
|
|
}
|
|
|
|
}
|
2020-03-07 10:20:38 -05:00
|
|
|
|
2020-04-03 14:16:14 +02:00
|
|
|
this.setState({
|
|
|
|
editingElement: element,
|
|
|
|
});
|
2020-03-17 23:11:27 -07:00
|
|
|
|
2020-04-03 14:16:14 +02:00
|
|
|
this.handleTextWysiwyg(element, {
|
2020-06-25 21:21:27 +02:00
|
|
|
isExistingElement: !!existingTextElement,
|
2020-03-08 18:09:45 -07:00
|
|
|
});
|
|
|
|
};
|
2020-03-07 10:20:38 -05:00
|
|
|
|
2020-03-26 01:12:51 +09:00
|
|
|
private handleCanvasDoubleClick = (
|
|
|
|
event: React.MouseEvent<HTMLCanvasElement>,
|
|
|
|
) => {
|
|
|
|
// case: double-clicking with arrow/line tool selected would both create
|
|
|
|
// text and enter multiElement mode
|
|
|
|
if (this.state.multiElement) {
|
|
|
|
return;
|
|
|
|
}
|
2020-07-27 14:05:52 -07:00
|
|
|
// we should only be able to double click when mode is selection
|
|
|
|
if (this.state.elementType !== "selection") {
|
|
|
|
return;
|
|
|
|
}
|
2020-03-26 01:12:51 +09:00
|
|
|
|
2020-06-01 11:35:44 +02:00
|
|
|
const selectedElements = getSelectedElements(
|
2020-07-30 14:50:59 +05:30
|
|
|
this.scene.getElements(),
|
2020-06-01 11:35:44 +02:00
|
|
|
this.state,
|
|
|
|
);
|
|
|
|
|
|
|
|
if (selectedElements.length === 1 && isLinearElement(selectedElements[0])) {
|
|
|
|
if (
|
|
|
|
!this.state.editingLinearElement ||
|
|
|
|
this.state.editingLinearElement.elementId !== selectedElements[0].id
|
|
|
|
) {
|
|
|
|
history.resumeRecording();
|
|
|
|
this.setState({
|
2020-07-30 14:50:59 +05:30
|
|
|
editingLinearElement: new LinearElementEditor(
|
|
|
|
selectedElements[0],
|
|
|
|
this.scene,
|
|
|
|
),
|
2020-06-01 11:35:44 +02:00
|
|
|
});
|
|
|
|
}
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
resetCursor();
|
|
|
|
|
2020-06-25 21:21:27 +02:00
|
|
|
const { x: sceneX, y: sceneY } = viewportCoordsToSceneCoords(
|
2020-03-26 01:12:51 +09:00
|
|
|
event,
|
|
|
|
this.state,
|
|
|
|
this.canvas,
|
|
|
|
window.devicePixelRatio,
|
|
|
|
);
|
|
|
|
|
2020-05-26 13:07:46 -07:00
|
|
|
const selectedGroupIds = getSelectedGroupIds(this.state);
|
|
|
|
|
|
|
|
if (selectedGroupIds.length > 0) {
|
2020-08-08 21:04:15 -07:00
|
|
|
const hitElement = this.getElementAtPosition(sceneX, sceneY);
|
2020-05-26 13:07:46 -07:00
|
|
|
|
|
|
|
const selectedGroupId =
|
|
|
|
hitElement &&
|
|
|
|
getSelectedGroupIdForElement(hitElement, this.state.selectedGroupIds);
|
|
|
|
|
|
|
|
if (selectedGroupId) {
|
|
|
|
this.setState((prevState) =>
|
|
|
|
selectGroupsForSelectedElements(
|
|
|
|
{
|
|
|
|
...prevState,
|
|
|
|
editingGroupId: selectedGroupId,
|
|
|
|
selectedElementIds: { [hitElement!.id]: true },
|
|
|
|
selectedGroupIds: {},
|
|
|
|
},
|
2020-07-30 14:50:59 +05:30
|
|
|
this.scene.getElements(),
|
2020-05-26 13:07:46 -07:00
|
|
|
),
|
|
|
|
);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
resetCursor();
|
|
|
|
|
2020-03-26 01:12:51 +09:00
|
|
|
this.startTextEditing({
|
2020-06-25 21:21:27 +02:00
|
|
|
sceneX,
|
|
|
|
sceneY,
|
|
|
|
insertAtParentCenter: !event.altKey,
|
2020-03-26 01:12:51 +09:00
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2020-03-08 18:09:45 -07:00
|
|
|
private handleCanvasPointerMove = (
|
|
|
|
event: React.PointerEvent<HTMLCanvasElement>,
|
|
|
|
) => {
|
2020-04-04 16:12:19 +01:00
|
|
|
this.savePointer(event.clientX, event.clientY, this.state.cursorButton);
|
|
|
|
|
2020-03-09 17:17:26 +01:00
|
|
|
if (gesture.pointers.has(event.pointerId)) {
|
|
|
|
gesture.pointers.set(event.pointerId, {
|
|
|
|
x: event.clientX,
|
|
|
|
y: event.clientY,
|
|
|
|
});
|
|
|
|
}
|
2020-03-07 10:20:38 -05:00
|
|
|
|
2020-03-08 19:25:16 -07:00
|
|
|
if (gesture.pointers.size === 2) {
|
2020-03-08 18:09:45 -07:00
|
|
|
const center = getCenter(gesture.pointers);
|
|
|
|
const deltaX = center.x - gesture.lastCenter!.x;
|
|
|
|
const deltaY = center.y - gesture.lastCenter!.y;
|
|
|
|
gesture.lastCenter = center;
|
2020-03-07 10:20:38 -05:00
|
|
|
|
2020-03-08 19:25:16 -07:00
|
|
|
const distance = getDistance(Array.from(gesture.pointers.values()));
|
2020-03-08 18:09:45 -07:00
|
|
|
const scaleFactor = distance / gesture.initialDistance!;
|
2020-03-07 10:20:38 -05:00
|
|
|
|
2020-03-08 18:09:45 -07:00
|
|
|
this.setState({
|
|
|
|
scrollX: normalizeScroll(this.state.scrollX + deltaX / this.state.zoom),
|
|
|
|
scrollY: normalizeScroll(this.state.scrollY + deltaY / this.state.zoom),
|
|
|
|
zoom: getNormalizedZoom(gesture.initialScale! * scaleFactor),
|
2020-03-28 16:59:36 -07:00
|
|
|
shouldCacheIgnoreZoom: true,
|
2020-03-08 18:09:45 -07:00
|
|
|
});
|
2020-03-28 16:59:36 -07:00
|
|
|
this.resetShouldCacheIgnoreZoomDebounced();
|
2020-03-08 18:09:45 -07:00
|
|
|
} else {
|
|
|
|
gesture.lastCenter = gesture.initialDistance = gesture.initialScale = null;
|
|
|
|
}
|
2020-03-07 10:20:38 -05:00
|
|
|
|
2020-03-08 18:09:45 -07:00
|
|
|
if (isHoldingSpace || isPanning || isDraggingScrollBar) {
|
|
|
|
return;
|
|
|
|
}
|
2020-07-09 14:15:42 -07:00
|
|
|
const isPointerOverScrollBars = isOverScrollBars(
|
|
|
|
currentScrollBars,
|
|
|
|
event.clientX,
|
|
|
|
event.clientY,
|
|
|
|
);
|
|
|
|
const isOverScrollBar = isPointerOverScrollBars.isOverEither;
|
2020-03-08 18:09:45 -07:00
|
|
|
if (!this.state.draggingElement && !this.state.multiElement) {
|
|
|
|
if (isOverScrollBar) {
|
|
|
|
resetCursor();
|
|
|
|
} else {
|
|
|
|
setCursorForShape(this.state.elementType);
|
|
|
|
}
|
|
|
|
}
|
2020-03-07 10:20:38 -05:00
|
|
|
|
2020-08-08 21:04:15 -07:00
|
|
|
const scenePointer = viewportCoordsToSceneCoords(
|
2020-03-08 18:09:45 -07:00
|
|
|
event,
|
|
|
|
this.state,
|
|
|
|
this.canvas,
|
2020-03-15 12:25:18 -07:00
|
|
|
window.devicePixelRatio,
|
2020-03-08 18:09:45 -07:00
|
|
|
);
|
2020-08-08 21:04:15 -07:00
|
|
|
const { x: scenePointerX, y: scenePointerY } = scenePointer;
|
2020-06-01 11:35:44 +02:00
|
|
|
|
|
|
|
if (
|
|
|
|
this.state.editingLinearElement &&
|
2020-07-30 12:58:06 +02:00
|
|
|
!this.state.editingLinearElement.isDragging
|
2020-06-01 11:35:44 +02:00
|
|
|
) {
|
|
|
|
const editingLinearElement = LinearElementEditor.handlePointerMove(
|
|
|
|
event,
|
|
|
|
scenePointerX,
|
|
|
|
scenePointerY,
|
|
|
|
this.state.editingLinearElement,
|
2020-07-31 00:09:51 +09:00
|
|
|
this.state.gridSize,
|
2020-06-01 11:35:44 +02:00
|
|
|
);
|
|
|
|
if (editingLinearElement !== this.state.editingLinearElement) {
|
|
|
|
this.setState({ editingLinearElement });
|
|
|
|
}
|
2020-08-08 21:04:15 -07:00
|
|
|
if (editingLinearElement.lastUncommittedPoint != null) {
|
|
|
|
this.maybeSuggestBindingAtCursor(scenePointer);
|
|
|
|
} else {
|
|
|
|
this.setState({ suggestedBindings: [] });
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (isBindingElementType(this.state.elementType)) {
|
|
|
|
// Hovering with a selected tool or creating new linear element via click
|
|
|
|
// and point
|
|
|
|
const { draggingElement } = this.state;
|
|
|
|
if (isBindingElement(draggingElement)) {
|
|
|
|
this.maybeSuggestBindingForLinearElementAtCursor(
|
|
|
|
draggingElement,
|
|
|
|
"end",
|
|
|
|
scenePointer,
|
|
|
|
this.state.startBoundElement,
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
this.maybeSuggestBindingAtCursor(scenePointer);
|
|
|
|
}
|
2020-06-01 11:35:44 +02:00
|
|
|
}
|
|
|
|
|
2020-03-08 18:09:45 -07:00
|
|
|
if (this.state.multiElement) {
|
|
|
|
const { multiElement } = this.state;
|
2020-03-18 16:43:06 +01:00
|
|
|
const { x: rx, y: ry } = multiElement;
|
2020-03-14 21:48:51 -07:00
|
|
|
|
2020-03-18 16:43:06 +01:00
|
|
|
const { points, lastCommittedPoint } = multiElement;
|
|
|
|
const lastPoint = points[points.length - 1];
|
|
|
|
|
|
|
|
setCursorForShape(this.state.elementType);
|
|
|
|
|
|
|
|
if (lastPoint === lastCommittedPoint) {
|
|
|
|
// if we haven't yet created a temp point and we're beyond commit-zone
|
|
|
|
// threshold, add a point
|
|
|
|
if (
|
2020-06-01 11:35:44 +02:00
|
|
|
distance2d(
|
|
|
|
scenePointerX - rx,
|
|
|
|
scenePointerY - ry,
|
|
|
|
lastPoint[0],
|
|
|
|
lastPoint[1],
|
|
|
|
) >= LINE_CONFIRM_THRESHOLD
|
2020-03-18 16:43:06 +01:00
|
|
|
) {
|
|
|
|
mutateElement(multiElement, {
|
2020-06-01 11:35:44 +02:00
|
|
|
points: [...points, [scenePointerX - rx, scenePointerY - ry]],
|
2020-03-18 16:43:06 +01:00
|
|
|
});
|
|
|
|
} else {
|
|
|
|
document.documentElement.style.cursor = CURSOR_TYPE.POINTER;
|
|
|
|
// in this branch, we're inside the commit zone, and no uncommitted
|
|
|
|
// point exists. Thus do nothing (don't add/remove points).
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// cursor moved inside commit zone, and there's uncommitted point,
|
|
|
|
// thus remove it
|
|
|
|
if (
|
|
|
|
points.length > 2 &&
|
|
|
|
lastCommittedPoint &&
|
|
|
|
distance2d(
|
2020-06-01 11:35:44 +02:00
|
|
|
scenePointerX - rx,
|
|
|
|
scenePointerY - ry,
|
2020-03-18 16:43:06 +01:00
|
|
|
lastCommittedPoint[0],
|
|
|
|
lastCommittedPoint[1],
|
2020-04-09 01:46:47 -07:00
|
|
|
) < LINE_CONFIRM_THRESHOLD
|
2020-03-18 16:43:06 +01:00
|
|
|
) {
|
|
|
|
document.documentElement.style.cursor = CURSOR_TYPE.POINTER;
|
|
|
|
mutateElement(multiElement, {
|
|
|
|
points: points.slice(0, -1),
|
|
|
|
});
|
|
|
|
} else {
|
2020-04-09 01:46:47 -07:00
|
|
|
if (isPathALoop(points)) {
|
|
|
|
document.documentElement.style.cursor = CURSOR_TYPE.POINTER;
|
|
|
|
}
|
2020-03-18 16:43:06 +01:00
|
|
|
// update last uncommitted point
|
|
|
|
mutateElement(multiElement, {
|
2020-06-01 11:35:44 +02:00
|
|
|
points: [
|
|
|
|
...points.slice(0, -1),
|
|
|
|
[scenePointerX - rx, scenePointerY - ry],
|
|
|
|
],
|
2020-03-18 16:43:06 +01:00
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
2020-08-08 21:04:15 -07:00
|
|
|
|
2020-03-08 18:09:45 -07:00
|
|
|
return;
|
|
|
|
}
|
2020-03-07 10:20:38 -05:00
|
|
|
|
2020-03-08 18:09:45 -07:00
|
|
|
const hasDeselectedButton = Boolean(event.buttons);
|
2020-04-06 22:26:54 +02:00
|
|
|
if (
|
|
|
|
hasDeselectedButton ||
|
|
|
|
(this.state.elementType !== "selection" &&
|
|
|
|
this.state.elementType !== "text")
|
|
|
|
) {
|
2020-03-08 18:09:45 -07:00
|
|
|
return;
|
|
|
|
}
|
2020-03-07 10:20:38 -05:00
|
|
|
|
2020-07-30 14:50:59 +05:30
|
|
|
const elements = this.scene.getElements();
|
2020-04-08 09:49:52 -07:00
|
|
|
|
|
|
|
const selectedElements = getSelectedElements(elements, this.state);
|
2020-06-01 11:35:44 +02:00
|
|
|
if (
|
|
|
|
selectedElements.length === 1 &&
|
|
|
|
!isOverScrollBar &&
|
|
|
|
!this.state.editingLinearElement
|
|
|
|
) {
|
2020-08-10 14:16:39 +02:00
|
|
|
const elementWithTransformHandleType = getElementWithTransformHandleType(
|
2020-04-08 09:49:52 -07:00
|
|
|
elements,
|
2020-03-08 18:09:45 -07:00
|
|
|
this.state,
|
2020-06-01 11:35:44 +02:00
|
|
|
scenePointerX,
|
|
|
|
scenePointerY,
|
2020-03-08 18:09:45 -07:00
|
|
|
this.state.zoom,
|
|
|
|
event.pointerType,
|
|
|
|
);
|
2020-08-10 14:16:39 +02:00
|
|
|
if (
|
|
|
|
elementWithTransformHandleType &&
|
|
|
|
elementWithTransformHandleType.transformHandleType
|
|
|
|
) {
|
2020-03-08 18:09:45 -07:00
|
|
|
document.documentElement.style.cursor = getCursorForResizingElement(
|
2020-08-10 14:16:39 +02:00
|
|
|
elementWithTransformHandleType,
|
2020-03-08 18:09:45 -07:00
|
|
|
);
|
|
|
|
return;
|
|
|
|
}
|
2020-04-07 17:49:59 +09:00
|
|
|
} else if (selectedElements.length > 1 && !isOverScrollBar) {
|
2020-08-10 14:16:39 +02:00
|
|
|
const transformHandleType = getTransformHandleTypeFromCoords(
|
2020-06-08 18:25:20 +09:00
|
|
|
getCommonBounds(selectedElements),
|
|
|
|
scenePointerX,
|
|
|
|
scenePointerY,
|
|
|
|
this.state.zoom,
|
|
|
|
event.pointerType,
|
|
|
|
);
|
2020-08-10 14:16:39 +02:00
|
|
|
if (transformHandleType) {
|
2020-06-08 18:25:20 +09:00
|
|
|
document.documentElement.style.cursor = getCursorForResizingElement({
|
2020-08-10 14:16:39 +02:00
|
|
|
transformHandleType,
|
2020-06-08 18:25:20 +09:00
|
|
|
});
|
|
|
|
return;
|
2020-04-07 17:49:59 +09:00
|
|
|
}
|
2020-03-08 18:09:45 -07:00
|
|
|
}
|
2020-08-26 17:37:44 +01:00
|
|
|
|
|
|
|
const hitElement = this.getElementAtPosition(
|
|
|
|
scenePointer.x,
|
|
|
|
scenePointer.y,
|
|
|
|
);
|
2020-04-06 22:26:54 +02:00
|
|
|
if (this.state.elementType === "text") {
|
|
|
|
document.documentElement.style.cursor = isTextElement(hitElement)
|
|
|
|
? CURSOR_TYPE.TEXT
|
|
|
|
: CURSOR_TYPE.CROSSHAIR;
|
2020-08-26 17:37:44 +01:00
|
|
|
} else if (isOverScrollBar) {
|
|
|
|
document.documentElement.style.cursor = CURSOR_TYPE.AUTO;
|
|
|
|
} else if (
|
|
|
|
hitElement ||
|
|
|
|
this.isHittingCommonBoundingBoxOfSelectedElements(
|
|
|
|
scenePointer,
|
|
|
|
selectedElements,
|
|
|
|
)
|
|
|
|
) {
|
|
|
|
document.documentElement.style.cursor = CURSOR_TYPE.MOVE;
|
2020-04-06 22:26:54 +02:00
|
|
|
} else {
|
2020-08-26 17:37:44 +01:00
|
|
|
document.documentElement.style.cursor = CURSOR_TYPE.AUTO;
|
2020-04-06 22:26:54 +02:00
|
|
|
}
|
2020-03-08 18:09:45 -07:00
|
|
|
};
|
2020-03-07 10:20:38 -05:00
|
|
|
|
2020-07-02 22:12:56 +01:00
|
|
|
// set touch moving for mobile context menu
|
|
|
|
private handleTouchMove = (event: React.TouchEvent<HTMLCanvasElement>) => {
|
|
|
|
touchMoving = true;
|
|
|
|
};
|
|
|
|
|
2020-03-08 18:09:45 -07:00
|
|
|
private handleCanvasPointerDown = (
|
|
|
|
event: React.PointerEvent<HTMLCanvasElement>,
|
|
|
|
) => {
|
2020-04-14 12:33:57 +02:00
|
|
|
event.persist();
|
|
|
|
|
2020-07-08 22:07:51 -07:00
|
|
|
this.maybeOpenContextMenuAfterPointerDownOnTouchDevices(event);
|
|
|
|
this.maybeCleanupAfterMissingPointerUp(event);
|
2020-03-07 10:20:38 -05:00
|
|
|
|
2020-03-08 18:09:45 -07:00
|
|
|
if (isPanning) {
|
|
|
|
return;
|
|
|
|
}
|
2020-03-07 10:20:38 -05:00
|
|
|
|
2020-04-04 16:12:19 +01:00
|
|
|
this.setState({
|
|
|
|
lastPointerDownWith: event.pointerType,
|
|
|
|
cursorButton: "down",
|
|
|
|
});
|
|
|
|
this.savePointer(event.clientX, event.clientY, "down");
|
2020-03-07 10:20:38 -05:00
|
|
|
|
2020-07-08 22:07:51 -07:00
|
|
|
if (this.handleCanvasPanUsingWheelOrSpaceDrag(event)) {
|
2020-03-08 18:09:45 -07:00
|
|
|
return;
|
|
|
|
}
|
2020-03-07 10:20:38 -05:00
|
|
|
|
2020-03-08 18:09:45 -07:00
|
|
|
// only handle left mouse button or touch
|
|
|
|
if (
|
|
|
|
event.button !== POINTER_BUTTON.MAIN &&
|
|
|
|
event.button !== POINTER_BUTTON.TOUCH
|
|
|
|
) {
|
|
|
|
return;
|
|
|
|
}
|
2020-03-07 10:20:38 -05:00
|
|
|
|
2020-07-08 22:07:51 -07:00
|
|
|
this.updateGestureOnPointerDown(event);
|
2020-03-07 10:20:38 -05:00
|
|
|
|
2020-03-08 18:09:45 -07:00
|
|
|
// fixes pointermove causing selection of UI texts #32
|
|
|
|
event.preventDefault();
|
|
|
|
// Preventing the event above disables default behavior
|
|
|
|
// of defocusing potentially focused element, which is what we
|
|
|
|
// want when clicking inside the canvas.
|
|
|
|
if (document.activeElement instanceof HTMLElement) {
|
|
|
|
document.activeElement.blur();
|
|
|
|
}
|
2020-03-07 10:20:38 -05:00
|
|
|
|
2020-03-08 18:09:45 -07:00
|
|
|
// don't select while panning
|
2020-03-08 19:25:16 -07:00
|
|
|
if (gesture.pointers.size > 1) {
|
2020-03-08 18:09:45 -07:00
|
|
|
return;
|
|
|
|
}
|
2020-03-07 10:20:38 -05:00
|
|
|
|
2020-07-09 09:30:38 -07:00
|
|
|
// State for the duration of a pointer interaction, which starts with a
|
|
|
|
// pointerDown event, ends with a pointerUp event (or another pointerDown)
|
2020-07-09 14:15:42 -07:00
|
|
|
const pointerDownState = this.initialPointerDownState(event);
|
2020-07-09 09:30:38 -07:00
|
|
|
|
2020-07-09 14:15:42 -07:00
|
|
|
if (this.handleDraggingScrollBar(event, pointerDownState)) {
|
2020-03-08 18:09:45 -07:00
|
|
|
return;
|
|
|
|
}
|
2020-03-07 10:20:38 -05:00
|
|
|
|
2020-07-09 09:30:38 -07:00
|
|
|
this.clearSelectionIfNotUsingSelection();
|
2020-06-02 18:41:40 +02:00
|
|
|
|
2020-07-09 09:30:38 -07:00
|
|
|
if (this.handleSelectionOnPointerDown(event, pointerDownState)) {
|
|
|
|
return;
|
2020-03-08 18:09:45 -07:00
|
|
|
}
|
2020-03-07 10:20:38 -05:00
|
|
|
|
2020-03-17 20:55:40 +01:00
|
|
|
if (this.state.elementType === "text") {
|
2020-07-09 09:30:38 -07:00
|
|
|
this.handleTextOnPointerDown(event, pointerDownState);
|
2020-03-08 18:09:45 -07:00
|
|
|
return;
|
|
|
|
} else if (
|
|
|
|
this.state.elementType === "arrow" ||
|
2020-05-12 20:10:11 +01:00
|
|
|
this.state.elementType === "draw" ||
|
2020-03-08 18:09:45 -07:00
|
|
|
this.state.elementType === "line"
|
|
|
|
) {
|
2020-07-09 09:30:38 -07:00
|
|
|
this.handleLinearElementOnPointerDown(
|
|
|
|
event,
|
|
|
|
this.state.elementType,
|
|
|
|
pointerDownState,
|
|
|
|
);
|
2020-03-08 18:09:45 -07:00
|
|
|
} else {
|
2020-07-09 09:30:38 -07:00
|
|
|
this.createGenericElementOnPointerDown(
|
|
|
|
this.state.elementType,
|
|
|
|
pointerDownState,
|
|
|
|
);
|
2020-03-08 18:09:45 -07:00
|
|
|
}
|
2020-03-07 10:20:38 -05:00
|
|
|
|
2020-07-09 14:15:42 -07:00
|
|
|
const onPointerMove = this.onPointerMoveFromPointerDownHandler(
|
|
|
|
pointerDownState,
|
|
|
|
);
|
|
|
|
|
|
|
|
const onPointerUp = this.onPointerUpFromPointerDownHandler(
|
|
|
|
pointerDownState,
|
|
|
|
);
|
|
|
|
|
|
|
|
lastPointerUp = onPointerUp;
|
|
|
|
|
|
|
|
window.addEventListener(EVENT.POINTER_MOVE, onPointerMove);
|
|
|
|
window.addEventListener(EVENT.POINTER_UP, onPointerUp);
|
|
|
|
pointerDownState.eventListeners.onMove = onPointerMove;
|
|
|
|
pointerDownState.eventListeners.onUp = 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);
|
2020-04-11 13:37:43 +02:00
|
|
|
|
2020-07-09 14:15:42 -07:00
|
|
|
document.documentElement.style.cursor = CURSOR_TYPE.GRABBING;
|
|
|
|
let { clientX: lastX, clientY: lastY } = event;
|
2020-03-16 19:07:47 -07:00
|
|
|
const onPointerMove = withBatchedUpdates((event: PointerEvent) => {
|
2020-07-09 14:15:42 -07:00
|
|
|
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);
|
2020-06-24 20:38:42 +09:00
|
|
|
}
|
|
|
|
|
2020-07-09 14:15:42 -07:00
|
|
|
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()),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private initialPointerDownState(
|
|
|
|
event: React.PointerEvent<HTMLCanvasElement>,
|
|
|
|
): PointerDownState {
|
|
|
|
const origin = viewportCoordsToSceneCoords(
|
|
|
|
event,
|
|
|
|
this.state,
|
|
|
|
this.canvas,
|
|
|
|
window.devicePixelRatio,
|
|
|
|
);
|
2020-07-26 19:21:38 +09:00
|
|
|
const selectedElements = getSelectedElements(
|
2020-07-30 14:50:59 +05:30
|
|
|
this.scene.getElements(),
|
2020-07-26 19:21:38 +09:00
|
|
|
this.state,
|
|
|
|
);
|
|
|
|
const [minX, minY, maxX, maxY] = getCommonBounds(selectedElements);
|
2020-07-09 14:15:42 -07:00
|
|
|
|
|
|
|
return {
|
|
|
|
origin,
|
|
|
|
originInGrid: tupleToCoors(
|
|
|
|
getGridPoint(origin.x, origin.y, this.state.gridSize),
|
|
|
|
),
|
|
|
|
scrollbars: isOverScrollBars(
|
|
|
|
currentScrollBars,
|
|
|
|
event.clientX,
|
|
|
|
event.clientY,
|
|
|
|
),
|
|
|
|
// we need to duplicate because we'll be updating this state
|
|
|
|
lastCoords: { ...origin },
|
|
|
|
resize: {
|
2020-08-10 14:16:39 +02:00
|
|
|
handleType: false,
|
2020-07-09 14:15:42 -07:00
|
|
|
isResizing: false,
|
|
|
|
offset: { x: 0, y: 0 },
|
|
|
|
arrowDirection: "origin",
|
2020-07-26 19:21:38 +09:00
|
|
|
center: { x: (maxX + minX) / 2, y: (maxY + minY) / 2 },
|
|
|
|
originalElements: selectedElements.map((element) => ({ ...element })),
|
2020-07-09 14:15:42 -07:00
|
|
|
},
|
|
|
|
hit: {
|
|
|
|
element: null,
|
2020-08-26 17:37:44 +01:00
|
|
|
allHitElements: [],
|
2020-07-09 14:15:42 -07:00
|
|
|
wasAddedToSelection: false,
|
|
|
|
hasBeenDuplicated: false,
|
2020-08-26 17:37:44 +01:00
|
|
|
hasHitCommonBoundingBoxOfSelectedElements: this.isHittingCommonBoundingBoxOfSelectedElements(
|
|
|
|
origin,
|
|
|
|
selectedElements,
|
|
|
|
),
|
2020-07-09 14:15:42 -07:00
|
|
|
},
|
|
|
|
drag: {
|
|
|
|
hasOccurred: false,
|
|
|
|
offset: null,
|
|
|
|
},
|
|
|
|
eventListeners: {
|
|
|
|
onMove: null,
|
|
|
|
onUp: null,
|
|
|
|
},
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
// Returns whether the event is a dragging a scrollbar
|
|
|
|
private handleDraggingScrollBar(
|
|
|
|
event: React.PointerEvent<HTMLCanvasElement>,
|
|
|
|
pointerDownState: PointerDownState,
|
|
|
|
): boolean {
|
|
|
|
if (
|
|
|
|
!(pointerDownState.scrollbars.isOverEither && !this.state.multiElement)
|
|
|
|
) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
isDraggingScrollBar = true;
|
|
|
|
pointerDownState.lastCoords.x = event.clientX;
|
|
|
|
pointerDownState.lastCoords.y = event.clientY;
|
|
|
|
const onPointerMove = withBatchedUpdates((event: PointerEvent) => {
|
2020-03-08 18:09:45 -07:00
|
|
|
const target = event.target;
|
|
|
|
if (!(target instanceof HTMLElement)) {
|
|
|
|
return;
|
|
|
|
}
|
2020-03-07 10:20:38 -05:00
|
|
|
|
2020-08-08 21:04:15 -07:00
|
|
|
this.handlePointerMoveOverScrollbars(event, pointerDownState);
|
2020-07-09 14:15:42 -07:00
|
|
|
});
|
2020-03-07 10:20:38 -05:00
|
|
|
|
2020-07-09 14:15:42 -07:00
|
|
|
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);
|
|
|
|
});
|
2020-04-07 17:49:59 +09:00
|
|
|
|
2020-07-09 14:15:42 -07:00
|
|
|
lastPointerUp = onPointerUp;
|
2020-03-07 10:20:38 -05:00
|
|
|
|
2020-07-09 14:15:42 -07:00
|
|
|
window.addEventListener(EVENT.POINTER_MOVE, onPointerMove);
|
|
|
|
window.addEventListener(EVENT.POINTER_UP, onPointerUp);
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
private clearSelectionIfNotUsingSelection = (): void => {
|
|
|
|
if (this.state.elementType !== "selection") {
|
|
|
|
this.setState({
|
|
|
|
selectedElementIds: {},
|
|
|
|
selectedGroupIds: {},
|
|
|
|
editingGroupId: null,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
// Returns whether the pointer event has been completely handled
|
|
|
|
private handleSelectionOnPointerDown = (
|
|
|
|
event: React.PointerEvent<HTMLCanvasElement>,
|
|
|
|
pointerDownState: PointerDownState,
|
|
|
|
): boolean => {
|
|
|
|
if (this.state.elementType === "selection") {
|
2020-07-30 14:50:59 +05:30
|
|
|
const elements = this.scene.getElements();
|
2020-07-09 14:15:42 -07:00
|
|
|
const selectedElements = getSelectedElements(elements, this.state);
|
|
|
|
if (selectedElements.length === 1 && !this.state.editingLinearElement) {
|
2020-08-10 14:16:39 +02:00
|
|
|
const elementWithTransformHandleType = getElementWithTransformHandleType(
|
2020-07-09 14:15:42 -07:00
|
|
|
elements,
|
2020-05-09 17:57:00 +09:00
|
|
|
this.state,
|
2020-07-09 14:15:42 -07:00
|
|
|
pointerDownState.origin.x,
|
|
|
|
pointerDownState.origin.y,
|
|
|
|
this.state.zoom,
|
|
|
|
event.pointerType,
|
2020-05-09 17:57:00 +09:00
|
|
|
);
|
2020-08-10 14:16:39 +02:00
|
|
|
if (elementWithTransformHandleType != null) {
|
2020-07-09 14:15:42 -07:00
|
|
|
this.setState({
|
2020-08-10 14:16:39 +02:00
|
|
|
resizingElement: elementWithTransformHandleType.element,
|
2020-07-09 14:15:42 -07:00
|
|
|
});
|
2020-08-10 14:16:39 +02:00
|
|
|
pointerDownState.resize.handleType =
|
|
|
|
elementWithTransformHandleType.transformHandleType;
|
2020-07-09 14:15:42 -07:00
|
|
|
}
|
|
|
|
} else if (selectedElements.length > 1) {
|
2020-08-10 14:16:39 +02:00
|
|
|
pointerDownState.resize.handleType = getTransformHandleTypeFromCoords(
|
2020-07-09 14:15:42 -07:00
|
|
|
getCommonBounds(selectedElements),
|
|
|
|
pointerDownState.origin.x,
|
|
|
|
pointerDownState.origin.y,
|
|
|
|
this.state.zoom,
|
|
|
|
event.pointerType,
|
|
|
|
);
|
|
|
|
}
|
2020-08-10 14:16:39 +02:00
|
|
|
if (pointerDownState.resize.handleType) {
|
2020-07-09 14:15:42 -07:00
|
|
|
document.documentElement.style.cursor = getCursorForResizingElement({
|
2020-08-10 14:16:39 +02:00
|
|
|
transformHandleType: pointerDownState.resize.handleType,
|
2020-05-09 17:57:00 +09:00
|
|
|
});
|
2020-07-09 14:15:42 -07:00
|
|
|
pointerDownState.resize.isResizing = true;
|
|
|
|
pointerDownState.resize.offset = tupleToCoors(
|
|
|
|
getResizeOffsetXY(
|
2020-08-10 14:16:39 +02:00
|
|
|
pointerDownState.resize.handleType,
|
2020-07-09 14:15:42 -07:00
|
|
|
selectedElements,
|
|
|
|
pointerDownState.origin.x,
|
|
|
|
pointerDownState.origin.y,
|
|
|
|
),
|
2020-06-24 00:24:52 +09:00
|
|
|
);
|
2020-05-11 00:41:36 +09:00
|
|
|
if (
|
2020-07-09 14:15:42 -07:00
|
|
|
selectedElements.length === 1 &&
|
|
|
|
isLinearElement(selectedElements[0]) &&
|
|
|
|
selectedElements[0].points.length === 2
|
2020-05-11 00:41:36 +09:00
|
|
|
) {
|
2020-07-09 14:15:42 -07:00
|
|
|
pointerDownState.resize.arrowDirection = getResizeArrowDirection(
|
2020-08-10 14:16:39 +02:00
|
|
|
pointerDownState.resize.handleType,
|
2020-07-09 14:15:42 -07:00
|
|
|
selectedElements[0],
|
|
|
|
);
|
2020-05-09 17:57:00 +09:00
|
|
|
}
|
2020-07-09 14:15:42 -07:00
|
|
|
} else {
|
|
|
|
if (this.state.editingLinearElement) {
|
|
|
|
const ret = LinearElementEditor.handlePointerDown(
|
|
|
|
event,
|
|
|
|
this.state,
|
|
|
|
(appState) => this.setState(appState),
|
|
|
|
history,
|
2020-08-08 21:04:15 -07:00
|
|
|
pointerDownState.origin,
|
2020-07-09 14:15:42 -07:00
|
|
|
);
|
|
|
|
if (ret.hitElement) {
|
|
|
|
pointerDownState.hit.element = ret.hitElement;
|
|
|
|
}
|
|
|
|
if (ret.didAddPoint) {
|
|
|
|
return true;
|
|
|
|
}
|
2020-06-01 11:35:44 +02:00
|
|
|
}
|
|
|
|
|
2020-07-09 14:15:42 -07:00
|
|
|
// hitElement may already be set above, so check first
|
|
|
|
pointerDownState.hit.element =
|
|
|
|
pointerDownState.hit.element ??
|
2020-08-08 21:04:15 -07:00
|
|
|
this.getElementAtPosition(
|
2020-07-09 14:15:42 -07:00
|
|
|
pointerDownState.origin.x,
|
|
|
|
pointerDownState.origin.y,
|
2020-03-08 18:09:45 -07:00
|
|
|
);
|
2020-04-11 13:37:43 +02:00
|
|
|
|
2020-08-26 17:37:44 +01:00
|
|
|
// For overlapped elements one position may hit
|
|
|
|
// multiple elements
|
|
|
|
pointerDownState.hit.allHitElements = this.getElementsAtPosition(
|
|
|
|
pointerDownState.origin.x,
|
|
|
|
pointerDownState.origin.y,
|
2020-07-09 14:15:42 -07:00
|
|
|
);
|
2020-04-11 13:37:43 +02:00
|
|
|
|
2020-07-09 14:15:42 -07:00
|
|
|
const hitElement = pointerDownState.hit.element;
|
2020-08-26 17:37:44 +01:00
|
|
|
const someHitElementIsSelected = pointerDownState.hit.allHitElements.some(
|
|
|
|
(element) => this.isASelectedElement(element),
|
|
|
|
);
|
|
|
|
if (
|
|
|
|
(hitElement === null || !someHitElementIsSelected) &&
|
|
|
|
!event.shiftKey &&
|
|
|
|
!pointerDownState.hit.hasHitCommonBoundingBoxOfSelectedElements
|
|
|
|
) {
|
|
|
|
this.clearSelection(hitElement);
|
|
|
|
}
|
|
|
|
|
|
|
|
// If we click on something
|
2020-07-09 14:15:42 -07:00
|
|
|
if (hitElement != null) {
|
|
|
|
// deselect if item is selected
|
|
|
|
// if shift is not clicked, this will always return true
|
|
|
|
// otherwise, it will trigger selection based on current
|
|
|
|
// state of the box
|
|
|
|
if (!this.state.selectedElementIds[hitElement.id]) {
|
|
|
|
// if we are currently editing a group, treat all selections outside of the group
|
|
|
|
// as exiting editing mode.
|
|
|
|
if (
|
|
|
|
this.state.editingGroupId &&
|
|
|
|
!isElementInGroup(hitElement, this.state.editingGroupId)
|
|
|
|
) {
|
|
|
|
this.setState({
|
|
|
|
selectedElementIds: {},
|
|
|
|
selectedGroupIds: {},
|
|
|
|
editingGroupId: null,
|
|
|
|
});
|
|
|
|
return true;
|
2020-04-11 13:37:43 +02:00
|
|
|
}
|
2020-08-26 17:37:44 +01:00
|
|
|
|
|
|
|
// Add hit element to selection. At this point if we're not holding
|
|
|
|
// SHIFT the previously selected element(s) were deselected above
|
|
|
|
// (make sure you use setState updater to use latest state)
|
|
|
|
if (
|
|
|
|
!someHitElementIsSelected &&
|
|
|
|
!pointerDownState.hit.hasHitCommonBoundingBoxOfSelectedElements
|
|
|
|
) {
|
|
|
|
this.setState((prevState) => {
|
|
|
|
return selectGroupsForSelectedElements(
|
|
|
|
{
|
|
|
|
...prevState,
|
|
|
|
selectedElementIds: {
|
|
|
|
...prevState.selectedElementIds,
|
|
|
|
[hitElement!.id]: true,
|
|
|
|
},
|
2020-07-09 14:15:42 -07:00
|
|
|
},
|
2020-08-26 17:37:44 +01:00
|
|
|
this.scene.getElements(),
|
|
|
|
);
|
|
|
|
});
|
|
|
|
pointerDownState.hit.wasAddedToSelection = true;
|
|
|
|
}
|
2020-04-11 13:37:43 +02:00
|
|
|
}
|
2020-03-08 18:09:45 -07:00
|
|
|
}
|
|
|
|
|
2020-07-09 14:15:42 -07:00
|
|
|
const { selectedElementIds } = this.state;
|
|
|
|
this.setState({
|
|
|
|
previousSelectedElementIds: selectedElementIds,
|
|
|
|
});
|
2020-03-08 18:09:45 -07:00
|
|
|
}
|
2020-07-09 14:15:42 -07:00
|
|
|
}
|
|
|
|
return false;
|
|
|
|
};
|
2020-03-08 18:09:45 -07:00
|
|
|
|
2020-08-26 17:37:44 +01:00
|
|
|
private isASelectedElement(hitElement: ExcalidrawElement | null): boolean {
|
|
|
|
return hitElement != null && this.state.selectedElementIds[hitElement.id];
|
|
|
|
}
|
|
|
|
|
|
|
|
private isHittingCommonBoundingBoxOfSelectedElements(
|
|
|
|
point: Readonly<{ x: number; y: number }>,
|
|
|
|
selectedElements: readonly ExcalidrawElement[],
|
|
|
|
): boolean {
|
|
|
|
if (selectedElements.length < 2) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
// How many pixels off the shape boundary we still consider a hit
|
|
|
|
const threshold = 10 / this.state.zoom;
|
|
|
|
const [x1, y1, x2, y2] = getCommonBounds(selectedElements);
|
|
|
|
return (
|
|
|
|
point.x > x1 - threshold &&
|
|
|
|
point.x < x2 + threshold &&
|
|
|
|
point.y > y1 - threshold &&
|
|
|
|
point.y < y2 + threshold
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2020-07-09 14:15:42 -07:00
|
|
|
private handleTextOnPointerDown = (
|
|
|
|
event: React.PointerEvent<HTMLCanvasElement>,
|
|
|
|
pointerDownState: PointerDownState,
|
|
|
|
): void => {
|
|
|
|
// if we're currently still editing text, clicking outside
|
|
|
|
// should only finalize it, not create another (irrespective
|
|
|
|
// of state.elementLocked)
|
|
|
|
if (this.state.editingElement?.type === "text") {
|
|
|
|
return;
|
|
|
|
}
|
2020-03-08 18:09:45 -07:00
|
|
|
|
2020-07-09 14:15:42 -07:00
|
|
|
this.startTextEditing({
|
|
|
|
sceneX: pointerDownState.origin.x,
|
|
|
|
sceneY: pointerDownState.origin.y,
|
|
|
|
insertAtParentCenter: !event.altKey,
|
2020-03-16 19:07:47 -07:00
|
|
|
});
|
2020-03-08 18:09:45 -07:00
|
|
|
|
2020-07-09 14:15:42 -07:00
|
|
|
resetCursor();
|
|
|
|
if (!this.state.elementLocked) {
|
2020-03-08 18:09:45 -07:00
|
|
|
this.setState({
|
2020-07-09 14:15:42 -07:00
|
|
|
elementType: "selection",
|
2020-03-08 18:09:45 -07:00
|
|
|
});
|
2020-07-09 14:15:42 -07:00
|
|
|
}
|
|
|
|
};
|
2020-03-08 18:09:45 -07:00
|
|
|
|
2020-07-09 14:15:42 -07:00
|
|
|
private handleLinearElementOnPointerDown = (
|
|
|
|
event: React.PointerEvent<HTMLCanvasElement>,
|
2020-08-08 21:04:15 -07:00
|
|
|
elementType: ExcalidrawLinearElement["type"],
|
2020-07-09 14:15:42 -07:00
|
|
|
pointerDownState: PointerDownState,
|
|
|
|
): void => {
|
|
|
|
if (this.state.multiElement) {
|
|
|
|
const { multiElement } = this.state;
|
2020-03-08 18:09:45 -07:00
|
|
|
|
2020-07-09 14:15:42 -07:00
|
|
|
// finalize if completing a loop
|
|
|
|
if (multiElement.type === "line" && isPathALoop(multiElement.points)) {
|
|
|
|
mutateElement(multiElement, {
|
|
|
|
lastCommittedPoint:
|
|
|
|
multiElement.points[multiElement.points.length - 1],
|
2020-03-08 18:09:45 -07:00
|
|
|
});
|
2020-07-09 14:15:42 -07:00
|
|
|
this.actionManager.executeAction(actionFinalize);
|
2020-03-08 18:09:45 -07:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2020-07-09 14:15:42 -07:00
|
|
|
const { x: rx, y: ry, lastCommittedPoint } = multiElement;
|
2020-03-08 18:09:45 -07:00
|
|
|
|
2020-07-09 14:15:42 -07:00
|
|
|
// clicking inside commit zone → finalize arrow
|
2020-05-26 13:07:46 -07:00
|
|
|
if (
|
2020-07-09 14:15:42 -07:00
|
|
|
multiElement.points.length > 1 &&
|
|
|
|
lastCommittedPoint &&
|
|
|
|
distance2d(
|
|
|
|
pointerDownState.origin.x - rx,
|
|
|
|
pointerDownState.origin.y - ry,
|
|
|
|
lastCommittedPoint[0],
|
|
|
|
lastCommittedPoint[1],
|
|
|
|
) < LINE_CONFIRM_THRESHOLD
|
2020-05-26 13:07:46 -07:00
|
|
|
) {
|
2020-07-09 14:15:42 -07:00
|
|
|
this.actionManager.executeAction(actionFinalize);
|
2020-03-08 18:09:45 -07:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2020-07-09 14:15:42 -07:00
|
|
|
this.setState((prevState) => ({
|
|
|
|
selectedElementIds: {
|
|
|
|
...prevState.selectedElementIds,
|
|
|
|
[multiElement.id]: true,
|
|
|
|
},
|
|
|
|
}));
|
|
|
|
// clicking outside commit zone → update reference for last committed
|
|
|
|
// point
|
|
|
|
mutateElement(multiElement, {
|
|
|
|
lastCommittedPoint: multiElement.points[multiElement.points.length - 1],
|
2020-07-08 22:07:51 -07:00
|
|
|
});
|
2020-07-09 14:15:42 -07:00
|
|
|
document.documentElement.style.cursor = CURSOR_TYPE.POINTER;
|
|
|
|
} else {
|
|
|
|
const [gridX, gridY] = getGridPoint(
|
|
|
|
pointerDownState.origin.x,
|
|
|
|
pointerDownState.origin.y,
|
|
|
|
elementType === "draw" ? null : this.state.gridSize,
|
|
|
|
);
|
|
|
|
const element = newLinearElement({
|
|
|
|
type: elementType,
|
|
|
|
x: gridX,
|
|
|
|
y: gridY,
|
|
|
|
strokeColor: this.state.currentItemStrokeColor,
|
|
|
|
backgroundColor: this.state.currentItemBackgroundColor,
|
|
|
|
fillStyle: this.state.currentItemFillStyle,
|
|
|
|
strokeWidth: this.state.currentItemStrokeWidth,
|
|
|
|
strokeStyle: this.state.currentItemStrokeStyle,
|
|
|
|
roughness: this.state.currentItemRoughness,
|
|
|
|
opacity: this.state.currentItemOpacity,
|
2020-08-15 00:59:43 +09:00
|
|
|
strokeSharpness: this.state.currentItemLinearStrokeSharpness,
|
2020-07-09 14:15:42 -07:00
|
|
|
});
|
|
|
|
this.setState((prevState) => ({
|
|
|
|
selectedElementIds: {
|
|
|
|
...prevState.selectedElementIds,
|
|
|
|
[element.id]: false,
|
|
|
|
},
|
|
|
|
}));
|
|
|
|
mutateElement(element, {
|
|
|
|
points: [...element.points, [0, 0]],
|
|
|
|
});
|
2020-08-08 21:04:15 -07:00
|
|
|
const boundElement = getHoveredElementForBinding(
|
|
|
|
pointerDownState.origin,
|
|
|
|
this.scene,
|
|
|
|
);
|
2020-07-30 14:50:59 +05:30
|
|
|
this.scene.replaceAllElements([
|
|
|
|
...this.scene.getElementsIncludingDeleted(),
|
2020-07-09 14:15:42 -07:00
|
|
|
element,
|
|
|
|
]);
|
|
|
|
this.setState({
|
|
|
|
draggingElement: element,
|
|
|
|
editingElement: element,
|
2020-08-08 21:04:15 -07:00
|
|
|
startBoundElement: boundElement,
|
|
|
|
suggestedBindings: [],
|
2020-07-09 14:15:42 -07:00
|
|
|
});
|
|
|
|
}
|
2020-07-08 22:07:51 -07:00
|
|
|
};
|
|
|
|
|
2020-07-09 14:15:42 -07:00
|
|
|
private createGenericElementOnPointerDown = (
|
|
|
|
elementType: ExcalidrawGenericElement["type"],
|
|
|
|
pointerDownState: PointerDownState,
|
|
|
|
): void => {
|
|
|
|
const [gridX, gridY] = getGridPoint(
|
|
|
|
pointerDownState.origin.x,
|
|
|
|
pointerDownState.origin.y,
|
|
|
|
this.state.gridSize,
|
|
|
|
);
|
|
|
|
const element = newElement({
|
|
|
|
type: elementType,
|
|
|
|
x: gridX,
|
|
|
|
y: gridY,
|
|
|
|
strokeColor: this.state.currentItemStrokeColor,
|
|
|
|
backgroundColor: this.state.currentItemBackgroundColor,
|
|
|
|
fillStyle: this.state.currentItemFillStyle,
|
|
|
|
strokeWidth: this.state.currentItemStrokeWidth,
|
|
|
|
strokeStyle: this.state.currentItemStrokeStyle,
|
|
|
|
roughness: this.state.currentItemRoughness,
|
|
|
|
opacity: this.state.currentItemOpacity,
|
2020-08-15 00:59:43 +09:00
|
|
|
strokeSharpness: this.state.currentItemStrokeSharpness,
|
2020-07-08 22:07:51 -07:00
|
|
|
});
|
|
|
|
|
2020-07-09 14:15:42 -07:00
|
|
|
if (element.type === "selection") {
|
|
|
|
this.setState({
|
|
|
|
selectionElement: element,
|
|
|
|
draggingElement: element,
|
|
|
|
});
|
|
|
|
} else {
|
2020-07-30 14:50:59 +05:30
|
|
|
this.scene.replaceAllElements([
|
|
|
|
...this.scene.getElementsIncludingDeleted(),
|
2020-07-09 14:15:42 -07:00
|
|
|
element,
|
|
|
|
]);
|
|
|
|
this.setState({
|
|
|
|
multiElement: null,
|
|
|
|
draggingElement: element,
|
|
|
|
editingElement: element,
|
|
|
|
});
|
2020-07-08 22:07:51 -07:00
|
|
|
}
|
2020-07-09 14:15:42 -07:00
|
|
|
};
|
2020-07-08 22:07:51 -07:00
|
|
|
|
2020-07-09 14:15:42 -07:00
|
|
|
private onPointerMoveFromPointerDownHandler(
|
2020-07-09 09:30:38 -07:00
|
|
|
pointerDownState: PointerDownState,
|
2020-07-09 14:15:42 -07:00
|
|
|
): (event: PointerEvent) => void {
|
|
|
|
return withBatchedUpdates((event: PointerEvent) => {
|
|
|
|
// We need to initialize dragOffsetXY only after we've updated
|
|
|
|
// `state.selectedElementIds` on pointerDown. Doing it here in pointerMove
|
|
|
|
// event handler should hopefully ensure we're already working with
|
|
|
|
// the updated state.
|
|
|
|
if (pointerDownState.drag.offset === null) {
|
|
|
|
pointerDownState.drag.offset = tupleToCoors(
|
|
|
|
getDragOffsetXY(
|
2020-07-30 14:50:59 +05:30
|
|
|
getSelectedElements(this.scene.getElements(), this.state),
|
2020-07-09 14:15:42 -07:00
|
|
|
pointerDownState.origin.x,
|
|
|
|
pointerDownState.origin.y,
|
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2020-07-08 22:07:51 -07:00
|
|
|
const target = event.target;
|
|
|
|
if (!(target instanceof HTMLElement)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2020-08-08 21:04:15 -07:00
|
|
|
if (this.handlePointerMoveOverScrollbars(event, pointerDownState)) {
|
2020-07-09 14:15:42 -07:00
|
|
|
return;
|
2020-07-08 22:07:51 -07:00
|
|
|
}
|
|
|
|
|
2020-08-08 21:04:15 -07:00
|
|
|
const pointerCoords = viewportCoordsToSceneCoords(
|
2020-07-09 14:15:42 -07:00
|
|
|
event,
|
|
|
|
this.state,
|
|
|
|
this.canvas,
|
|
|
|
window.devicePixelRatio,
|
|
|
|
);
|
2020-08-08 21:04:15 -07:00
|
|
|
const [gridX, gridY] = getGridPoint(
|
|
|
|
pointerCoords.x,
|
|
|
|
pointerCoords.y,
|
|
|
|
this.state.gridSize,
|
|
|
|
);
|
2020-07-08 22:07:51 -07:00
|
|
|
|
2020-07-09 14:15:42 -07:00
|
|
|
// for arrows/lines, don't start dragging until a given threshold
|
|
|
|
// to ensure we don't create a 2-point arrow by mistake when
|
|
|
|
// user clicks mouse in a way that it moves a tiny bit (thus
|
|
|
|
// triggering pointermove)
|
|
|
|
if (
|
|
|
|
!pointerDownState.drag.hasOccurred &&
|
|
|
|
(this.state.elementType === "arrow" ||
|
|
|
|
this.state.elementType === "line")
|
|
|
|
) {
|
|
|
|
if (
|
|
|
|
distance2d(
|
2020-08-08 21:04:15 -07:00
|
|
|
pointerCoords.x,
|
|
|
|
pointerCoords.y,
|
2020-07-09 14:15:42 -07:00
|
|
|
pointerDownState.origin.x,
|
|
|
|
pointerDownState.origin.y,
|
|
|
|
) < DRAGGING_THRESHOLD
|
|
|
|
) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (pointerDownState.resize.isResizing) {
|
|
|
|
const selectedElements = getSelectedElements(
|
2020-07-30 14:50:59 +05:30
|
|
|
this.scene.getElements(),
|
2020-07-09 14:15:42 -07:00
|
|
|
this.state,
|
|
|
|
);
|
2020-08-10 14:16:39 +02:00
|
|
|
const transformHandleType = pointerDownState.resize.handleType;
|
2020-07-09 14:15:42 -07:00
|
|
|
this.setState({
|
|
|
|
// TODO: rename this state field to "isScaling" to distinguish
|
|
|
|
// it from the generic "isResizing" which includes scaling and
|
|
|
|
// rotating
|
2020-08-10 14:16:39 +02:00
|
|
|
isResizing: transformHandleType && transformHandleType !== "rotation",
|
|
|
|
isRotating: transformHandleType === "rotation",
|
2020-07-09 14:15:42 -07:00
|
|
|
});
|
|
|
|
const [resizeX, resizeY] = getGridPoint(
|
2020-08-08 21:04:15 -07:00
|
|
|
pointerCoords.x - pointerDownState.resize.offset.x,
|
|
|
|
pointerCoords.y - pointerDownState.resize.offset.y,
|
2020-07-09 14:15:42 -07:00
|
|
|
this.state.gridSize,
|
|
|
|
);
|
|
|
|
if (
|
|
|
|
resizeElements(
|
2020-08-10 14:16:39 +02:00
|
|
|
transformHandleType,
|
|
|
|
(newTransformHandle) => {
|
|
|
|
pointerDownState.resize.handleType = newTransformHandle;
|
2020-07-09 14:15:42 -07:00
|
|
|
},
|
|
|
|
selectedElements,
|
|
|
|
pointerDownState.resize.arrowDirection,
|
|
|
|
getRotateWithDiscreteAngleKey(event),
|
|
|
|
getResizeWithSidesSameLengthKey(event),
|
|
|
|
getResizeCenterPointKey(event),
|
|
|
|
resizeX,
|
|
|
|
resizeY,
|
2020-07-26 19:21:38 +09:00
|
|
|
pointerDownState.resize.center.x,
|
|
|
|
pointerDownState.resize.center.y,
|
|
|
|
pointerDownState.resize.originalElements,
|
2020-07-09 14:15:42 -07:00
|
|
|
)
|
|
|
|
) {
|
2020-08-08 21:04:15 -07:00
|
|
|
this.maybeSuggestBindingForAll(selectedElements);
|
2020-07-09 14:15:42 -07:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (this.state.editingLinearElement) {
|
|
|
|
const didDrag = LinearElementEditor.handlePointDragging(
|
|
|
|
this.state,
|
|
|
|
(appState) => this.setState(appState),
|
2020-08-08 21:04:15 -07:00
|
|
|
pointerCoords.x,
|
|
|
|
pointerCoords.y,
|
|
|
|
(element, startOrEnd) => {
|
|
|
|
this.maybeSuggestBindingForLinearElementAtCursor(
|
|
|
|
element,
|
|
|
|
startOrEnd,
|
|
|
|
pointerCoords,
|
|
|
|
);
|
|
|
|
},
|
2020-07-09 14:15:42 -07:00
|
|
|
);
|
|
|
|
|
|
|
|
if (didDrag) {
|
2020-08-08 21:04:15 -07:00
|
|
|
pointerDownState.lastCoords.x = pointerCoords.x;
|
|
|
|
pointerDownState.lastCoords.y = pointerCoords.y;
|
2020-07-09 14:15:42 -07:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-08-26 17:37:44 +01:00
|
|
|
const hasHitASelectedElement = pointerDownState.hit.allHitElements.some(
|
|
|
|
(element) => this.isASelectedElement(element),
|
|
|
|
);
|
|
|
|
if (
|
|
|
|
hasHitASelectedElement ||
|
|
|
|
pointerDownState.hit.hasHitCommonBoundingBoxOfSelectedElements
|
|
|
|
) {
|
2020-07-09 14:15:42 -07:00
|
|
|
// Marking that click was used for dragging to check
|
|
|
|
// if elements should be deselected on pointerup
|
|
|
|
pointerDownState.drag.hasOccurred = true;
|
|
|
|
const selectedElements = getSelectedElements(
|
2020-07-30 14:50:59 +05:30
|
|
|
this.scene.getElements(),
|
2020-07-09 14:15:42 -07:00
|
|
|
this.state,
|
|
|
|
);
|
|
|
|
if (selectedElements.length > 0) {
|
|
|
|
const [dragX, dragY] = getGridPoint(
|
2020-08-08 21:04:15 -07:00
|
|
|
pointerCoords.x - pointerDownState.drag.offset.x,
|
|
|
|
pointerCoords.y - pointerDownState.drag.offset.y,
|
2020-07-09 14:15:42 -07:00
|
|
|
this.state.gridSize,
|
|
|
|
);
|
2020-08-08 21:04:15 -07:00
|
|
|
dragSelectedElements(selectedElements, dragX, dragY, this.scene);
|
|
|
|
this.maybeSuggestBindingForAll(selectedElements);
|
2020-07-09 14:15:42 -07:00
|
|
|
|
|
|
|
// We duplicate the selected element if alt is pressed on pointer move
|
|
|
|
if (event.altKey && !pointerDownState.hit.hasBeenDuplicated) {
|
|
|
|
// Move the currently selected elements to the top of the z index stack, and
|
|
|
|
// put the duplicates where the selected elements used to be.
|
|
|
|
// (the origin point where the dragging started)
|
|
|
|
|
|
|
|
pointerDownState.hit.hasBeenDuplicated = true;
|
|
|
|
|
|
|
|
const nextElements = [];
|
|
|
|
const elementsToAppend = [];
|
|
|
|
const groupIdMap = new Map();
|
2020-08-08 21:04:15 -07:00
|
|
|
const oldIdToDuplicatedId = new Map();
|
2020-08-26 17:37:44 +01:00
|
|
|
const hitElement = pointerDownState.hit.element;
|
2020-07-30 14:50:59 +05:30
|
|
|
for (const element of this.scene.getElementsIncludingDeleted()) {
|
2020-07-09 14:15:42 -07:00
|
|
|
if (
|
|
|
|
this.state.selectedElementIds[element.id] ||
|
|
|
|
// case: the state.selectedElementIds might not have been
|
|
|
|
// updated yet by the time this mousemove event is fired
|
2020-08-26 17:37:44 +01:00
|
|
|
(element.id === hitElement?.id &&
|
2020-07-09 14:15:42 -07:00
|
|
|
pointerDownState.hit.wasAddedToSelection)
|
|
|
|
) {
|
|
|
|
const duplicatedElement = duplicateElement(
|
|
|
|
this.state.editingGroupId,
|
|
|
|
groupIdMap,
|
|
|
|
element,
|
|
|
|
);
|
|
|
|
const [originDragX, originDragY] = getGridPoint(
|
|
|
|
pointerDownState.origin.x - pointerDownState.drag.offset.x,
|
|
|
|
pointerDownState.origin.y - pointerDownState.drag.offset.y,
|
|
|
|
this.state.gridSize,
|
|
|
|
);
|
|
|
|
mutateElement(duplicatedElement, {
|
|
|
|
x: duplicatedElement.x + (originDragX - dragX),
|
|
|
|
y: duplicatedElement.y + (originDragY - dragY),
|
|
|
|
});
|
|
|
|
nextElements.push(duplicatedElement);
|
|
|
|
elementsToAppend.push(element);
|
2020-08-08 21:04:15 -07:00
|
|
|
oldIdToDuplicatedId.set(element.id, duplicatedElement.id);
|
2020-07-09 14:15:42 -07:00
|
|
|
} else {
|
|
|
|
nextElements.push(element);
|
|
|
|
}
|
|
|
|
}
|
2020-08-08 21:04:15 -07:00
|
|
|
const nextSceneElements = [...nextElements, ...elementsToAppend];
|
|
|
|
fixBindingsAfterDuplication(
|
|
|
|
nextSceneElements,
|
|
|
|
elementsToAppend,
|
|
|
|
oldIdToDuplicatedId,
|
|
|
|
"duplicatesServeAsOld",
|
|
|
|
);
|
|
|
|
this.scene.replaceAllElements(nextSceneElements);
|
2020-07-09 14:15:42 -07:00
|
|
|
}
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// It is very important to read this.state within each move event,
|
|
|
|
// otherwise we would read a stale one!
|
|
|
|
const draggingElement = this.state.draggingElement;
|
|
|
|
if (!draggingElement) {
|
|
|
|
return;
|
|
|
|
}
|
2020-07-08 22:07:51 -07:00
|
|
|
|
2020-07-09 14:15:42 -07:00
|
|
|
if (isLinearElement(draggingElement)) {
|
|
|
|
pointerDownState.drag.hasOccurred = true;
|
|
|
|
const points = draggingElement.points;
|
|
|
|
let dx: number;
|
|
|
|
let dy: number;
|
|
|
|
if (draggingElement.type === "draw") {
|
2020-08-08 21:04:15 -07:00
|
|
|
dx = pointerCoords.x - draggingElement.x;
|
|
|
|
dy = pointerCoords.y - draggingElement.y;
|
2020-07-09 14:15:42 -07:00
|
|
|
} else {
|
|
|
|
dx = gridX - draggingElement.x;
|
|
|
|
dy = gridY - draggingElement.y;
|
|
|
|
}
|
2020-07-08 22:07:51 -07:00
|
|
|
|
2020-07-09 14:15:42 -07:00
|
|
|
if (getRotateWithDiscreteAngleKey(event) && points.length === 2) {
|
|
|
|
({ width: dx, height: dy } = getPerfectElementSize(
|
|
|
|
this.state.elementType,
|
|
|
|
dx,
|
|
|
|
dy,
|
|
|
|
));
|
|
|
|
}
|
2020-07-09 09:30:38 -07:00
|
|
|
|
2020-07-09 14:15:42 -07:00
|
|
|
if (points.length === 1) {
|
|
|
|
mutateElement(draggingElement, { points: [...points, [dx, dy]] });
|
|
|
|
} else if (points.length > 1) {
|
|
|
|
if (draggingElement.type === "draw") {
|
|
|
|
mutateElement(draggingElement, {
|
2020-08-08 21:50:16 -04:00
|
|
|
points: simplify(
|
|
|
|
[...(points as Point[]), [dx, dy]],
|
|
|
|
0.7 / this.state.zoom,
|
|
|
|
),
|
2020-07-09 14:15:42 -07:00
|
|
|
});
|
|
|
|
} else {
|
|
|
|
mutateElement(draggingElement, {
|
|
|
|
points: [...points.slice(0, -1), [dx, dy]],
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
2020-08-08 21:04:15 -07:00
|
|
|
if (isBindingElement(draggingElement)) {
|
|
|
|
// When creating a linear element by dragging
|
|
|
|
this.maybeSuggestBindingForLinearElementAtCursor(
|
|
|
|
draggingElement,
|
|
|
|
"end",
|
|
|
|
pointerCoords,
|
|
|
|
this.state.startBoundElement,
|
|
|
|
);
|
|
|
|
}
|
2020-07-09 14:15:42 -07:00
|
|
|
} else if (draggingElement.type === "selection") {
|
|
|
|
dragNewElement(
|
|
|
|
draggingElement,
|
|
|
|
this.state.elementType,
|
2020-07-09 09:30:38 -07:00
|
|
|
pointerDownState.origin.x,
|
|
|
|
pointerDownState.origin.y,
|
2020-08-08 21:04:15 -07:00
|
|
|
pointerCoords.x,
|
|
|
|
pointerCoords.y,
|
|
|
|
distance(pointerDownState.origin.x, pointerCoords.x),
|
|
|
|
distance(pointerDownState.origin.y, pointerCoords.y),
|
2020-07-09 14:15:42 -07:00
|
|
|
getResizeWithSidesSameLengthKey(event),
|
|
|
|
getResizeCenterPointKey(event),
|
2020-07-09 09:30:38 -07:00
|
|
|
);
|
2020-07-09 14:15:42 -07:00
|
|
|
} else {
|
|
|
|
dragNewElement(
|
|
|
|
draggingElement,
|
|
|
|
this.state.elementType,
|
|
|
|
pointerDownState.originInGrid.x,
|
|
|
|
pointerDownState.originInGrid.y,
|
|
|
|
gridX,
|
|
|
|
gridY,
|
|
|
|
distance(pointerDownState.originInGrid.x, gridX),
|
|
|
|
distance(pointerDownState.originInGrid.y, gridY),
|
|
|
|
getResizeWithSidesSameLengthKey(event),
|
|
|
|
getResizeCenterPointKey(event),
|
|
|
|
);
|
2020-08-08 21:04:15 -07:00
|
|
|
this.maybeSuggestBindingForAll([draggingElement]);
|
2020-07-09 14:15:42 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
if (this.state.elementType === "selection") {
|
2020-07-30 14:50:59 +05:30
|
|
|
const elements = this.scene.getElements();
|
2020-07-09 14:15:42 -07:00
|
|
|
if (!event.shiftKey && isSomeElementSelected(elements, this.state)) {
|
2020-07-09 09:30:38 -07:00
|
|
|
this.setState({
|
2020-07-09 14:15:42 -07:00
|
|
|
selectedElementIds: {},
|
|
|
|
selectedGroupIds: {},
|
|
|
|
editingGroupId: null,
|
2020-07-09 09:30:38 -07:00
|
|
|
});
|
|
|
|
}
|
2020-07-09 14:15:42 -07:00
|
|
|
const elementsWithinSelection = getElementsWithinSelection(
|
|
|
|
elements,
|
|
|
|
draggingElement,
|
2020-07-09 09:30:38 -07:00
|
|
|
);
|
2020-07-09 14:15:42 -07:00
|
|
|
this.setState((prevState) =>
|
|
|
|
selectGroupsForSelectedElements(
|
|
|
|
{
|
|
|
|
...prevState,
|
|
|
|
selectedElementIds: {
|
|
|
|
...prevState.selectedElementIds,
|
|
|
|
...elementsWithinSelection.reduce((map, element) => {
|
|
|
|
map[element.id] = true;
|
|
|
|
return map;
|
|
|
|
}, {} as any),
|
|
|
|
},
|
|
|
|
},
|
2020-07-30 14:50:59 +05:30
|
|
|
this.scene.getElements(),
|
2020-07-09 09:30:38 -07:00
|
|
|
),
|
|
|
|
);
|
2020-07-09 14:15:42 -07:00
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
2020-07-09 09:30:38 -07:00
|
|
|
|
2020-08-08 21:04:15 -07:00
|
|
|
// Returns whether the pointer move happened over either scrollbar
|
|
|
|
private handlePointerMoveOverScrollbars(
|
|
|
|
event: PointerEvent,
|
|
|
|
pointerDownState: PointerDownState,
|
|
|
|
): boolean {
|
|
|
|
if (pointerDownState.scrollbars.isOverHorizontal) {
|
|
|
|
const x = event.clientX;
|
|
|
|
const dx = x - pointerDownState.lastCoords.x;
|
|
|
|
this.setState({
|
|
|
|
scrollX: normalizeScroll(this.state.scrollX - dx / this.state.zoom),
|
|
|
|
});
|
|
|
|
pointerDownState.lastCoords.x = x;
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (pointerDownState.scrollbars.isOverVertical) {
|
|
|
|
const y = event.clientY;
|
|
|
|
const dy = y - pointerDownState.lastCoords.y;
|
|
|
|
this.setState({
|
|
|
|
scrollY: normalizeScroll(this.state.scrollY - dy / this.state.zoom),
|
|
|
|
});
|
|
|
|
pointerDownState.lastCoords.y = y;
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2020-07-09 14:15:42 -07:00
|
|
|
private onPointerUpFromPointerDownHandler(
|
|
|
|
pointerDownState: PointerDownState,
|
|
|
|
): (event: PointerEvent) => void {
|
|
|
|
return withBatchedUpdates((childEvent: PointerEvent) => {
|
|
|
|
const {
|
|
|
|
draggingElement,
|
|
|
|
resizingElement,
|
|
|
|
multiElement,
|
|
|
|
elementType,
|
|
|
|
elementLocked,
|
2020-08-08 21:04:15 -07:00
|
|
|
isResizing,
|
2020-07-09 14:15:42 -07:00
|
|
|
} = this.state;
|
2020-07-09 09:30:38 -07:00
|
|
|
|
2020-07-09 14:15:42 -07:00
|
|
|
this.setState({
|
|
|
|
isResizing: false,
|
|
|
|
isRotating: false,
|
|
|
|
resizingElement: null,
|
|
|
|
selectionElement: null,
|
|
|
|
cursorButton: "up",
|
|
|
|
// text elements are reset on finalize, and resetting on pointerup
|
|
|
|
// may cause issues with double taps
|
|
|
|
editingElement:
|
|
|
|
multiElement || isTextElement(this.state.editingElement)
|
|
|
|
? this.state.editingElement
|
|
|
|
: null,
|
|
|
|
});
|
|
|
|
|
|
|
|
this.savePointer(childEvent.clientX, childEvent.clientY, "up");
|
|
|
|
|
2020-08-08 21:04:15 -07:00
|
|
|
// Handle end of dragging a point of a linear element, might close a loop
|
|
|
|
// and sets binding element
|
2020-07-09 14:15:42 -07:00
|
|
|
if (this.state.editingLinearElement) {
|
|
|
|
const editingLinearElement = LinearElementEditor.handlePointerUp(
|
2020-08-08 21:04:15 -07:00
|
|
|
childEvent,
|
2020-07-09 14:15:42 -07:00
|
|
|
this.state.editingLinearElement,
|
2020-08-08 21:04:15 -07:00
|
|
|
this.state,
|
2020-07-09 09:30:38 -07:00
|
|
|
);
|
2020-07-09 14:15:42 -07:00
|
|
|
if (editingLinearElement !== this.state.editingLinearElement) {
|
2020-08-08 21:04:15 -07:00
|
|
|
this.setState({
|
|
|
|
editingLinearElement,
|
|
|
|
suggestedBindings: [],
|
|
|
|
});
|
2020-07-09 14:15:42 -07:00
|
|
|
}
|
|
|
|
}
|
2020-07-09 09:30:38 -07:00
|
|
|
|
2020-07-09 14:15:42 -07:00
|
|
|
lastPointerUp = null;
|
|
|
|
|
|
|
|
window.removeEventListener(
|
|
|
|
EVENT.POINTER_MOVE,
|
|
|
|
pointerDownState.eventListeners.onMove!,
|
|
|
|
);
|
|
|
|
window.removeEventListener(
|
|
|
|
EVENT.POINTER_UP,
|
|
|
|
pointerDownState.eventListeners.onUp!,
|
|
|
|
);
|
|
|
|
|
|
|
|
if (draggingElement?.type === "draw") {
|
|
|
|
this.actionManager.executeAction(actionFinalize);
|
|
|
|
return;
|
|
|
|
}
|
2020-08-26 17:37:44 +01:00
|
|
|
|
2020-07-09 14:15:42 -07:00
|
|
|
if (isLinearElement(draggingElement)) {
|
|
|
|
if (draggingElement!.points.length > 1) {
|
|
|
|
history.resumeRecording();
|
|
|
|
}
|
2020-08-08 21:04:15 -07:00
|
|
|
const pointerCoords = viewportCoordsToSceneCoords(
|
|
|
|
childEvent,
|
|
|
|
this.state,
|
|
|
|
this.canvas,
|
|
|
|
window.devicePixelRatio,
|
|
|
|
);
|
2020-08-26 17:37:44 +01:00
|
|
|
|
2020-07-09 14:15:42 -07:00
|
|
|
if (
|
|
|
|
!pointerDownState.drag.hasOccurred &&
|
|
|
|
draggingElement &&
|
|
|
|
!multiElement
|
|
|
|
) {
|
|
|
|
mutateElement(draggingElement, {
|
|
|
|
points: [
|
|
|
|
...draggingElement.points,
|
2020-08-08 21:04:15 -07:00
|
|
|
[
|
|
|
|
pointerCoords.x - draggingElement.x,
|
|
|
|
pointerCoords.y - draggingElement.y,
|
|
|
|
],
|
2020-07-09 14:15:42 -07:00
|
|
|
],
|
|
|
|
});
|
|
|
|
this.setState({
|
|
|
|
multiElement: draggingElement,
|
|
|
|
editingElement: this.state.draggingElement,
|
|
|
|
});
|
|
|
|
} else if (pointerDownState.drag.hasOccurred && !multiElement) {
|
2020-08-08 21:04:15 -07:00
|
|
|
if (
|
|
|
|
isBindingEnabled(this.state) &&
|
|
|
|
isBindingElement(draggingElement)
|
|
|
|
) {
|
|
|
|
maybeBindLinearElement(
|
|
|
|
draggingElement,
|
|
|
|
this.state,
|
|
|
|
this.scene,
|
|
|
|
pointerCoords,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
this.setState({ suggestedBindings: [], startBoundElement: null });
|
2020-07-09 14:15:42 -07:00
|
|
|
if (!elementLocked) {
|
|
|
|
resetCursor();
|
|
|
|
this.setState((prevState) => ({
|
|
|
|
draggingElement: null,
|
|
|
|
elementType: "selection",
|
|
|
|
selectedElementIds: {
|
|
|
|
...prevState.selectedElementIds,
|
|
|
|
[this.state.draggingElement!.id]: true,
|
|
|
|
},
|
|
|
|
}));
|
|
|
|
} else {
|
|
|
|
this.setState((prevState) => ({
|
|
|
|
draggingElement: null,
|
|
|
|
selectedElementIds: {
|
|
|
|
...prevState.selectedElementIds,
|
|
|
|
[this.state.draggingElement!.id]: true,
|
|
|
|
},
|
|
|
|
}));
|
2020-07-09 09:30:38 -07:00
|
|
|
}
|
|
|
|
}
|
2020-08-26 17:37:44 +01:00
|
|
|
|
2020-07-09 14:15:42 -07:00
|
|
|
return;
|
|
|
|
}
|
2020-07-09 09:30:38 -07:00
|
|
|
|
2020-07-09 14:15:42 -07:00
|
|
|
if (
|
|
|
|
elementType !== "selection" &&
|
|
|
|
draggingElement &&
|
|
|
|
isInvisiblySmallElement(draggingElement)
|
|
|
|
) {
|
|
|
|
// remove invisible element which was added in onPointerDown
|
2020-07-30 14:50:59 +05:30
|
|
|
this.scene.replaceAllElements(
|
|
|
|
this.scene.getElementsIncludingDeleted().slice(0, -1),
|
2020-07-09 14:15:42 -07:00
|
|
|
);
|
2020-07-09 09:30:38 -07:00
|
|
|
this.setState({
|
2020-07-09 14:15:42 -07:00
|
|
|
draggingElement: null,
|
2020-07-09 09:30:38 -07:00
|
|
|
});
|
2020-07-09 14:15:42 -07:00
|
|
|
return;
|
2020-07-09 09:30:38 -07:00
|
|
|
}
|
|
|
|
|
2020-07-09 14:15:42 -07:00
|
|
|
if (draggingElement) {
|
|
|
|
mutateElement(
|
|
|
|
draggingElement,
|
|
|
|
getNormalizedDimensions(draggingElement),
|
|
|
|
);
|
2020-08-08 21:04:15 -07:00
|
|
|
|
|
|
|
if (
|
|
|
|
isBindingEnabled(this.state) &&
|
|
|
|
isBindableElement(draggingElement)
|
|
|
|
) {
|
|
|
|
maybeBindBindableElement(draggingElement);
|
|
|
|
}
|
2020-07-09 14:15:42 -07:00
|
|
|
}
|
2020-07-09 09:30:38 -07:00
|
|
|
|
2020-07-09 14:15:42 -07:00
|
|
|
if (resizingElement) {
|
|
|
|
history.resumeRecording();
|
|
|
|
}
|
2020-07-09 09:30:38 -07:00
|
|
|
|
2020-07-09 14:15:42 -07:00
|
|
|
if (resizingElement && isInvisiblySmallElement(resizingElement)) {
|
2020-07-30 14:50:59 +05:30
|
|
|
this.scene.replaceAllElements(
|
|
|
|
this.scene
|
2020-07-09 14:15:42 -07:00
|
|
|
.getElementsIncludingDeleted()
|
|
|
|
.filter((el) => el.id !== resizingElement.id),
|
|
|
|
);
|
|
|
|
}
|
2020-07-09 09:30:38 -07:00
|
|
|
|
2020-08-26 17:37:44 +01:00
|
|
|
// Code below handles selection when element(s) weren't
|
|
|
|
// drag or added to selection on pointer down phase.
|
2020-07-09 14:15:42 -07:00
|
|
|
const hitElement = pointerDownState.hit.element;
|
|
|
|
if (
|
|
|
|
hitElement &&
|
|
|
|
!pointerDownState.drag.hasOccurred &&
|
|
|
|
!pointerDownState.hit.wasAddedToSelection
|
|
|
|
) {
|
|
|
|
if (childEvent.shiftKey) {
|
2020-08-26 17:37:44 +01:00
|
|
|
if (this.state.selectedElementIds[hitElement.id]) {
|
|
|
|
if (isSelectedViaGroup(this.state, hitElement)) {
|
|
|
|
// We want to unselect all groups hitElement is part of
|
|
|
|
// as well as all elements that are part of the groups
|
|
|
|
// hitElement is part of
|
|
|
|
const idsOfSelectedElementsThatAreInGroups = hitElement.groupIds
|
|
|
|
.flatMap((groupId) =>
|
|
|
|
getElementsInGroup(this.scene.getElements(), groupId),
|
|
|
|
)
|
|
|
|
.map((element) => ({ [element.id]: false }))
|
|
|
|
.reduce((prevId, acc) => ({ ...prevId, ...acc }), {});
|
|
|
|
|
|
|
|
this.setState((_prevState) => ({
|
|
|
|
selectedGroupIds: {
|
|
|
|
..._prevState.selectedElementIds,
|
|
|
|
...hitElement.groupIds
|
|
|
|
.map((gId) => ({ [gId]: false }))
|
|
|
|
.reduce((prev, acc) => ({ ...prev, ...acc }), {}),
|
|
|
|
},
|
|
|
|
selectedElementIds: {
|
|
|
|
..._prevState.selectedElementIds,
|
|
|
|
...idsOfSelectedElementsThatAreInGroups,
|
|
|
|
},
|
|
|
|
}));
|
|
|
|
} else {
|
|
|
|
// remove element from selection while
|
|
|
|
// keeping prev elements selected
|
|
|
|
this.setState((prevState) => ({
|
|
|
|
selectedElementIds: {
|
|
|
|
...prevState.selectedElementIds,
|
|
|
|
[hitElement!.id]: false,
|
|
|
|
},
|
|
|
|
}));
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// add element to selection while
|
|
|
|
// keeping prev elements selected
|
|
|
|
this.setState((_prevState) => ({
|
|
|
|
selectedElementIds: {
|
|
|
|
..._prevState.selectedElementIds,
|
|
|
|
[hitElement!.id]: true,
|
|
|
|
},
|
|
|
|
}));
|
|
|
|
}
|
2020-07-09 14:15:42 -07:00
|
|
|
} else {
|
2020-08-26 17:37:44 +01:00
|
|
|
if (isSelectedViaGroup(this.state, hitElement)) {
|
|
|
|
/*
|
|
|
|
We want to select the group(s) the hit element is in not the particular element.
|
|
|
|
That means we have to deselect elements that are not part of the groups of the
|
|
|
|
hit element, while keeping the elements that are.
|
|
|
|
*/
|
|
|
|
const idsOfSelectedElementsThatAreInGroups = hitElement.groupIds
|
|
|
|
.flatMap((groupId) =>
|
|
|
|
getElementsInGroup(this.scene.getElements(), groupId),
|
|
|
|
)
|
|
|
|
.map((element) => ({ [element.id]: true }))
|
|
|
|
.reduce((prevId, acc) => ({ ...prevId, ...acc }), {});
|
|
|
|
|
|
|
|
this.setState((_prevState) => ({
|
|
|
|
selectedGroupIds: {
|
|
|
|
...hitElement.groupIds
|
|
|
|
.map((gId) => ({ [gId]: true }))
|
|
|
|
.reduce((prevId, acc) => ({ ...prevId, ...acc }), {}),
|
|
|
|
},
|
|
|
|
selectedElementIds: { ...idsOfSelectedElementsThatAreInGroups },
|
|
|
|
}));
|
|
|
|
} else {
|
|
|
|
this.setState((_prevState) => ({
|
|
|
|
selectedGroupIds: {},
|
|
|
|
selectedElementIds: { [hitElement!.id]: true },
|
|
|
|
}));
|
|
|
|
}
|
2020-07-09 14:15:42 -07:00
|
|
|
}
|
|
|
|
}
|
2020-07-09 09:30:38 -07:00
|
|
|
|
2020-08-26 17:37:44 +01:00
|
|
|
if (
|
|
|
|
!this.state.editingLinearElement &&
|
|
|
|
!pointerDownState.drag.hasOccurred &&
|
|
|
|
!this.state.isResizing &&
|
|
|
|
((hitElement &&
|
|
|
|
isHittingElementBoundingBoxWithoutHittingElement(
|
|
|
|
hitElement,
|
|
|
|
this.state,
|
|
|
|
pointerDownState.origin.x,
|
|
|
|
pointerDownState.origin.y,
|
|
|
|
)) ||
|
|
|
|
(!hitElement &&
|
|
|
|
pointerDownState.hit.hasHitCommonBoundingBoxOfSelectedElements))
|
|
|
|
) {
|
|
|
|
// Deselect selected elements
|
|
|
|
this.setState({
|
|
|
|
selectedElementIds: {},
|
|
|
|
selectedGroupIds: {},
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2020-07-09 14:15:42 -07:00
|
|
|
if (draggingElement === null) {
|
|
|
|
// if no element is clicked, clear the selection and redraw
|
|
|
|
this.setState({
|
|
|
|
selectedElementIds: {},
|
|
|
|
selectedGroupIds: {},
|
|
|
|
editingGroupId: null,
|
2020-07-09 09:30:38 -07:00
|
|
|
});
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2020-07-09 14:15:42 -07:00
|
|
|
if (!elementLocked) {
|
|
|
|
this.setState((prevState) => ({
|
|
|
|
selectedElementIds: {
|
|
|
|
...prevState.selectedElementIds,
|
|
|
|
[draggingElement.id]: true,
|
|
|
|
},
|
|
|
|
}));
|
|
|
|
}
|
2020-07-09 09:30:38 -07:00
|
|
|
|
|
|
|
if (
|
2020-07-09 14:15:42 -07:00
|
|
|
elementType !== "selection" ||
|
2020-07-30 14:50:59 +05:30
|
|
|
isSomeElementSelected(this.scene.getElements(), this.state)
|
2020-07-09 09:30:38 -07:00
|
|
|
) {
|
2020-07-09 14:15:42 -07:00
|
|
|
history.resumeRecording();
|
2020-07-09 09:30:38 -07:00
|
|
|
}
|
|
|
|
|
2020-08-08 21:04:15 -07:00
|
|
|
if (pointerDownState.drag.hasOccurred || isResizing) {
|
|
|
|
(isBindingEnabled(this.state)
|
|
|
|
? bindOrUnbindSelectedElements
|
|
|
|
: unbindLinearElements)(
|
|
|
|
getSelectedElements(this.scene.getElements(), this.state),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2020-07-09 14:15:42 -07:00
|
|
|
if (!elementLocked) {
|
|
|
|
resetCursor();
|
|
|
|
this.setState({
|
|
|
|
draggingElement: null,
|
2020-08-08 21:04:15 -07:00
|
|
|
suggestedBindings: [],
|
2020-07-09 14:15:42 -07:00
|
|
|
elementType: "selection",
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
this.setState({
|
|
|
|
draggingElement: null,
|
2020-08-08 21:04:15 -07:00
|
|
|
suggestedBindings: [],
|
2020-07-09 14:15:42 -07:00
|
|
|
});
|
|
|
|
}
|
2020-07-09 09:30:38 -07:00
|
|
|
});
|
2020-07-09 14:15:42 -07:00
|
|
|
}
|
2020-07-09 09:30:38 -07:00
|
|
|
|
2020-08-08 21:04:15 -07:00
|
|
|
private maybeSuggestBindingAtCursor = (pointerCoords: {
|
|
|
|
x: number;
|
|
|
|
y: number;
|
|
|
|
}): void => {
|
|
|
|
const hoveredBindableElement = getHoveredElementForBinding(
|
|
|
|
pointerCoords,
|
|
|
|
this.scene,
|
|
|
|
);
|
|
|
|
this.setState({
|
|
|
|
suggestedBindings:
|
|
|
|
hoveredBindableElement != null ? [hoveredBindableElement] : [],
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
private maybeSuggestBindingForLinearElementAtCursor = (
|
|
|
|
linearElement: NonDeleted<ExcalidrawLinearElement>,
|
|
|
|
startOrEnd: "start" | "end",
|
|
|
|
pointerCoords: {
|
|
|
|
x: number;
|
|
|
|
y: number;
|
|
|
|
},
|
|
|
|
// During line creation the start binding hasn't been written yet
|
|
|
|
// into `linearElement`
|
|
|
|
oppositeBindingBoundElement?: ExcalidrawBindableElement | null,
|
|
|
|
): void => {
|
|
|
|
const hoveredBindableElement = getElligibleElementForBindingElementAtCoors(
|
|
|
|
linearElement,
|
|
|
|
startOrEnd,
|
|
|
|
pointerCoords,
|
|
|
|
);
|
|
|
|
this.setState({
|
|
|
|
suggestedBindings:
|
|
|
|
hoveredBindableElement != null &&
|
|
|
|
!isLinearElementSimpleAndAlreadyBound(
|
|
|
|
linearElement,
|
|
|
|
oppositeBindingBoundElement?.id,
|
|
|
|
hoveredBindableElement,
|
|
|
|
)
|
|
|
|
? [hoveredBindableElement]
|
|
|
|
: [],
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
private maybeSuggestBindingForAll(
|
|
|
|
selectedElements: NonDeleted<ExcalidrawElement>[],
|
|
|
|
): void {
|
|
|
|
const suggestedBindings = getEligibleElementsForBinding(selectedElements);
|
|
|
|
this.setState({ suggestedBindings });
|
|
|
|
}
|
|
|
|
|
2020-08-26 17:37:44 +01:00
|
|
|
private clearSelection(hitElement: ExcalidrawElement | null): void {
|
2020-07-08 22:07:51 -07:00
|
|
|
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,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2020-04-04 14:55:36 +02:00
|
|
|
private handleCanvasRef = (canvas: HTMLCanvasElement) => {
|
|
|
|
// canvas is null when unmounting
|
|
|
|
if (canvas !== null) {
|
|
|
|
this.canvas = canvas;
|
|
|
|
this.rc = rough.canvas(this.canvas);
|
|
|
|
|
2020-04-12 06:12:02 +05:30
|
|
|
this.canvas.addEventListener(EVENT.WHEEL, this.handleWheel, {
|
2020-04-04 14:55:36 +02:00
|
|
|
passive: false,
|
|
|
|
});
|
2020-04-12 06:12:02 +05:30
|
|
|
this.canvas.addEventListener(EVENT.TOUCH_START, this.onTapStart);
|
2020-06-02 18:41:40 +02:00
|
|
|
this.canvas.addEventListener(EVENT.TOUCH_END, this.onTapEnd);
|
2020-04-04 14:55:36 +02:00
|
|
|
} else {
|
2020-04-12 06:12:02 +05:30
|
|
|
this.canvas?.removeEventListener(EVENT.WHEEL, this.handleWheel);
|
|
|
|
this.canvas?.removeEventListener(EVENT.TOUCH_START, this.onTapStart);
|
2020-06-02 18:41:40 +02:00
|
|
|
this.canvas?.removeEventListener(EVENT.TOUCH_END, this.onTapEnd);
|
2020-04-04 14:55:36 +02:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2020-04-04 15:27:53 +02:00
|
|
|
private handleCanvasOnDrop = (event: React.DragEvent<HTMLCanvasElement>) => {
|
2020-07-10 02:20:23 -07:00
|
|
|
const libraryShapes = event.dataTransfer.getData(
|
2020-07-27 15:29:19 +03:00
|
|
|
"application/vnd.excalidrawlib+json",
|
2020-07-10 02:20:23 -07:00
|
|
|
);
|
|
|
|
if (libraryShapes !== "") {
|
|
|
|
this.addElementsFromPasteOrLibrary(
|
|
|
|
JSON.parse(libraryShapes),
|
|
|
|
event.clientX,
|
|
|
|
event.clientY,
|
|
|
|
);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2020-04-04 15:27:53 +02:00
|
|
|
const file = event.dataTransfer?.files[0];
|
|
|
|
if (
|
|
|
|
file?.type === "application/json" ||
|
|
|
|
file?.name.endsWith(".excalidraw")
|
|
|
|
) {
|
|
|
|
this.setState({ isLoading: true });
|
2020-07-27 17:18:49 +05:30
|
|
|
loadFromBlob(file, this.state)
|
2020-04-04 15:27:53 +02:00
|
|
|
.then(({ elements, appState }) =>
|
|
|
|
this.syncActionResult({
|
|
|
|
elements,
|
|
|
|
appState: {
|
|
|
|
...(appState || this.state),
|
|
|
|
isLoading: false,
|
|
|
|
},
|
|
|
|
commitToHistory: false,
|
|
|
|
}),
|
|
|
|
)
|
|
|
|
.catch((error) => {
|
2020-04-10 10:58:09 +01:00
|
|
|
this.setState({ isLoading: false, errorMessage: error.message });
|
2020-04-04 15:27:53 +02:00
|
|
|
});
|
2020-07-27 15:29:19 +03:00
|
|
|
} else if (
|
|
|
|
file?.type === "application/vnd.excalidrawlib+json" ||
|
|
|
|
file?.name.endsWith(".excalidrawlib")
|
|
|
|
) {
|
|
|
|
Library.importLibrary(file)
|
|
|
|
.then(() => {
|
|
|
|
this.setState({ isLibraryOpen: false });
|
|
|
|
})
|
|
|
|
.catch((error) =>
|
|
|
|
this.setState({ isLoading: false, errorMessage: error.message }),
|
|
|
|
);
|
2020-04-04 15:27:53 +02:00
|
|
|
} else {
|
|
|
|
this.setState({
|
|
|
|
isLoading: false,
|
|
|
|
errorMessage: t("alerts.couldNotLoadInvalidFile"),
|
|
|
|
});
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2020-03-21 09:03:17 -07:00
|
|
|
private handleCanvasContextMenu = (
|
|
|
|
event: React.PointerEvent<HTMLCanvasElement>,
|
|
|
|
) => {
|
|
|
|
event.preventDefault();
|
2020-07-02 22:12:56 +01:00
|
|
|
this.openContextMenu(event);
|
|
|
|
};
|
2020-03-21 09:03:17 -07:00
|
|
|
|
2020-07-02 22:12:56 +01:00
|
|
|
private openContextMenu = ({
|
|
|
|
clientX,
|
|
|
|
clientY,
|
|
|
|
}: {
|
|
|
|
clientX: number;
|
|
|
|
clientY: number;
|
|
|
|
}) => {
|
2020-03-21 09:03:17 -07:00
|
|
|
const { x, y } = viewportCoordsToSceneCoords(
|
2020-07-02 22:12:56 +01:00
|
|
|
{ clientX, clientY },
|
2020-03-21 09:03:17 -07:00
|
|
|
this.state,
|
|
|
|
this.canvas,
|
|
|
|
window.devicePixelRatio,
|
|
|
|
);
|
|
|
|
|
2020-07-30 14:50:59 +05:30
|
|
|
const elements = this.scene.getElements();
|
2020-08-08 21:04:15 -07:00
|
|
|
const element = this.getElementAtPosition(x, y);
|
2020-03-21 09:03:17 -07:00
|
|
|
if (!element) {
|
|
|
|
ContextMenu.push({
|
|
|
|
options: [
|
|
|
|
navigator.clipboard && {
|
|
|
|
label: t("labels.paste"),
|
|
|
|
action: () => this.pasteFromClipboard(null),
|
|
|
|
},
|
|
|
|
probablySupportsClipboardBlob &&
|
2020-04-08 09:49:52 -07:00
|
|
|
elements.length > 0 && {
|
2020-03-21 09:03:17 -07:00
|
|
|
label: t("labels.copyAsPng"),
|
|
|
|
action: this.copyToClipboardAsPng,
|
|
|
|
},
|
2020-04-05 16:13:17 -07:00
|
|
|
probablySupportsClipboardWriteText &&
|
2020-04-08 09:49:52 -07:00
|
|
|
elements.length > 0 && {
|
2020-04-05 16:13:17 -07:00
|
|
|
label: t("labels.copyAsSvg"),
|
|
|
|
action: this.copyToClipboardAsSvg,
|
|
|
|
},
|
2020-03-23 13:05:07 +02:00
|
|
|
...this.actionManager.getContextMenuItems((action) =>
|
2020-05-30 18:56:17 +05:30
|
|
|
CANVAS_ONLY_ACTIONS.includes(action.name),
|
2020-03-21 09:03:17 -07:00
|
|
|
),
|
2020-06-24 00:24:52 +09:00
|
|
|
{
|
|
|
|
label: t("labels.toggleGridMode"),
|
|
|
|
action: this.toggleGridMode,
|
|
|
|
},
|
2020-03-21 09:03:17 -07:00
|
|
|
],
|
2020-07-02 22:12:56 +01:00
|
|
|
top: clientY,
|
|
|
|
left: clientX,
|
2020-03-21 09:03:17 -07:00
|
|
|
});
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!this.state.selectedElementIds[element.id]) {
|
|
|
|
this.setState({ selectedElementIds: { [element.id]: true } });
|
|
|
|
}
|
|
|
|
|
|
|
|
ContextMenu.push({
|
|
|
|
options: [
|
|
|
|
navigator.clipboard && {
|
|
|
|
label: t("labels.copy"),
|
2020-03-27 15:18:14 -07:00
|
|
|
action: this.copyAll,
|
2020-03-21 09:03:17 -07:00
|
|
|
},
|
|
|
|
navigator.clipboard && {
|
|
|
|
label: t("labels.paste"),
|
|
|
|
action: () => this.pasteFromClipboard(null),
|
|
|
|
},
|
|
|
|
probablySupportsClipboardBlob && {
|
|
|
|
label: t("labels.copyAsPng"),
|
|
|
|
action: this.copyToClipboardAsPng,
|
|
|
|
},
|
2020-04-05 16:13:17 -07:00
|
|
|
probablySupportsClipboardWriteText && {
|
|
|
|
label: t("labels.copyAsSvg"),
|
|
|
|
action: this.copyToClipboardAsSvg,
|
|
|
|
},
|
2020-03-21 09:03:17 -07:00
|
|
|
...this.actionManager.getContextMenuItems(
|
2020-05-30 18:56:17 +05:30
|
|
|
(action) => !CANVAS_ONLY_ACTIONS.includes(action.name),
|
2020-03-21 09:03:17 -07:00
|
|
|
),
|
|
|
|
],
|
2020-07-02 22:12:56 +01:00
|
|
|
top: clientY,
|
|
|
|
left: clientX,
|
2020-03-21 09:03:17 -07:00
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2020-03-16 19:07:47 -07:00
|
|
|
private handleWheel = withBatchedUpdates((event: WheelEvent) => {
|
2020-03-07 10:20:38 -05:00
|
|
|
event.preventDefault();
|
|
|
|
const { deltaX, deltaY } = event;
|
2020-06-02 18:41:40 +02:00
|
|
|
const { selectedElementIds, previousSelectedElementIds } = this.state;
|
2020-03-07 10:20:38 -05:00
|
|
|
|
2020-03-09 15:19:38 +01:00
|
|
|
// note that event.ctrlKey is necessary to handle pinch zooming
|
|
|
|
if (event.metaKey || event.ctrlKey) {
|
2020-03-07 10:20:38 -05:00
|
|
|
const sign = Math.sign(deltaY);
|
|
|
|
const MAX_STEP = 10;
|
|
|
|
let delta = Math.abs(deltaY);
|
|
|
|
if (delta > MAX_STEP) {
|
|
|
|
delta = MAX_STEP;
|
|
|
|
}
|
|
|
|
delta *= sign;
|
2020-06-02 18:41:40 +02:00
|
|
|
if (Object.keys(previousSelectedElementIds).length !== 0) {
|
|
|
|
setTimeout(() => {
|
|
|
|
this.setState({
|
|
|
|
selectedElementIds: previousSelectedElementIds,
|
|
|
|
previousSelectedElementIds: {},
|
|
|
|
});
|
|
|
|
}, 1000);
|
|
|
|
}
|
2020-03-07 10:20:38 -05:00
|
|
|
this.setState(({ zoom }) => ({
|
|
|
|
zoom: getNormalizedZoom(zoom - delta / 100),
|
2020-06-02 18:41:40 +02:00
|
|
|
selectedElementIds: {},
|
|
|
|
previousSelectedElementIds:
|
|
|
|
Object.keys(selectedElementIds).length !== 0
|
|
|
|
? selectedElementIds
|
|
|
|
: previousSelectedElementIds,
|
2020-03-07 10:20:38 -05:00
|
|
|
}));
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2020-04-30 22:34:38 +02:00
|
|
|
// scroll horizontally when shift pressed
|
|
|
|
if (event.shiftKey) {
|
|
|
|
this.setState(({ zoom, scrollX }) => ({
|
2020-05-02 22:15:28 +02:00
|
|
|
// on Mac, shift+wheel tends to result in deltaX
|
|
|
|
scrollX: normalizeScroll(scrollX - (deltaY || deltaX) / zoom),
|
2020-04-30 22:34:38 +02:00
|
|
|
}));
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2020-03-07 10:20:38 -05:00
|
|
|
this.setState(({ zoom, scrollX, scrollY }) => ({
|
|
|
|
scrollX: normalizeScroll(scrollX - deltaX / zoom),
|
|
|
|
scrollY: normalizeScroll(scrollY - deltaY / zoom),
|
|
|
|
}));
|
2020-03-16 19:07:47 -07:00
|
|
|
});
|
2020-03-07 10:20:38 -05:00
|
|
|
|
2020-03-28 14:30:41 -07:00
|
|
|
private getTextWysiwygSnappedToCenterPosition(
|
|
|
|
x: number,
|
|
|
|
y: number,
|
2020-07-27 17:18:49 +05:30
|
|
|
appState: AppState,
|
2020-03-28 14:30:41 -07:00
|
|
|
canvas: HTMLCanvasElement | null,
|
|
|
|
scale: number,
|
|
|
|
) {
|
2020-03-14 21:48:51 -07:00
|
|
|
const elementClickedInside = getElementContainingPosition(
|
2020-07-30 14:50:59 +05:30
|
|
|
this.scene
|
2020-06-25 21:21:27 +02:00
|
|
|
.getElementsIncludingDeleted()
|
|
|
|
.filter((element) => !isTextElement(element)),
|
2020-03-14 21:48:51 -07:00
|
|
|
x,
|
|
|
|
y,
|
|
|
|
);
|
2020-03-07 10:20:38 -05:00
|
|
|
if (elementClickedInside) {
|
|
|
|
const elementCenterX =
|
|
|
|
elementClickedInside.x + elementClickedInside.width / 2;
|
|
|
|
const elementCenterY =
|
|
|
|
elementClickedInside.y + elementClickedInside.height / 2;
|
|
|
|
const distanceToCenter = Math.hypot(
|
|
|
|
x - elementCenterX,
|
|
|
|
y - elementCenterY,
|
|
|
|
);
|
|
|
|
const isSnappedToCenter =
|
|
|
|
distanceToCenter < TEXT_TO_CENTER_SNAP_THRESHOLD;
|
|
|
|
if (isSnappedToCenter) {
|
2020-06-25 21:21:27 +02:00
|
|
|
const { x: viewportX, y: viewportY } = sceneCoordsToViewportCoords(
|
2020-03-28 14:30:41 -07:00
|
|
|
{ sceneX: elementCenterX, sceneY: elementCenterY },
|
2020-07-27 17:18:49 +05:30
|
|
|
appState,
|
2020-03-28 14:30:41 -07:00
|
|
|
canvas,
|
|
|
|
scale,
|
|
|
|
);
|
2020-06-25 21:21:27 +02:00
|
|
|
return { viewportX, viewportY, elementCenterX, elementCenterY };
|
2020-03-07 10:20:38 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-04-04 16:12:19 +01:00
|
|
|
private savePointer = (x: number, y: number, button: "up" | "down") => {
|
|
|
|
if (!x || !y) {
|
|
|
|
return;
|
|
|
|
}
|
2020-08-14 20:14:22 +02:00
|
|
|
const pointer = viewportCoordsToSceneCoords(
|
2020-04-04 16:12:19 +01:00
|
|
|
{ clientX: x, clientY: y },
|
|
|
|
this.state,
|
|
|
|
this.canvas,
|
|
|
|
window.devicePixelRatio,
|
|
|
|
);
|
|
|
|
|
2020-08-14 20:14:22 +02:00
|
|
|
if (isNaN(pointer.x) || isNaN(pointer.y)) {
|
basic Socket.io implementation of collaborative editing (#879)
* Enable collaborative syncing for elements
* Don't fall back to local storage if using a room, as that is confusing
* Use remote socket server
* Send updates to new users when they join
* ~
* add mouse tracking
* enable collaboration, rooms, and mouse tracking
* fix syncing bugs and add a button to start syncing mid session
* enable collaboration, rooms, and mouse tracking
* fix syncing bugs and add a button to start syncing mid session
* Add Live button and app state to support tracking collaborator counts
* Enable collaborative syncing for elements
* add mouse tracking
* enable collaboration, rooms, and mouse tracking
* fix syncing bugs and add a button to start syncing mid session
* fix syncing bugs and add a button to start syncing mid session
* Add Live button and app state to support tracking collaborator counts
* prettier
* Fix bug with remote pointers not changing on scroll
* Enable collaborative syncing for elements
* add mouse tracking
* enable collaboration, rooms, and mouse tracking
* fix syncing bugs and add a button to start syncing mid session
* enable collaboration, rooms, and mouse tracking
* fix syncing bugs and add a button to start syncing mid session
* Add Live button and app state to support tracking collaborator counts
* enable collaboration, rooms, and mouse tracking
* fix syncing bugs and add a button to start syncing mid session
* fix syncing bugs and add a button to start syncing mid session
* Fix bug with remote pointers not changing on scroll
* remove UI for collaboration
* remove link
* clean up lingering unused UI
* set random IV passed per encrypted message, reduce room id length, refactored socket broadcasting API, rename room_id to room, removed throttling of pointer movement
* fix package.json conflict
2020-03-09 08:48:25 -07:00
|
|
|
// sometimes the pointer goes off screen
|
|
|
|
return;
|
|
|
|
}
|
2020-04-08 10:18:56 -07:00
|
|
|
this.portal.socket &&
|
2020-04-11 15:26:27 -07:00
|
|
|
// do not broadcast when more than 1 pointer since that shows flickering on the other side
|
|
|
|
gesture.pointers.size < 2 &&
|
2020-04-04 16:12:19 +01:00
|
|
|
this.broadcastMouseLocation({
|
2020-08-14 20:14:22 +02:00
|
|
|
pointer,
|
2020-04-04 16:12:19 +01:00
|
|
|
button,
|
|
|
|
});
|
basic Socket.io implementation of collaborative editing (#879)
* Enable collaborative syncing for elements
* Don't fall back to local storage if using a room, as that is confusing
* Use remote socket server
* Send updates to new users when they join
* ~
* add mouse tracking
* enable collaboration, rooms, and mouse tracking
* fix syncing bugs and add a button to start syncing mid session
* enable collaboration, rooms, and mouse tracking
* fix syncing bugs and add a button to start syncing mid session
* Add Live button and app state to support tracking collaborator counts
* Enable collaborative syncing for elements
* add mouse tracking
* enable collaboration, rooms, and mouse tracking
* fix syncing bugs and add a button to start syncing mid session
* fix syncing bugs and add a button to start syncing mid session
* Add Live button and app state to support tracking collaborator counts
* prettier
* Fix bug with remote pointers not changing on scroll
* Enable collaborative syncing for elements
* add mouse tracking
* enable collaboration, rooms, and mouse tracking
* fix syncing bugs and add a button to start syncing mid session
* enable collaboration, rooms, and mouse tracking
* fix syncing bugs and add a button to start syncing mid session
* Add Live button and app state to support tracking collaborator counts
* enable collaboration, rooms, and mouse tracking
* fix syncing bugs and add a button to start syncing mid session
* fix syncing bugs and add a button to start syncing mid session
* Fix bug with remote pointers not changing on scroll
* remove UI for collaboration
* remove link
* clean up lingering unused UI
* set random IV passed per encrypted message, reduce room id length, refactored socket broadcasting API, rename room_id to room, removed throttling of pointer movement
* fix package.json conflict
2020-03-09 08:48:25 -07:00
|
|
|
};
|
|
|
|
|
2020-03-28 16:59:36 -07:00
|
|
|
private resetShouldCacheIgnoreZoomDebounced = debounce(() => {
|
|
|
|
this.setState({ shouldCacheIgnoreZoom: false });
|
2020-04-02 23:56:14 -07:00
|
|
|
}, 300);
|
2020-03-28 16:59:36 -07:00
|
|
|
|
2020-07-27 17:18:49 +05:30
|
|
|
private getCanvasOffsets() {
|
|
|
|
if (this.excalidrawRef?.current) {
|
|
|
|
const parentElement = this.excalidrawRef.current.parentElement;
|
|
|
|
const { left, top } = parentElement.getBoundingClientRect();
|
|
|
|
return {
|
|
|
|
offsetLeft: left,
|
|
|
|
offsetTop: top,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
return {
|
|
|
|
offsetLeft: 0,
|
|
|
|
offsetTop: 0,
|
|
|
|
};
|
|
|
|
}
|
2020-03-07 10:20:38 -05:00
|
|
|
}
|
2020-03-18 20:44:05 +01:00
|
|
|
|
|
|
|
// -----------------------------------------------------------------------------
|
|
|
|
// TEST HOOKS
|
|
|
|
// -----------------------------------------------------------------------------
|
|
|
|
|
|
|
|
declare global {
|
|
|
|
interface Window {
|
|
|
|
h: {
|
|
|
|
elements: readonly ExcalidrawElement[];
|
|
|
|
state: AppState;
|
2020-03-26 08:28:50 +01:00
|
|
|
setState: React.Component<any, AppState>["setState"];
|
2020-03-18 20:44:05 +01:00
|
|
|
history: SceneHistory;
|
2020-03-26 08:28:50 +01:00
|
|
|
app: InstanceType<typeof App>;
|
2020-07-27 15:29:19 +03:00
|
|
|
library: ReturnType<typeof loadLibrary>;
|
2020-03-18 20:44:05 +01:00
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-04-12 06:12:02 +05:30
|
|
|
if (
|
|
|
|
process.env.NODE_ENV === ENV.TEST ||
|
|
|
|
process.env.NODE_ENV === ENV.DEVELOPMENT
|
|
|
|
) {
|
2020-03-18 20:44:05 +01:00
|
|
|
window.h = {} as Window["h"];
|
|
|
|
|
|
|
|
Object.defineProperties(window.h, {
|
|
|
|
elements: {
|
|
|
|
get() {
|
2020-07-30 14:50:59 +05:30
|
|
|
return this.app.scene.getElementsIncludingDeleted();
|
2020-03-18 20:44:05 +01:00
|
|
|
},
|
|
|
|
set(elements: ExcalidrawElement[]) {
|
2020-07-30 14:50:59 +05:30
|
|
|
return this.app.scene.replaceAllElements(elements);
|
2020-03-18 20:44:05 +01:00
|
|
|
},
|
|
|
|
},
|
|
|
|
history: {
|
2020-05-20 16:21:37 +03:00
|
|
|
get: () => history,
|
2020-03-18 20:44:05 +01:00
|
|
|
},
|
2020-07-27 15:29:19 +03:00
|
|
|
library: {
|
|
|
|
get: () => loadLibrary(),
|
|
|
|
},
|
2020-03-18 20:44:05 +01:00
|
|
|
});
|
|
|
|
}
|
2020-04-12 16:24:52 +05:30
|
|
|
export default App;
|