2021-04-08 19:54:50 +02:00
|
|
|
import React, { useContext } from "react";
|
2020-03-07 10:20:38 -05:00
|
|
|
import { RoughCanvas } from "roughjs/bin/canvas";
|
2020-12-27 18:26:30 +02:00
|
|
|
import rough from "roughjs/bin/rough";
|
2021-02-02 02:26:42 +05:30
|
|
|
import clsx from "clsx";
|
2021-04-21 23:38:24 +05:30
|
|
|
import { nanoid } from "nanoid";
|
2021-02-02 02:26:42 +05:30
|
|
|
|
2021-01-28 00:41:17 +05:30
|
|
|
import {
|
|
|
|
actionAddToLibrary,
|
|
|
|
actionBringForward,
|
|
|
|
actionBringToFront,
|
|
|
|
actionCopy,
|
|
|
|
actionCopyAsPng,
|
|
|
|
actionCopyAsSvg,
|
2022-04-05 15:31:19 +02:00
|
|
|
copyAllTextNodesAsText,
|
2021-01-28 00:41:17 +05:30
|
|
|
actionCopyStyles,
|
|
|
|
actionCut,
|
|
|
|
actionDeleteSelected,
|
|
|
|
actionDuplicateSelection,
|
|
|
|
actionFinalize,
|
2021-03-26 11:45:08 -04:00
|
|
|
actionFlipHorizontal,
|
|
|
|
actionFlipVertical,
|
2021-01-28 00:41:17 +05:30
|
|
|
actionGroup,
|
|
|
|
actionPasteStyles,
|
|
|
|
actionSelectAll,
|
|
|
|
actionSendBackward,
|
|
|
|
actionSendToBack,
|
|
|
|
actionToggleGridMode,
|
|
|
|
actionToggleStats,
|
|
|
|
actionToggleZenMode,
|
2022-02-01 20:11:24 +05:30
|
|
|
actionUnbindText,
|
2022-03-21 17:54:54 +05:30
|
|
|
actionBindText,
|
2021-01-28 00:41:17 +05:30
|
|
|
actionUngroup,
|
2022-02-03 20:34:59 +05:30
|
|
|
actionLink,
|
2021-01-28 00:41:17 +05:30
|
|
|
} from "../actions";
|
2020-12-27 18:26:30 +02:00
|
|
|
import { createRedoAction, createUndoAction } from "../actions/actionHistory";
|
|
|
|
import { ActionManager } from "../actions/manager";
|
2020-03-07 10:20:38 -05:00
|
|
|
import { actions } from "../actions/register";
|
|
|
|
import { ActionResult } from "../actions/types";
|
2021-01-05 20:06:14 +02:00
|
|
|
import { trackEvent } from "../analytics";
|
2022-03-11 19:53:42 +05:30
|
|
|
import { getDefaultAppState, isEraserActive } from "../appState";
|
2020-03-14 22:53:18 +01:00
|
|
|
import {
|
2020-09-04 14:58:32 +02:00
|
|
|
parseClipboard,
|
2020-03-14 22:53:18 +01:00
|
|
|
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 {
|
2020-12-27 18:26:30 +02:00
|
|
|
APP_NAME,
|
2020-03-07 10:20:38 -05:00
|
|
|
CURSOR_TYPE,
|
2021-10-21 22:05:48 +02:00
|
|
|
DEFAULT_MAX_IMAGE_WIDTH_OR_HEIGHT,
|
2021-04-04 15:57:14 +05:30
|
|
|
DEFAULT_UI_OPTIONS,
|
2020-12-27 18:26:30 +02:00
|
|
|
DEFAULT_VERTICAL_ALIGN,
|
|
|
|
DRAGGING_THRESHOLD,
|
2022-03-22 16:40:28 +05:30
|
|
|
ELEMENT_READY_TO_ERASE_OPACITY,
|
2020-03-07 10:20:38 -05:00
|
|
|
ELEMENT_SHIFT_TRANSLATE_AMOUNT,
|
|
|
|
ELEMENT_TRANSLATE_AMOUNT,
|
2020-04-12 06:12:02 +05:30
|
|
|
ENV,
|
2020-12-27 18:26:30 +02:00
|
|
|
EVENT,
|
2021-02-06 21:22:28 +05:30
|
|
|
GRID_SIZE,
|
2021-10-21 22:05:48 +02:00
|
|
|
IMAGE_RENDER_TIMEOUT,
|
2020-12-27 18:26:30 +02:00
|
|
|
LINE_CONFIRM_THRESHOLD,
|
2021-10-21 22:05:48 +02:00
|
|
|
MAX_ALLOWED_FILE_BYTES,
|
2020-10-13 14:47:07 +02:00
|
|
|
MIME_TYPES,
|
2021-04-08 19:54:50 +02:00
|
|
|
MQ_MAX_HEIGHT_LANDSCAPE,
|
|
|
|
MQ_MAX_WIDTH_LANDSCAPE,
|
|
|
|
MQ_MAX_WIDTH_PORTRAIT,
|
2020-12-27 18:26:30 +02:00
|
|
|
POINTER_BUTTON,
|
2021-02-14 18:18:34 +05:30
|
|
|
SCROLL_TIMEOUT,
|
2020-04-12 06:12:02 +05:30
|
|
|
TAP_TWICE_TIMEOUT,
|
2020-12-27 18:26:30 +02:00
|
|
|
TEXT_TO_CENTER_SNAP_THRESHOLD,
|
2021-10-14 14:15:57 +05:30
|
|
|
THEME,
|
2020-07-02 22:12:56 +01:00
|
|
|
TOUCH_CTX_MENU_TIMEOUT,
|
2021-03-26 18:10:43 +01:00
|
|
|
URL_HASH_KEYS,
|
|
|
|
URL_QUERY_KEYS,
|
2022-03-02 20:06:07 +05:30
|
|
|
VERTICAL_ALIGN,
|
2021-01-30 18:03:23 +01:00
|
|
|
ZOOM_STEP,
|
2020-12-05 20:00:53 +05:30
|
|
|
} from "../constants";
|
2021-01-30 10:30:00 +01:00
|
|
|
import { loadFromBlob } from "../data";
|
2020-12-27 18:26:30 +02:00
|
|
|
import { isValidLibrary } from "../data/json";
|
2021-04-21 23:38:24 +05:30
|
|
|
import Library from "../data/library";
|
2021-11-17 23:53:43 +05:30
|
|
|
import { restore, restoreElements, restoreLibraryItems } from "../data/restore";
|
2020-12-27 18:26:30 +02:00
|
|
|
import {
|
|
|
|
dragNewElement,
|
|
|
|
dragSelectedElements,
|
|
|
|
duplicateElement,
|
|
|
|
getCommonBounds,
|
|
|
|
getCursorForResizingElement,
|
|
|
|
getDragOffsetXY,
|
|
|
|
getElementWithTransformHandleType,
|
|
|
|
getNormalizedDimensions,
|
|
|
|
getPerfectElementSize,
|
|
|
|
getResizeArrowDirection,
|
|
|
|
getResizeOffsetXY,
|
|
|
|
getTransformHandleTypeFromCoords,
|
|
|
|
hitTest,
|
|
|
|
isHittingElementBoundingBoxWithoutHittingElement,
|
|
|
|
isInvisiblySmallElement,
|
|
|
|
isNonDeletedElement,
|
|
|
|
isTextElement,
|
|
|
|
newElement,
|
|
|
|
newLinearElement,
|
|
|
|
newTextElement,
|
2021-10-21 22:05:48 +02:00
|
|
|
newImageElement,
|
2020-12-27 18:26:30 +02:00
|
|
|
textWysiwyg,
|
|
|
|
transformElements,
|
|
|
|
updateTextElement,
|
|
|
|
} from "../element";
|
|
|
|
import {
|
|
|
|
bindOrUnbindSelectedElements,
|
|
|
|
fixBindingsAfterDeletion,
|
|
|
|
fixBindingsAfterDuplication,
|
|
|
|
getEligibleElementsForBinding,
|
|
|
|
getHoveredElementForBinding,
|
|
|
|
isBindingEnabled,
|
|
|
|
isLinearElementSimpleAndAlreadyBound,
|
|
|
|
maybeBindLinearElement,
|
|
|
|
shouldEnableBindingForPointerEvent,
|
|
|
|
unbindLinearElements,
|
|
|
|
updateBoundElements,
|
|
|
|
} from "../element/binding";
|
|
|
|
import { LinearElementEditor } from "../element/linearElementEditor";
|
2022-02-16 13:56:36 +01:00
|
|
|
import { mutateElement, newElementWith } from "../element/mutateElement";
|
2021-05-09 16:42:10 +01:00
|
|
|
import { deepCopyElement, newFreeDrawElement } from "../element/newElement";
|
2020-08-08 21:04:15 -07:00
|
|
|
import {
|
2021-12-16 21:14:03 +05:30
|
|
|
hasBoundTextElement,
|
2020-08-08 21:04:15 -07:00
|
|
|
isBindingElement,
|
|
|
|
isBindingElementType,
|
2021-12-23 22:07:16 +05:30
|
|
|
isBoundToContainer,
|
2021-10-21 22:05:48 +02:00
|
|
|
isImageElement,
|
|
|
|
isInitializedImageElement,
|
2020-12-27 18:26:30 +02:00
|
|
|
isLinearElement,
|
|
|
|
isLinearElementType,
|
2022-03-02 17:04:09 +01:00
|
|
|
isTextBindableContainer,
|
2020-08-08 21:04:15 -07:00
|
|
|
} from "../element/typeChecks";
|
2020-05-26 13:07:46 -07:00
|
|
|
import {
|
2020-12-27 18:26:30 +02:00
|
|
|
ExcalidrawBindableElement,
|
|
|
|
ExcalidrawElement,
|
2021-05-09 16:42:10 +01:00
|
|
|
ExcalidrawFreeDrawElement,
|
2020-12-27 18:26:30 +02:00
|
|
|
ExcalidrawGenericElement,
|
|
|
|
ExcalidrawLinearElement,
|
|
|
|
ExcalidrawTextElement,
|
|
|
|
NonDeleted,
|
2021-10-21 22:05:48 +02:00
|
|
|
InitializedExcalidrawImageElement,
|
|
|
|
ExcalidrawImageElement,
|
|
|
|
FileId,
|
2022-02-03 20:34:59 +05:30
|
|
|
NonDeletedExcalidrawElement,
|
2022-03-02 17:04:09 +01:00
|
|
|
ExcalidrawTextContainer,
|
2020-12-27 18:26:30 +02:00
|
|
|
} from "../element/types";
|
|
|
|
import { getCenter, getDistance } from "../gesture";
|
|
|
|
import {
|
|
|
|
editGroupForSelectedElement,
|
|
|
|
getElementsInGroup,
|
|
|
|
getSelectedGroupIdForElement,
|
2020-05-26 13:07:46 -07:00
|
|
|
getSelectedGroupIds,
|
2020-12-27 18:26:30 +02:00
|
|
|
isElementInGroup,
|
2020-08-26 17:37:44 +01:00
|
|
|
isSelectedViaGroup,
|
2020-05-26 13:07:46 -07:00
|
|
|
selectGroupsForSelectedElements,
|
|
|
|
} from "../groups";
|
2021-04-24 18:21:02 +05:30
|
|
|
import History from "../history";
|
2021-01-05 20:06:14 +02:00
|
|
|
import { defaultLang, getLanguage, languages, setLanguage, t } from "../i18n";
|
2020-08-08 21:04:15 -07:00
|
|
|
import {
|
2020-12-27 18:26:30 +02:00
|
|
|
CODES,
|
2021-10-21 22:05:48 +02:00
|
|
|
shouldResizeFromCenter,
|
|
|
|
shouldMaintainAspectRatio,
|
|
|
|
shouldRotateWithDiscreteAngle,
|
2020-12-27 18:26:30 +02:00
|
|
|
isArrowKey,
|
|
|
|
KEYS,
|
2022-02-07 19:46:29 +01:00
|
|
|
isAndroid,
|
2020-12-27 18:26:30 +02:00
|
|
|
} from "../keys";
|
|
|
|
import { distance2d, getGridPoint, isPathALoop } from "../math";
|
|
|
|
import { renderScene } from "../renderer";
|
|
|
|
import { invalidateShapeForElement } from "../renderer/renderElement";
|
|
|
|
import {
|
|
|
|
calculateScrollCenter,
|
2022-03-02 17:04:09 +01:00
|
|
|
getTextBindableContainerAtPosition,
|
2020-12-27 18:26:30 +02:00
|
|
|
getElementsAtPosition,
|
|
|
|
getElementsWithinSelection,
|
|
|
|
getNormalizedZoom,
|
|
|
|
getSelectedElements,
|
2021-07-04 22:27:33 +02:00
|
|
|
hasBackground,
|
2020-12-27 18:26:30 +02:00
|
|
|
isOverScrollBars,
|
|
|
|
isSomeElementSelected,
|
|
|
|
} from "../scene";
|
|
|
|
import Scene from "../scene/Scene";
|
2021-11-25 14:05:22 +01:00
|
|
|
import { RenderConfig, ScrollBars } from "../scene/types";
|
2022-01-29 21:12:44 +01:00
|
|
|
import { getStateForZoom } from "../scene/zoom";
|
2020-12-27 18:26:30 +02:00
|
|
|
import { findShapeByKey } from "../shapes";
|
2021-04-21 23:38:24 +05:30
|
|
|
import {
|
2021-10-21 22:05:48 +02:00
|
|
|
AppClassProperties,
|
2021-04-21 23:38:24 +05:30
|
|
|
AppProps,
|
|
|
|
AppState,
|
2021-10-21 22:05:48 +02:00
|
|
|
BinaryFileData,
|
|
|
|
DataURL,
|
2021-06-01 23:52:13 +05:30
|
|
|
ExcalidrawImperativeAPI,
|
2021-10-21 22:05:48 +02:00
|
|
|
BinaryFiles,
|
2021-04-21 23:38:24 +05:30
|
|
|
Gesture,
|
|
|
|
GestureEvent,
|
|
|
|
LibraryItems,
|
2021-06-01 23:52:13 +05:30
|
|
|
PointerDownState,
|
2021-04-21 23:38:24 +05:30
|
|
|
SceneData,
|
2022-03-16 15:59:30 +01:00
|
|
|
DeviceType,
|
2021-04-21 23:38:24 +05:30
|
|
|
} from "../types";
|
2020-12-27 18:26:30 +02:00
|
|
|
import {
|
|
|
|
debounce,
|
|
|
|
distance,
|
2021-12-16 21:14:03 +05:30
|
|
|
getFontString,
|
2021-04-09 20:44:54 +05:30
|
|
|
getNearestScrollableContainer,
|
2020-12-27 18:26:30 +02:00
|
|
|
isInputLike,
|
|
|
|
isToolIcon,
|
|
|
|
isWritableElement,
|
|
|
|
resetCursor,
|
|
|
|
resolvablePromise,
|
|
|
|
sceneCoordsToViewportCoords,
|
2021-03-03 14:04:02 +01:00
|
|
|
setCursor,
|
2020-12-27 18:26:30 +02:00
|
|
|
setCursorForShape,
|
|
|
|
tupleToCoors,
|
|
|
|
viewportCoordsToSceneCoords,
|
|
|
|
withBatchedUpdates,
|
2022-02-08 11:25:35 +01:00
|
|
|
wrapEvent,
|
2022-02-06 17:45:37 +01:00
|
|
|
withBatchedUpdatesThrottled,
|
2022-03-16 15:59:30 +01:00
|
|
|
updateObject,
|
2022-03-15 20:56:39 +05:30
|
|
|
setEraserCursor,
|
2020-12-27 18:26:30 +02:00
|
|
|
} from "../utils";
|
2021-02-02 02:26:42 +05:30
|
|
|
import ContextMenu, { ContextMenuOption } from "./ContextMenu";
|
2020-12-27 18:26:30 +02:00
|
|
|
import LayerUI from "./LayerUI";
|
2020-12-07 18:35:16 +02:00
|
|
|
import { Stats } from "./Stats";
|
2021-01-15 20:32:46 +05:30
|
|
|
import { Toast } from "./Toast";
|
2021-02-02 02:26:42 +05:30
|
|
|
import { actionToggleViewMode } from "../actions/actionToggleViewMode";
|
2021-10-21 22:05:48 +02:00
|
|
|
import {
|
|
|
|
dataURLToFile,
|
|
|
|
generateIdFromFile,
|
|
|
|
getDataURL,
|
|
|
|
isSupportedImageFile,
|
|
|
|
resizeImageFile,
|
|
|
|
SVGStringToFile,
|
|
|
|
} from "../data/blob";
|
|
|
|
import {
|
|
|
|
getInitializedImageElements,
|
|
|
|
loadHTMLImageElement,
|
|
|
|
normalizeSVG,
|
|
|
|
updateImageCache as _updateImageCache,
|
|
|
|
} from "../element/image";
|
|
|
|
import throttle from "lodash.throttle";
|
|
|
|
import { fileOpen, nativeFileSystemSupported } from "../data/filesystem";
|
2021-12-16 21:14:03 +05:30
|
|
|
import {
|
|
|
|
bindTextToShapeAfterDuplication,
|
|
|
|
getApproxMinLineHeight,
|
|
|
|
getApproxMinLineWidth,
|
2022-03-02 17:04:09 +01:00
|
|
|
getBoundTextElement,
|
2021-12-16 21:14:03 +05:30
|
|
|
} from "../element/textElement";
|
2021-12-13 13:35:07 +01:00
|
|
|
import { isHittingElementNotConsideringBoundingBox } from "../element/collision";
|
2022-02-03 20:34:59 +05:30
|
|
|
import {
|
|
|
|
normalizeLink,
|
|
|
|
showHyperlinkTooltip,
|
|
|
|
hideHyperlinkToolip,
|
|
|
|
Hyperlink,
|
|
|
|
isPointHittingLinkIcon,
|
|
|
|
isLocalLink,
|
|
|
|
} from "../element/Hyperlink";
|
2020-05-07 14:13:18 -07:00
|
|
|
|
2022-03-16 15:59:30 +01:00
|
|
|
const defaultDeviceTypeContext: DeviceType = {
|
|
|
|
isMobile: false,
|
|
|
|
isTouchScreen: false,
|
|
|
|
};
|
|
|
|
const DeviceTypeContext = React.createContext(defaultDeviceTypeContext);
|
|
|
|
export const useDeviceType = () => useContext(DeviceTypeContext);
|
2021-06-10 02:46:56 +05:30
|
|
|
const ExcalidrawContainerContext = React.createContext<{
|
|
|
|
container: HTMLDivElement | null;
|
|
|
|
id: string | null;
|
|
|
|
}>({ container: null, id: null });
|
2021-04-13 23:02:57 +05:30
|
|
|
export const useExcalidrawContainer = () =>
|
|
|
|
useContext(ExcalidrawContainerContext);
|
2021-04-08 19:54:50 +02:00
|
|
|
|
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;
|
2020-12-05 15:06:50 -08:00
|
|
|
let invalidateContextMenu = 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,
|
|
|
|
};
|
|
|
|
|
2021-04-04 15:57:14 +05:30
|
|
|
class App extends React.Component<AppProps, AppState> {
|
2021-10-21 22:05:48 +02:00
|
|
|
canvas: AppClassProperties["canvas"] = null;
|
2020-04-07 15:29:43 -07:00
|
|
|
rc: RoughCanvas | null = null;
|
2020-05-30 18:56:17 +05:30
|
|
|
unmounted: boolean = false;
|
2020-03-07 10:20:38 -05:00
|
|
|
actionManager: ActionManager;
|
2022-03-16 15:59:30 +01:00
|
|
|
deviceType: DeviceType = {
|
|
|
|
isMobile: false,
|
|
|
|
isTouchScreen: false,
|
|
|
|
};
|
2021-04-08 19:54:50 +02:00
|
|
|
detachIsMobileMqHandler?: () => void;
|
|
|
|
|
2020-12-05 20:00:53 +05:30
|
|
|
private excalidrawContainerRef = React.createRef<HTMLDivElement>();
|
2020-03-26 18:28:26 +01:00
|
|
|
|
2021-04-04 15:57:14 +05:30
|
|
|
public static defaultProps: Partial<AppProps> = {
|
|
|
|
// needed for tests to pass since we directly render App in many tests
|
|
|
|
UIOptions: DEFAULT_UI_OPTIONS,
|
|
|
|
};
|
|
|
|
|
2020-07-30 14:50:59 +05:30
|
|
|
private scene: Scene;
|
2021-04-01 21:10:11 +05:30
|
|
|
private resizeObserver: ResizeObserver | undefined;
|
2021-04-09 20:44:54 +05:30
|
|
|
private nearestScrollableContainer: HTMLElement | Document | undefined;
|
2021-10-21 22:05:48 +02:00
|
|
|
public library: AppClassProperties["library"];
|
2021-04-21 23:38:24 +05:30
|
|
|
public libraryItemsFromStorage: LibraryItems | undefined;
|
|
|
|
private id: string;
|
2021-04-24 18:21:02 +05:30
|
|
|
private history: History;
|
2021-06-10 02:46:56 +05:30
|
|
|
private excalidrawContainerValue: {
|
|
|
|
container: HTMLDivElement | null;
|
|
|
|
id: string;
|
|
|
|
};
|
2021-04-13 23:02:57 +05:30
|
|
|
|
2021-10-21 22:05:48 +02:00
|
|
|
public files: BinaryFiles = {};
|
|
|
|
public imageCache: AppClassProperties["imageCache"] = new Map();
|
|
|
|
|
2022-02-03 20:34:59 +05:30
|
|
|
hitLinkElement?: NonDeletedExcalidrawElement;
|
|
|
|
lastPointerDown: React.PointerEvent<HTMLCanvasElement> | null = null;
|
|
|
|
lastPointerUp: React.PointerEvent<HTMLElement> | PointerEvent | null = null;
|
|
|
|
contextMenuOpen: boolean = false;
|
2022-03-11 19:53:42 +05:30
|
|
|
lastScenePointer: { x: number; y: number } | null = null;
|
2022-02-03 20:34:59 +05:30
|
|
|
|
2021-04-04 15:57:14 +05:30
|
|
|
constructor(props: AppProps) {
|
2020-03-07 10:20:38 -05:00
|
|
|
super(props);
|
2020-07-07 20:40:39 +05:30
|
|
|
const defaultAppState = getDefaultAppState();
|
2020-12-05 20:00:53 +05:30
|
|
|
const {
|
|
|
|
excalidrawRef,
|
2021-02-02 02:26:42 +05:30
|
|
|
viewModeEnabled = false,
|
2021-02-06 21:22:28 +05:30
|
|
|
zenModeEnabled = false,
|
|
|
|
gridModeEnabled = false,
|
2021-03-15 11:33:46 -07:00
|
|
|
theme = defaultAppState.theme,
|
2021-03-20 16:08:03 +05:30
|
|
|
name = defaultAppState.name,
|
2020-12-05 20:00:53 +05:30
|
|
|
} = props;
|
2020-07-07 20:40:39 +05:30
|
|
|
this.state = {
|
|
|
|
...defaultAppState,
|
2021-03-15 11:33:46 -07:00
|
|
|
theme,
|
2020-07-07 20:40:39 +05:30
|
|
|
isLoading: true,
|
2021-03-20 13:00:49 +05:30
|
|
|
...this.getCanvasOffsets(),
|
2021-02-02 02:26:42 +05:30
|
|
|
viewModeEnabled,
|
2021-02-06 21:22:28 +05:30
|
|
|
zenModeEnabled,
|
|
|
|
gridSize: gridModeEnabled ? GRID_SIZE : null,
|
2021-03-20 16:08:03 +05:30
|
|
|
name,
|
2021-04-04 15:05:16 +05:30
|
|
|
width: window.innerWidth,
|
|
|
|
height: window.innerHeight,
|
2022-02-03 20:34:59 +05:30
|
|
|
showHyperlinkPopup: false,
|
2020-07-07 20:40:39 +05:30
|
|
|
};
|
2021-04-13 23:02:57 +05:30
|
|
|
|
2021-04-21 23:38:24 +05:30
|
|
|
this.id = nanoid();
|
|
|
|
|
2020-12-05 20:00:53 +05:30
|
|
|
if (excalidrawRef) {
|
|
|
|
const readyPromise =
|
2020-12-09 01:35:08 +05:30
|
|
|
("current" in excalidrawRef && excalidrawRef.current?.readyPromise) ||
|
|
|
|
resolvablePromise<ExcalidrawImperativeAPI>();
|
|
|
|
|
2020-12-05 20:00:53 +05:30
|
|
|
const api: ExcalidrawImperativeAPI = {
|
|
|
|
ready: true,
|
|
|
|
readyPromise,
|
2020-10-11 21:41:26 +05:30
|
|
|
updateScene: this.updateScene,
|
2021-10-21 22:05:48 +02:00
|
|
|
addFiles: this.addFiles,
|
2020-10-25 19:39:57 +05:30
|
|
|
resetScene: this.resetScene,
|
|
|
|
getSceneElementsIncludingDeleted: this.getSceneElementsIncludingDeleted,
|
2020-12-05 20:00:53 +05:30
|
|
|
history: {
|
|
|
|
clear: this.resetHistory,
|
|
|
|
},
|
2021-04-25 12:28:41 +02:00
|
|
|
scrollToContent: this.scrollToContent,
|
2020-12-05 20:00:53 +05:30
|
|
|
getSceneElements: this.getSceneElements,
|
2021-01-25 10:47:35 +01:00
|
|
|
getAppState: () => this.state,
|
2021-10-21 22:05:48 +02:00
|
|
|
getFiles: () => this.files,
|
2021-04-04 22:05:02 +05:30
|
|
|
refresh: this.refresh,
|
2021-03-26 18:10:43 +01:00
|
|
|
importLibrary: this.importLibraryFromUrl,
|
2021-03-29 20:06:34 +05:30
|
|
|
setToastMessage: this.setToastMessage,
|
2021-04-21 23:38:24 +05:30
|
|
|
id: this.id,
|
2020-12-05 20:00:53 +05:30
|
|
|
} as const;
|
|
|
|
if (typeof excalidrawRef === "function") {
|
|
|
|
excalidrawRef(api);
|
|
|
|
} else {
|
|
|
|
excalidrawRef.current = api;
|
|
|
|
}
|
|
|
|
readyPromise.resolve(api);
|
2020-10-11 21:41:26 +05:30
|
|
|
}
|
2021-06-10 02:46:56 +05:30
|
|
|
|
|
|
|
this.excalidrawContainerValue = {
|
|
|
|
container: this.excalidrawContainerRef.current,
|
|
|
|
id: this.id,
|
|
|
|
};
|
|
|
|
|
2020-07-30 14:50:59 +05:30
|
|
|
this.scene = new Scene();
|
2021-04-21 23:38:24 +05:30
|
|
|
this.library = new Library(this);
|
2021-04-24 18:21:02 +05:30
|
|
|
this.history = new History();
|
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(),
|
2021-01-28 00:41:17 +05:30
|
|
|
this,
|
2020-03-07 10:20:38 -05:00
|
|
|
);
|
|
|
|
this.actionManager.registerAll(actions);
|
|
|
|
|
2021-04-24 18:21:02 +05:30
|
|
|
this.actionManager.registerAction(createUndoAction(this.history));
|
|
|
|
this.actionManager.registerAction(createRedoAction(this.history));
|
2020-03-07 10:20:38 -05:00
|
|
|
}
|
|
|
|
|
2021-02-02 02:26:42 +05:30
|
|
|
private renderCanvas() {
|
|
|
|
const canvasScale = window.devicePixelRatio;
|
|
|
|
const {
|
|
|
|
width: canvasDOMWidth,
|
|
|
|
height: canvasDOMHeight,
|
|
|
|
viewModeEnabled,
|
|
|
|
} = this.state;
|
|
|
|
const canvasWidth = canvasDOMWidth * canvasScale;
|
|
|
|
const canvasHeight = canvasDOMHeight * canvasScale;
|
|
|
|
if (viewModeEnabled) {
|
|
|
|
return (
|
|
|
|
<canvas
|
2021-06-10 02:46:56 +05:30
|
|
|
className="excalidraw__canvas"
|
2021-02-02 02:26:42 +05:30
|
|
|
style={{
|
|
|
|
width: canvasDOMWidth,
|
|
|
|
height: canvasDOMHeight,
|
2021-07-09 18:00:13 -04:00
|
|
|
cursor: CURSOR_TYPE.GRAB,
|
2021-02-02 02:26:42 +05:30
|
|
|
}}
|
|
|
|
width={canvasWidth}
|
|
|
|
height={canvasHeight}
|
|
|
|
ref={this.handleCanvasRef}
|
|
|
|
onContextMenu={this.handleCanvasContextMenu}
|
|
|
|
onPointerMove={this.handleCanvasPointerMove}
|
2022-02-07 13:20:19 +01:00
|
|
|
onPointerUp={this.handleCanvasPointerUp}
|
2021-02-02 02:26:42 +05:30
|
|
|
onPointerCancel={this.removePointer}
|
|
|
|
onTouchMove={this.handleTouchMove}
|
|
|
|
onPointerDown={this.handleCanvasPointerDown}
|
|
|
|
>
|
|
|
|
{t("labels.drawingCanvas")}
|
|
|
|
</canvas>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
return (
|
|
|
|
<canvas
|
2021-06-10 02:46:56 +05:30
|
|
|
className="excalidraw__canvas"
|
2021-02-02 02:26:42 +05:30
|
|
|
style={{
|
|
|
|
width: canvasDOMWidth,
|
|
|
|
height: canvasDOMHeight,
|
|
|
|
}}
|
|
|
|
width={canvasWidth}
|
|
|
|
height={canvasHeight}
|
|
|
|
ref={this.handleCanvasRef}
|
|
|
|
onContextMenu={this.handleCanvasContextMenu}
|
|
|
|
onPointerDown={this.handleCanvasPointerDown}
|
|
|
|
onDoubleClick={this.handleCanvasDoubleClick}
|
|
|
|
onPointerMove={this.handleCanvasPointerMove}
|
2022-02-07 13:20:19 +01:00
|
|
|
onPointerUp={this.handleCanvasPointerUp}
|
2021-02-02 02:26:42 +05:30
|
|
|
onPointerCancel={this.removePointer}
|
|
|
|
onTouchMove={this.handleTouchMove}
|
|
|
|
>
|
|
|
|
{t("labels.drawingCanvas")}
|
|
|
|
</canvas>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2020-03-22 10:24:50 -07:00
|
|
|
public render() {
|
2021-04-18 13:33:05 +02:00
|
|
|
const { zenModeEnabled, viewModeEnabled } = this.state;
|
2022-02-03 20:34:59 +05:30
|
|
|
const selectedElement = getSelectedElements(
|
|
|
|
this.scene.getElements(),
|
|
|
|
this.state,
|
|
|
|
);
|
2021-03-29 20:06:34 +05:30
|
|
|
const {
|
|
|
|
onCollabButtonClick,
|
2021-05-13 21:02:59 +05:30
|
|
|
renderTopRightUI,
|
2021-03-29 20:06:34 +05:30
|
|
|
renderFooter,
|
|
|
|
renderCustomStats,
|
|
|
|
} = this.props;
|
2020-03-22 10:24:50 -07:00
|
|
|
|
|
|
|
return (
|
2020-07-27 17:18:49 +05:30
|
|
|
<div
|
2021-04-13 01:29:25 +05:30
|
|
|
className={clsx("excalidraw excalidraw-container", {
|
2021-02-02 02:26:42 +05:30
|
|
|
"excalidraw--view-mode": viewModeEnabled,
|
2022-03-16 15:59:30 +01:00
|
|
|
"excalidraw--mobile": this.deviceType.isMobile,
|
2021-02-02 02:26:42 +05:30
|
|
|
})}
|
2020-12-05 20:00:53 +05:30
|
|
|
ref={this.excalidrawContainerRef}
|
2021-04-06 22:17:00 +07:00
|
|
|
onDrop={this.handleAppOnDrop}
|
2021-04-13 01:29:25 +05:30
|
|
|
tabIndex={0}
|
|
|
|
onKeyDown={
|
|
|
|
this.props.handleKeyboardGlobally ? undefined : this.onKeyDown
|
|
|
|
}
|
2020-07-27 17:18:49 +05:30
|
|
|
>
|
2021-04-13 23:02:57 +05:30
|
|
|
<ExcalidrawContainerContext.Provider
|
2021-06-10 02:46:56 +05:30
|
|
|
value={this.excalidrawContainerValue}
|
2021-04-13 23:02:57 +05:30
|
|
|
>
|
2022-03-16 15:59:30 +01:00
|
|
|
<DeviceTypeContext.Provider value={this.deviceType}>
|
2021-04-13 23:02:57 +05:30
|
|
|
<LayerUI
|
|
|
|
canvas={this.canvas}
|
2021-04-08 19:54:50 +02:00
|
|
|
appState={this.state}
|
2021-10-21 22:05:48 +02:00
|
|
|
files={this.files}
|
2021-04-08 19:54:50 +02:00
|
|
|
setAppState={this.setAppState}
|
2021-04-13 23:02:57 +05:30
|
|
|
actionManager={this.actionManager}
|
2021-04-08 19:54:50 +02:00
|
|
|
elements={this.scene.getElements()}
|
2021-04-13 23:02:57 +05:30
|
|
|
onCollabButtonClick={onCollabButtonClick}
|
|
|
|
onLockToggle={this.toggleLock}
|
2022-02-02 14:31:38 +01:00
|
|
|
onPenModeToggle={this.togglePenMode}
|
2021-04-13 23:02:57 +05:30
|
|
|
onInsertElements={(elements) =>
|
2021-04-18 13:33:05 +02:00
|
|
|
this.addElementsFromPasteOrLibrary({
|
2021-04-13 23:02:57 +05:30
|
|
|
elements,
|
2021-04-18 13:33:05 +02:00
|
|
|
position: "center",
|
2021-10-21 22:05:48 +02:00
|
|
|
files: null,
|
2021-04-18 13:33:05 +02:00
|
|
|
})
|
2021-04-13 23:02:57 +05:30
|
|
|
}
|
|
|
|
zenModeEnabled={zenModeEnabled}
|
|
|
|
toggleZenMode={this.toggleZenMode}
|
|
|
|
langCode={getLanguage().code}
|
2022-01-17 17:35:35 +05:30
|
|
|
isCollaborating={this.props.isCollaborating}
|
2021-05-13 21:02:59 +05:30
|
|
|
renderTopRightUI={renderTopRightUI}
|
2021-04-13 23:02:57 +05:30
|
|
|
renderCustomFooter={renderFooter}
|
|
|
|
viewModeEnabled={viewModeEnabled}
|
|
|
|
showExitZenModeBtn={
|
|
|
|
typeof this.props?.zenModeEnabled === "undefined" &&
|
|
|
|
zenModeEnabled
|
|
|
|
}
|
|
|
|
showThemeBtn={
|
|
|
|
typeof this.props?.theme === "undefined" &&
|
|
|
|
this.props.UIOptions.canvasActions.theme
|
|
|
|
}
|
|
|
|
libraryReturnUrl={this.props.libraryReturnUrl}
|
|
|
|
UIOptions={this.props.UIOptions}
|
|
|
|
focusContainer={this.focusContainer}
|
2021-04-21 23:38:24 +05:30
|
|
|
library={this.library}
|
|
|
|
id={this.id}
|
2021-10-21 22:05:48 +02:00
|
|
|
onImageAction={this.onImageAction}
|
2021-04-08 19:54:50 +02:00
|
|
|
/>
|
2021-04-13 23:02:57 +05:30
|
|
|
<div className="excalidraw-textEditorContainer" />
|
|
|
|
<div className="excalidraw-contextMenuContainer" />
|
2022-02-03 20:34:59 +05:30
|
|
|
{selectedElement.length === 1 && this.state.showHyperlinkPopup && (
|
|
|
|
<Hyperlink
|
|
|
|
key={selectedElement[0].id}
|
|
|
|
element={selectedElement[0]}
|
|
|
|
appState={this.state}
|
|
|
|
setAppState={this.setAppState}
|
2022-02-08 11:25:35 +01:00
|
|
|
onLinkOpen={this.props.onLinkOpen}
|
2022-02-03 20:34:59 +05:30
|
|
|
/>
|
|
|
|
)}
|
2021-04-13 23:02:57 +05:30
|
|
|
{this.state.showStats && (
|
|
|
|
<Stats
|
|
|
|
appState={this.state}
|
|
|
|
setAppState={this.setAppState}
|
|
|
|
elements={this.scene.getElements()}
|
|
|
|
onClose={this.toggleStats}
|
|
|
|
renderCustomStats={renderCustomStats}
|
|
|
|
/>
|
|
|
|
)}
|
|
|
|
{this.state.toastMessage !== null && (
|
|
|
|
<Toast
|
|
|
|
message={this.state.toastMessage}
|
|
|
|
clearToast={this.clearToast}
|
|
|
|
/>
|
|
|
|
)}
|
|
|
|
<main>{this.renderCanvas()}</main>
|
2022-03-16 15:59:30 +01:00
|
|
|
</DeviceTypeContext.Provider>
|
2021-04-13 23:02:57 +05:30
|
|
|
</ExcalidrawContainerContext.Provider>
|
2020-03-22 10:24:50 -07:00
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2021-10-21 22:05:48 +02:00
|
|
|
public focusContainer: AppClassProperties["focusContainer"] = () => {
|
2021-06-02 19:54:21 +05:30
|
|
|
if (this.props.autoFocus) {
|
|
|
|
this.excalidrawContainerRef.current?.focus();
|
|
|
|
}
|
2021-04-13 01:29:25 +05:30
|
|
|
};
|
|
|
|
|
2020-10-21 19:11:20 +05:30
|
|
|
public getSceneElementsIncludingDeleted = () => {
|
|
|
|
return this.scene.getElementsIncludingDeleted();
|
|
|
|
};
|
|
|
|
|
2020-12-05 20:00:53 +05:30
|
|
|
public getSceneElements = () => {
|
|
|
|
return this.scene.getElements();
|
|
|
|
};
|
|
|
|
|
2020-07-07 13:53:44 +02:00
|
|
|
private syncActionResult = withBatchedUpdates(
|
|
|
|
(actionResult: ActionResult) => {
|
2022-02-03 20:34:59 +05:30
|
|
|
// Since context menu closes when action triggered so setting to false
|
|
|
|
this.contextMenuOpen = false;
|
2020-07-07 13:53:44 +02:00
|
|
|
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) {
|
2021-04-24 18:21:02 +05:30
|
|
|
this.history.resumeRecording();
|
2020-04-11 17:10:56 +01:00
|
|
|
}
|
2020-03-07 10:20:38 -05:00
|
|
|
}
|
|
|
|
|
2021-10-21 22:05:48 +02:00
|
|
|
if (actionResult.files) {
|
|
|
|
this.files = actionResult.replaceFiles
|
|
|
|
? actionResult.files
|
|
|
|
: { ...this.files, ...actionResult.files };
|
|
|
|
this.addNewImagesToImageCache();
|
|
|
|
}
|
|
|
|
|
2020-07-07 13:53:44 +02:00
|
|
|
if (actionResult.appState || editingElement) {
|
|
|
|
if (actionResult.commitToHistory) {
|
2021-04-24 18:21:02 +05:30
|
|
|
this.history.resumeRecording();
|
2020-07-07 13:53:44 +02:00
|
|
|
}
|
2021-02-02 02:26:42 +05:30
|
|
|
|
|
|
|
let viewModeEnabled = actionResult?.appState?.viewModeEnabled || false;
|
2021-02-06 21:22:28 +05:30
|
|
|
let zenModeEnabled = actionResult?.appState?.zenModeEnabled || false;
|
|
|
|
let gridSize = actionResult?.appState?.gridSize || null;
|
2021-10-14 14:15:57 +05:30
|
|
|
let theme = actionResult?.appState?.theme || THEME.LIGHT;
|
2021-03-20 21:57:58 +05:30
|
|
|
let name = actionResult?.appState?.name ?? this.state.name;
|
2021-02-02 02:26:42 +05:30
|
|
|
if (typeof this.props.viewModeEnabled !== "undefined") {
|
|
|
|
viewModeEnabled = this.props.viewModeEnabled;
|
|
|
|
}
|
|
|
|
|
2021-02-06 21:22:28 +05:30
|
|
|
if (typeof this.props.zenModeEnabled !== "undefined") {
|
|
|
|
zenModeEnabled = this.props.zenModeEnabled;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (typeof this.props.gridModeEnabled !== "undefined") {
|
|
|
|
gridSize = this.props.gridModeEnabled ? GRID_SIZE : null;
|
|
|
|
}
|
|
|
|
|
2021-03-15 11:33:46 -07:00
|
|
|
if (typeof this.props.theme !== "undefined") {
|
|
|
|
theme = this.props.theme;
|
|
|
|
}
|
|
|
|
|
2021-03-20 16:08:03 +05:30
|
|
|
if (typeof this.props.name !== "undefined") {
|
|
|
|
name = this.props.name;
|
|
|
|
}
|
2020-07-07 13:53:44 +02:00
|
|
|
this.setState(
|
2021-03-07 15:52:59 +01:00
|
|
|
(state) => {
|
|
|
|
// using Object.assign instead of spread to fool TS 4.2.2+ into
|
|
|
|
// regarding the resulting type as not containing undefined
|
|
|
|
// (which the following expression will never contain)
|
|
|
|
return Object.assign(actionResult.appState || {}, {
|
|
|
|
editingElement:
|
|
|
|
editingElement || actionResult.appState?.editingElement || null,
|
|
|
|
viewModeEnabled,
|
|
|
|
zenModeEnabled,
|
|
|
|
gridSize,
|
2021-03-15 11:33:46 -07:00
|
|
|
theme,
|
2021-03-20 16:08:03 +05:30
|
|
|
name,
|
2021-03-07 15:52:59 +01:00
|
|
|
});
|
|
|
|
},
|
2020-07-07 13:53:44 +02:00
|
|
|
() => {
|
|
|
|
if (actionResult.syncHistory) {
|
2021-04-24 18:21:02 +05:30
|
|
|
this.history.setCurrentState(
|
2020-07-07 13:53:44 +02:00
|
|
|
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.onBlur();
|
|
|
|
};
|
|
|
|
|
2021-11-02 14:24:16 +02:00
|
|
|
private disableEvent: EventListener = (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();
|
|
|
|
};
|
|
|
|
|
2021-03-26 18:32:38 +01:00
|
|
|
private importLibraryFromUrl = async (url: string, token?: string | null) => {
|
2021-03-26 18:10:43 +01:00
|
|
|
if (window.location.hash.includes(URL_HASH_KEYS.addLibrary)) {
|
|
|
|
const hash = new URLSearchParams(window.location.hash.slice(1));
|
|
|
|
hash.delete(URL_HASH_KEYS.addLibrary);
|
|
|
|
window.history.replaceState({}, APP_NAME, `#${hash.toString()}`);
|
|
|
|
} else if (window.location.search.includes(URL_QUERY_KEYS.addLibrary)) {
|
|
|
|
const query = new URLSearchParams(window.location.search);
|
|
|
|
query.delete(URL_QUERY_KEYS.addLibrary);
|
|
|
|
window.history.replaceState({}, APP_NAME, `?${query.toString()}`);
|
|
|
|
}
|
|
|
|
|
2020-10-01 10:12:43 -07:00
|
|
|
try {
|
2021-03-13 12:35:35 +01:00
|
|
|
const request = await fetch(decodeURIComponent(url));
|
2020-10-01 10:12:43 -07:00
|
|
|
const blob = await request.blob();
|
|
|
|
const json = JSON.parse(await blob.text());
|
|
|
|
if (!isValidLibrary(json)) {
|
|
|
|
throw new Error();
|
|
|
|
}
|
|
|
|
if (
|
2021-04-21 23:38:24 +05:30
|
|
|
token === this.id ||
|
2020-10-01 10:12:43 -07:00
|
|
|
window.confirm(
|
2021-11-17 20:54:40 +01:00
|
|
|
t("alerts.confirmAddLibrary", {
|
|
|
|
numShapes: (json.libraryItems || json.library || []).length,
|
|
|
|
}),
|
2020-10-01 10:12:43 -07:00
|
|
|
)
|
|
|
|
) {
|
2021-11-17 23:53:43 +05:30
|
|
|
await this.library.importLibrary(blob, "published");
|
2021-03-26 18:10:43 +01:00
|
|
|
// hack to rerender the library items after import
|
|
|
|
if (this.state.isLibraryOpen) {
|
|
|
|
this.setState({ isLibraryOpen: false });
|
|
|
|
}
|
|
|
|
this.setState({ isLibraryOpen: true });
|
2020-10-01 10:12:43 -07:00
|
|
|
}
|
2021-11-02 14:24:16 +02:00
|
|
|
} catch (error: any) {
|
2020-10-01 10:12:43 -07:00
|
|
|
window.alert(t("alerts.errorLoadingLibrary"));
|
|
|
|
console.error(error);
|
2021-04-13 01:29:25 +05:30
|
|
|
} finally {
|
|
|
|
this.focusContainer();
|
2020-10-01 10:12:43 -07:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2020-10-25 15:48:16 +01:00
|
|
|
private resetHistory = () => {
|
2021-04-24 18:21:02 +05:30
|
|
|
this.history.clear();
|
2020-10-25 15:48:16 +01:00
|
|
|
};
|
|
|
|
|
2020-12-05 20:00:53 +05:30
|
|
|
/**
|
|
|
|
* Resets scene & history.
|
|
|
|
* ! Do not use to clear scene user action !
|
|
|
|
*/
|
|
|
|
private resetScene = withBatchedUpdates(
|
|
|
|
(opts?: { resetLoadingState: boolean }) => {
|
|
|
|
this.scene.replaceAllElements([]);
|
|
|
|
this.setState((state) => ({
|
|
|
|
...getDefaultAppState(),
|
|
|
|
isLoading: opts?.resetLoadingState ? false : state.isLoading,
|
2021-03-13 18:58:06 +05:30
|
|
|
theme: this.state.theme,
|
2020-12-05 20:00:53 +05:30
|
|
|
}));
|
|
|
|
this.resetHistory();
|
|
|
|
},
|
|
|
|
);
|
2020-10-06 04:31:34 +02:00
|
|
|
|
2020-03-26 18:28:26 +01:00
|
|
|
private initializeScene = async () => {
|
2020-09-22 15:21:22 +02:00
|
|
|
if ("launchQueue" in window && "LaunchParams" in window) {
|
|
|
|
(window as any).launchQueue.setConsumer(
|
|
|
|
async (launchParams: { files: any[] }) => {
|
|
|
|
if (!launchParams.files.length) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
const fileHandle = launchParams.files[0];
|
2020-10-19 10:53:37 +02:00
|
|
|
const blob: Blob = await fileHandle.getFile();
|
2020-09-22 15:21:22 +02:00
|
|
|
blob.handle = fileHandle;
|
2021-07-05 13:43:53 +02:00
|
|
|
loadFromBlob(
|
|
|
|
blob,
|
|
|
|
this.state,
|
|
|
|
this.scene.getElementsIncludingDeleted(),
|
|
|
|
)
|
2021-10-21 22:05:48 +02:00
|
|
|
.then((scene) => {
|
2020-09-22 15:21:22 +02:00
|
|
|
this.syncActionResult({
|
2021-10-21 22:05:48 +02:00
|
|
|
...scene,
|
2020-09-22 15:21:22 +02:00
|
|
|
appState: {
|
2021-10-21 22:05:48 +02:00
|
|
|
...(scene.appState || this.state),
|
2020-09-22 15:21:22 +02:00
|
|
|
isLoading: false,
|
|
|
|
},
|
|
|
|
commitToHistory: true,
|
2021-10-21 22:05:48 +02:00
|
|
|
});
|
|
|
|
})
|
2020-09-22 15:21:22 +02:00
|
|
|
.catch((error) => {
|
|
|
|
this.setState({ isLoading: false, errorMessage: error.message });
|
|
|
|
});
|
|
|
|
},
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2020-07-10 07:16:28 +02:00
|
|
|
if (!this.state.isLoading) {
|
|
|
|
this.setState({ isLoading: true });
|
|
|
|
}
|
2020-12-05 20:00:53 +05:30
|
|
|
let initialData = null;
|
|
|
|
try {
|
|
|
|
initialData = (await this.props.initialData) || null;
|
2021-04-21 23:38:24 +05:30
|
|
|
if (initialData?.libraryItems) {
|
2021-11-17 23:53:43 +05:30
|
|
|
this.libraryItemsFromStorage = restoreLibraryItems(
|
|
|
|
initialData.libraryItems,
|
|
|
|
"unpublished",
|
|
|
|
) as LibraryItems;
|
2021-04-21 23:38:24 +05:30
|
|
|
}
|
2021-10-11 19:11:26 +01:00
|
|
|
} catch (error: any) {
|
2020-12-05 20:00:53 +05:30
|
|
|
console.error(error);
|
2021-04-10 19:17:49 +02:00
|
|
|
initialData = {
|
|
|
|
appState: {
|
|
|
|
errorMessage:
|
|
|
|
error.message ||
|
|
|
|
"Encountered an error during importing or restoring scene data",
|
|
|
|
},
|
|
|
|
};
|
2020-03-26 18:28:26 +01:00
|
|
|
}
|
|
|
|
|
2021-07-04 22:23:35 +02:00
|
|
|
const scene = restore(initialData, null, null);
|
2020-12-05 20:00:53 +05:30
|
|
|
scene.appState = {
|
|
|
|
...scene.appState,
|
2022-03-25 20:46:01 +05:30
|
|
|
activeTool:
|
|
|
|
scene.appState.activeTool.type === "image"
|
2022-03-29 17:10:19 +05:30
|
|
|
? { ...scene.appState.activeTool, type: "selection" }
|
2022-03-25 20:46:01 +05:30
|
|
|
: scene.appState.activeTool,
|
2020-12-05 20:00:53 +05:30
|
|
|
isLoading: false,
|
|
|
|
};
|
2021-03-16 23:02:17 +05:30
|
|
|
if (initialData?.scrollToContent) {
|
2021-02-21 19:01:34 +05:30
|
|
|
scene.appState = {
|
|
|
|
...scene.appState,
|
|
|
|
...calculateScrollCenter(
|
|
|
|
scene.elements,
|
|
|
|
{
|
|
|
|
...scene.appState,
|
|
|
|
width: this.state.width,
|
|
|
|
height: this.state.height,
|
|
|
|
offsetTop: this.state.offsetTop,
|
|
|
|
offsetLeft: this.state.offsetLeft,
|
|
|
|
},
|
|
|
|
null,
|
|
|
|
),
|
|
|
|
};
|
|
|
|
}
|
2020-10-01 10:12:43 -07:00
|
|
|
|
2020-12-05 20:00:53 +05:30
|
|
|
this.resetHistory();
|
|
|
|
this.syncActionResult({
|
|
|
|
...scene,
|
|
|
|
commitToHistory: true,
|
|
|
|
});
|
|
|
|
|
2021-03-26 18:10:43 +01:00
|
|
|
const libraryUrl =
|
|
|
|
// current
|
|
|
|
new URLSearchParams(window.location.hash.slice(1)).get(
|
|
|
|
URL_HASH_KEYS.addLibrary,
|
|
|
|
) ||
|
|
|
|
// legacy, kept for compat reasons
|
|
|
|
new URLSearchParams(window.location.search).get(
|
|
|
|
URL_QUERY_KEYS.addLibrary,
|
|
|
|
);
|
2020-10-01 10:12:43 -07:00
|
|
|
|
2021-03-26 18:10:43 +01:00
|
|
|
if (libraryUrl) {
|
|
|
|
await this.importLibraryFromUrl(libraryUrl);
|
2020-10-01 10:12:43 -07:00
|
|
|
}
|
2020-03-26 18:28:26 +01:00
|
|
|
};
|
|
|
|
|
2020-03-22 10:24:50 -07:00
|
|
|
public async componentDidMount() {
|
2022-02-01 06:02:22 -05:00
|
|
|
this.unmounted = false;
|
2021-11-01 15:24:05 +02:00
|
|
|
this.excalidrawContainerValue.container =
|
|
|
|
this.excalidrawContainerRef.current;
|
2021-06-10 02:46:56 +05:30
|
|
|
|
2020-03-22 10:24:50 -07:00
|
|
|
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,
|
|
|
|
},
|
2021-04-24 18:21:02 +05:30
|
|
|
history: {
|
|
|
|
configurable: true,
|
|
|
|
value: this.history,
|
|
|
|
},
|
2020-03-22 10:24:50 -07:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2020-07-30 14:50:59 +05:30
|
|
|
this.scene.addCallback(this.onSceneUpdated);
|
2020-05-30 18:56:17 +05:30
|
|
|
this.addEventListeners();
|
2020-10-23 19:06:16 +02:00
|
|
|
|
2021-04-13 01:29:25 +05:30
|
|
|
if (this.excalidrawContainerRef.current) {
|
|
|
|
this.focusContainer();
|
|
|
|
}
|
|
|
|
|
2021-04-04 15:05:16 +05:30
|
|
|
if ("ResizeObserver" in window && this.excalidrawContainerRef?.current) {
|
|
|
|
this.resizeObserver = new ResizeObserver(() => {
|
2021-04-08 19:54:50 +02:00
|
|
|
// compute isMobile state
|
|
|
|
// ---------------------------------------------------------------------
|
2021-11-01 15:24:05 +02:00
|
|
|
const { width, height } =
|
|
|
|
this.excalidrawContainerRef.current!.getBoundingClientRect();
|
2022-03-16 15:59:30 +01:00
|
|
|
this.deviceType = updateObject(this.deviceType, {
|
|
|
|
isMobile:
|
|
|
|
width < MQ_MAX_WIDTH_PORTRAIT ||
|
|
|
|
(height < MQ_MAX_HEIGHT_LANDSCAPE &&
|
|
|
|
width < MQ_MAX_WIDTH_LANDSCAPE),
|
|
|
|
});
|
2021-04-08 19:54:50 +02:00
|
|
|
// refresh offsets
|
|
|
|
// ---------------------------------------------------------------------
|
2021-04-04 15:05:16 +05:30
|
|
|
this.updateDOMRect();
|
|
|
|
});
|
|
|
|
this.resizeObserver?.observe(this.excalidrawContainerRef.current);
|
2021-04-08 19:54:50 +02:00
|
|
|
} else if (window.matchMedia) {
|
|
|
|
const mediaQuery = window.matchMedia(
|
|
|
|
`(max-width: ${MQ_MAX_WIDTH_PORTRAIT}px), (max-height: ${MQ_MAX_HEIGHT_LANDSCAPE}px) and (max-width: ${MQ_MAX_WIDTH_LANDSCAPE}px)`,
|
|
|
|
);
|
2022-03-16 15:59:30 +01:00
|
|
|
const handler = () => {
|
|
|
|
this.deviceType = updateObject(this.deviceType, {
|
|
|
|
isMobile: mediaQuery.matches,
|
|
|
|
});
|
|
|
|
};
|
2021-04-08 19:54:50 +02:00
|
|
|
mediaQuery.addListener(handler);
|
|
|
|
this.detachIsMobileMqHandler = () => mediaQuery.removeListener(handler);
|
2021-04-01 21:10:11 +05:30
|
|
|
}
|
2021-04-08 19:54:50 +02:00
|
|
|
|
2021-03-14 03:12:54 +05:30
|
|
|
const searchParams = new URLSearchParams(window.location.search.slice(1));
|
|
|
|
|
|
|
|
if (searchParams.has("web-share-target")) {
|
|
|
|
// Obtain a file that was shared via the Web Share Target API.
|
|
|
|
this.restoreFileFromShare();
|
2020-10-23 19:06:16 +02:00
|
|
|
} else {
|
2021-04-04 15:05:16 +05:30
|
|
|
this.updateDOMRect(this.initializeScene);
|
2020-10-23 19:06:16 +02:00
|
|
|
}
|
2020-03-22 10:24:50 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
public componentWillUnmount() {
|
2021-10-21 22:05:48 +02:00
|
|
|
this.files = {};
|
|
|
|
this.imageCache.clear();
|
2021-04-01 21:10:11 +05:30
|
|
|
this.resizeObserver?.disconnect();
|
2020-03-22 10:24:50 -07:00
|
|
|
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-12-05 15:06:50 -08:00
|
|
|
touchTimeout = 0;
|
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-05-30 18:56:17 +05:30
|
|
|
private removeEventListeners() {
|
2021-08-23 22:47:29 +02:00
|
|
|
document.removeEventListener(EVENT.POINTER_UP, this.removePointer);
|
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);
|
2021-04-09 20:44:54 +05:30
|
|
|
this.nearestScrollableContainer?.removeEventListener(
|
|
|
|
EVENT.SCROLL,
|
|
|
|
this.onScroll,
|
|
|
|
);
|
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);
|
2021-12-22 14:25:34 +01:00
|
|
|
this.excalidrawContainerRef.current?.removeEventListener(
|
|
|
|
EVENT.DRAG_OVER,
|
|
|
|
this.disableEvent,
|
|
|
|
false,
|
|
|
|
);
|
|
|
|
this.excalidrawContainerRef.current?.removeEventListener(
|
|
|
|
EVENT.DROP,
|
|
|
|
this.disableEvent,
|
|
|
|
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,
|
|
|
|
);
|
2021-04-08 19:54:50 +02:00
|
|
|
|
|
|
|
this.detachIsMobileMqHandler?.();
|
2020-03-22 10:24:50 -07:00
|
|
|
}
|
2020-05-30 18:56:17 +05:30
|
|
|
|
|
|
|
private addEventListeners() {
|
2021-02-02 02:26:42 +05:30
|
|
|
this.removeEventListeners();
|
2021-08-23 22:47:29 +02:00
|
|
|
document.addEventListener(EVENT.POINTER_UP, this.removePointer); // #3553
|
2020-05-30 18:56:17 +05:30
|
|
|
document.addEventListener(EVENT.COPY, this.onCopy);
|
2021-04-13 01:29:25 +05:30
|
|
|
if (this.props.handleKeyboardGlobally) {
|
|
|
|
document.addEventListener(EVENT.KEYDOWN, this.onKeyDown, false);
|
|
|
|
}
|
2020-05-30 18:56:17 +05:30
|
|
|
document.addEventListener(EVENT.KEYUP, this.onKeyUp, { passive: true });
|
|
|
|
document.addEventListener(
|
|
|
|
EVENT.MOUSE_MOVE,
|
|
|
|
this.updateCurrentCursorPosition,
|
|
|
|
);
|
|
|
|
// 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,
|
|
|
|
);
|
2021-02-02 02:26:42 +05:30
|
|
|
if (this.state.viewModeEnabled) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
document.addEventListener(EVENT.PASTE, this.pasteFromClipboard);
|
|
|
|
document.addEventListener(EVENT.CUT, this.onCut);
|
2021-04-09 20:44:54 +05:30
|
|
|
if (this.props.detectScroll) {
|
|
|
|
this.nearestScrollableContainer = getNearestScrollableContainer(
|
|
|
|
this.excalidrawContainerRef.current!,
|
|
|
|
);
|
|
|
|
this.nearestScrollableContainer.addEventListener(
|
|
|
|
EVENT.SCROLL,
|
|
|
|
this.onScroll,
|
|
|
|
);
|
|
|
|
}
|
2021-02-02 02:26:42 +05:30
|
|
|
window.addEventListener(EVENT.RESIZE, this.onResize, false);
|
|
|
|
window.addEventListener(EVENT.UNLOAD, this.onUnload, false);
|
|
|
|
window.addEventListener(EVENT.BLUR, this.onBlur, false);
|
2021-12-22 14:25:34 +01:00
|
|
|
this.excalidrawContainerRef.current?.addEventListener(
|
|
|
|
EVENT.DRAG_OVER,
|
|
|
|
this.disableEvent,
|
|
|
|
false,
|
|
|
|
);
|
|
|
|
this.excalidrawContainerRef.current?.addEventListener(
|
|
|
|
EVENT.DROP,
|
|
|
|
this.disableEvent,
|
|
|
|
false,
|
|
|
|
);
|
2020-05-30 18:56:17 +05:30
|
|
|
}
|
2020-03-22 10:24:50 -07:00
|
|
|
|
2021-04-04 15:57:14 +05:30
|
|
|
componentDidUpdate(prevProps: AppProps, prevState: AppState) {
|
2022-03-11 19:53:42 +05:30
|
|
|
if (
|
|
|
|
Object.keys(this.state.selectedElementIds).length &&
|
|
|
|
isEraserActive(this.state)
|
|
|
|
) {
|
2022-03-29 17:10:19 +05:30
|
|
|
this.setState({
|
|
|
|
activeTool: { ...this.state.activeTool, type: "selection" },
|
|
|
|
});
|
2022-03-11 19:53:42 +05:30
|
|
|
}
|
2022-03-15 20:56:39 +05:30
|
|
|
if (prevState.theme !== this.state.theme) {
|
|
|
|
setEraserCursor(this.canvas, this.state.theme);
|
|
|
|
}
|
2022-02-03 20:34:59 +05:30
|
|
|
// Hide hyperlink popup if shown when element type is not selection
|
|
|
|
if (
|
2022-03-25 20:46:01 +05:30
|
|
|
prevState.activeTool.type === "selection" &&
|
|
|
|
this.state.activeTool.type !== "selection" &&
|
2022-02-03 20:34:59 +05:30
|
|
|
this.state.showHyperlinkPopup
|
|
|
|
) {
|
|
|
|
this.setState({ showHyperlinkPopup: false });
|
|
|
|
}
|
2021-01-04 02:21:52 +05:30
|
|
|
if (prevProps.langCode !== this.props.langCode) {
|
|
|
|
this.updateLanguage();
|
|
|
|
}
|
|
|
|
|
2021-02-02 02:26:42 +05:30
|
|
|
if (prevProps.viewModeEnabled !== this.props.viewModeEnabled) {
|
2021-06-16 19:01:16 +02:00
|
|
|
this.setState({ viewModeEnabled: !!this.props.viewModeEnabled });
|
2021-02-02 02:26:42 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
if (prevState.viewModeEnabled !== this.state.viewModeEnabled) {
|
|
|
|
this.addEventListeners();
|
2021-06-16 19:01:16 +02:00
|
|
|
this.deselectElements();
|
2021-02-02 02:26:42 +05:30
|
|
|
}
|
|
|
|
|
2021-02-06 21:22:28 +05:30
|
|
|
if (prevProps.zenModeEnabled !== this.props.zenModeEnabled) {
|
|
|
|
this.setState({ zenModeEnabled: !!this.props.zenModeEnabled });
|
|
|
|
}
|
|
|
|
|
2021-03-15 11:33:46 -07:00
|
|
|
if (prevProps.theme !== this.props.theme && this.props.theme) {
|
|
|
|
this.setState({ theme: this.props.theme });
|
|
|
|
}
|
|
|
|
|
2021-02-06 21:22:28 +05:30
|
|
|
if (prevProps.gridModeEnabled !== this.props.gridModeEnabled) {
|
|
|
|
this.setState({
|
|
|
|
gridSize: this.props.gridModeEnabled ? GRID_SIZE : null,
|
|
|
|
});
|
|
|
|
}
|
2021-03-20 16:08:03 +05:30
|
|
|
|
|
|
|
if (this.props.name && prevProps.name !== this.props.name) {
|
|
|
|
this.setState({
|
|
|
|
name: this.props.name,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2021-04-13 23:02:57 +05:30
|
|
|
this.excalidrawContainerRef.current?.classList.toggle(
|
|
|
|
"theme--dark",
|
|
|
|
this.state.theme === "dark",
|
|
|
|
);
|
2020-08-13 04:35:31 -07: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 (
|
2022-03-25 20:46:01 +05:30
|
|
|
prevState.activeTool !== this.state.activeTool &&
|
2020-08-08 21:04:15 -07:00
|
|
|
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;
|
|
|
|
} = {};
|
2021-11-25 14:05:22 +01:00
|
|
|
const pointerViewportCoords: RenderConfig["remotePointerViewportCoords"] =
|
|
|
|
{};
|
|
|
|
const remoteSelectedElementIds: RenderConfig["remoteSelectedElementIds"] =
|
|
|
|
{};
|
2020-04-07 14:02:42 +01:00
|
|
|
const pointerUsernames: { [id: string]: string } = {};
|
2021-02-04 11:55:43 +01:00
|
|
|
const pointerUserStates: { [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
|
|
|
}
|
2021-02-04 11:55:43 +01:00
|
|
|
if (user.userState) {
|
|
|
|
pointerUserStates[socketId] = user.userState;
|
|
|
|
}
|
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,
|
|
|
|
);
|
2020-08-14 20:14:22 +02:00
|
|
|
cursorButton[socketId] = user.button;
|
2020-03-22 10:24:50 -07:00
|
|
|
});
|
2021-10-21 22:05:48 +02:00
|
|
|
const renderingElements = this.scene.getElements().filter((element) => {
|
|
|
|
if (isImageElement(element)) {
|
|
|
|
if (
|
|
|
|
// not placed on canvas yet (but in elements array)
|
|
|
|
this.state.pendingImageElement &&
|
|
|
|
element.id === this.state.pendingImageElement.id
|
|
|
|
) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// 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
|
|
|
const { atLeastOneVisibleElement, scrollBars } = renderScene(
|
2021-10-21 22:05:48 +02:00
|
|
|
renderingElements,
|
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-11-29 18:32:51 +02:00
|
|
|
remoteSelectedElementIds,
|
2020-04-07 14:02:42 +01:00
|
|
|
remotePointerUsernames: pointerUsernames,
|
2021-02-04 11:55:43 +01:00
|
|
|
remotePointerUserStates: pointerUserStates,
|
2020-03-28 16:59:36 -07:00
|
|
|
shouldCacheIgnoreZoom: this.state.shouldCacheIgnoreZoom,
|
2021-10-21 22:05:48 +02:00
|
|
|
theme: this.state.theme,
|
|
|
|
imageCache: this.imageCache,
|
2021-11-25 14:05:22 +01:00
|
|
|
isExporting: false,
|
2022-03-16 15:59:30 +01:00
|
|
|
renderScrollbars: !this.deviceType.isMobile,
|
2020-03-22 10:24:50 -07:00
|
|
|
},
|
|
|
|
);
|
2022-02-03 20:34:59 +05:30
|
|
|
|
2020-03-22 10:24:50 -07:00
|
|
|
if (scrollBars) {
|
|
|
|
currentScrollBars = scrollBars;
|
|
|
|
}
|
2020-06-25 21:21:27 +02:00
|
|
|
const scrolledOutside =
|
|
|
|
// hide when editing text
|
2021-12-16 21:14:03 +05:30
|
|
|
isTextElement(this.state.editingElement)
|
2020-06-25 21:21:27 +02:00
|
|
|
? false
|
2021-10-21 22:05:48 +02:00
|
|
|
: !atLeastOneVisibleElement && renderingElements.length > 0;
|
2020-03-22 10:24:50 -07:00
|
|
|
if (this.state.scrolledOutside !== scrolledOutside) {
|
2020-11-29 18:32:51 +02:00
|
|
|
this.setState({ scrolledOutside });
|
2020-03-22 10:24:50 -07:00
|
|
|
}
|
|
|
|
|
2021-04-24 18:21:02 +05:30
|
|
|
this.history.record(this.state, this.scene.getElementsIncludingDeleted());
|
2020-08-20 16:45:20 +02:00
|
|
|
|
2021-10-21 22:05:48 +02:00
|
|
|
this.scheduleImageRefresh();
|
|
|
|
|
2020-12-29 21:03:34 +01:00
|
|
|
// Do not notify consumers if we're still loading the scene. Among other
|
|
|
|
// potential issues, this fixes a case where the tab isn't focused during
|
|
|
|
// init, which would trigger onChange with empty elements, which would then
|
|
|
|
// override whatever is in localStorage currently.
|
|
|
|
if (!this.state.isLoading) {
|
|
|
|
this.props.onChange?.(
|
|
|
|
this.scene.getElementsIncludingDeleted(),
|
|
|
|
this.state,
|
2021-10-21 22:05:48 +02:00
|
|
|
this.files,
|
2020-12-29 21:03:34 +01:00
|
|
|
);
|
|
|
|
}
|
2020-03-22 10:24:50 -07:00
|
|
|
}
|
|
|
|
|
2021-02-14 18:18:34 +05:30
|
|
|
private onScroll = debounce(() => {
|
2021-03-06 19:06:42 +05:30
|
|
|
const { offsetTop, offsetLeft } = this.getCanvasOffsets();
|
|
|
|
this.setState((state) => {
|
|
|
|
if (state.offsetLeft === offsetLeft && state.offsetTop === offsetTop) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
return { offsetTop, offsetLeft };
|
|
|
|
});
|
2021-02-14 18:18:34 +05:30
|
|
|
}, SCROLL_TIMEOUT);
|
|
|
|
|
2020-03-22 10:24:50 -07:00
|
|
|
// Copy/paste
|
|
|
|
|
|
|
|
private onCut = withBatchedUpdates((event: ClipboardEvent) => {
|
2021-04-25 15:13:42 +05:30
|
|
|
const isExcalidrawActive = this.excalidrawContainerRef.current?.contains(
|
|
|
|
document.activeElement,
|
|
|
|
);
|
|
|
|
if (!isExcalidrawActive || isWritableElement(event.target)) {
|
2020-03-22 10:24:50 -07:00
|
|
|
return;
|
|
|
|
}
|
2020-12-12 05:54:34 -06:00
|
|
|
this.cutAll();
|
2020-03-22 10:24:50 -07:00
|
|
|
event.preventDefault();
|
|
|
|
});
|
|
|
|
|
|
|
|
private onCopy = withBatchedUpdates((event: ClipboardEvent) => {
|
2021-04-25 15:13:42 +05:30
|
|
|
const isExcalidrawActive = this.excalidrawContainerRef.current?.contains(
|
|
|
|
document.activeElement,
|
|
|
|
);
|
|
|
|
if (!isExcalidrawActive || isWritableElement(event.target)) {
|
2020-03-22 10:24:50 -07:00
|
|
|
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
|
|
|
|
2020-12-12 05:54:34 -06:00
|
|
|
private cutAll = () => {
|
2022-03-28 14:46:40 +02:00
|
|
|
this.actionManager.executeAction(actionCut, "keyboard");
|
2020-12-12 05:54:34 -06:00
|
|
|
};
|
|
|
|
|
2020-03-27 15:18:14 -07:00
|
|
|
private copyAll = () => {
|
2022-03-28 14:46:40 +02:00
|
|
|
this.actionManager.executeAction(actionCopy, "keyboard");
|
2020-03-22 10:24:50 -07:00
|
|
|
};
|
|
|
|
|
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) => {
|
2022-02-06 20:07:37 +01:00
|
|
|
// fix for Apple Pencil Scribble
|
2022-02-07 19:46:29 +01:00
|
|
|
// On Android, preventing the event would disable contextMenu on tap-hold
|
|
|
|
if (!isAndroid) {
|
|
|
|
event.preventDefault();
|
|
|
|
}
|
2022-02-06 20:07:37 +01:00
|
|
|
|
2020-04-04 14:55:36 +02:00
|
|
|
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);
|
|
|
|
}
|
2022-02-07 19:46:29 +01:00
|
|
|
if (isAndroid) {
|
|
|
|
event.preventDefault();
|
|
|
|
}
|
2022-02-06 20:07:37 +01:00
|
|
|
|
2020-06-02 18:41:40 +02:00
|
|
|
if (event.touches.length === 2) {
|
|
|
|
this.setState({
|
|
|
|
selectedElementIds: {},
|
|
|
|
});
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
private onTapEnd = (event: TouchEvent) => {
|
2022-02-09 20:42:02 +01:00
|
|
|
this.resetContextMenuTimer();
|
2020-06-02 18:41:40 +02:00
|
|
|
if (event.touches.length > 0) {
|
|
|
|
this.setState({
|
|
|
|
previousSelectedElementIds: {},
|
2020-08-27 20:59:46 +02:00
|
|
|
selectedElementIds: this.state.previousSelectedElementIds,
|
2020-06-02 18:41:40 +02:00
|
|
|
});
|
2022-02-09 20:42:02 +01:00
|
|
|
} else {
|
|
|
|
gesture.pointers.clear();
|
2020-06-02 18:41:40 +02:00
|
|
|
}
|
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;
|
2021-11-01 15:24:05 +02:00
|
|
|
const isExcalidrawActive =
|
|
|
|
this.excalidrawContainerRef.current?.contains(target);
|
2021-04-25 15:13:42 +05:30
|
|
|
if (!isExcalidrawActive) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2020-03-22 10:24:50 -07:00
|
|
|
const elementUnderCursor = document.elementFromPoint(cursorX, cursorY);
|
|
|
|
if (
|
|
|
|
// if no ClipboardEvent supplied, assume we're pasting via contextMenu
|
2020-11-05 19:06:18 +02:00
|
|
|
// 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;
|
|
|
|
}
|
2021-10-21 22:05:48 +02:00
|
|
|
|
2021-10-22 21:04:04 +02:00
|
|
|
// must be called in the same frame (thus before any awaits) as the paste
|
|
|
|
// event else some browsers (FF...) will clear the clipboardData
|
|
|
|
// (something something security)
|
2021-10-21 22:05:48 +02:00
|
|
|
let file = event?.clipboardData?.files[0];
|
|
|
|
|
2021-10-22 21:04:04 +02:00
|
|
|
const data = await parseClipboard(event);
|
|
|
|
|
2021-10-21 22:05:48 +02:00
|
|
|
if (!file && data.text) {
|
|
|
|
const string = data.text.trim();
|
|
|
|
if (string.startsWith("<svg") && string.endsWith("</svg>")) {
|
|
|
|
// ignore SVG validation/normalization which will be done during image
|
|
|
|
// initialization
|
|
|
|
file = SVGStringToFile(string);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-01-07 23:18:04 +01:00
|
|
|
// prefer spreadsheet data over image file (MS Office/Libre Office)
|
|
|
|
if (isSupportedImageFile(file) && !data.spreadsheet) {
|
2021-10-21 22:05:48 +02:00
|
|
|
const { x: sceneX, y: sceneY } = viewportCoordsToSceneCoords(
|
|
|
|
{ clientX: cursorX, clientY: cursorY },
|
|
|
|
this.state,
|
|
|
|
);
|
|
|
|
|
|
|
|
const imageElement = this.createImageElement({ sceneX, sceneY });
|
|
|
|
this.insertImageElement(imageElement, file);
|
|
|
|
this.initializeImageDimensions(imageElement);
|
|
|
|
this.setState({ selectedElementIds: { [imageElement.id]: true } });
|
|
|
|
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2021-04-09 16:49:58 +02:00
|
|
|
if (this.props.onPaste) {
|
2021-09-13 17:28:53 +02:00
|
|
|
try {
|
|
|
|
if ((await this.props.onPaste(data, event)) === false) {
|
|
|
|
return;
|
|
|
|
}
|
2021-11-02 14:24:16 +02:00
|
|
|
} catch (error: any) {
|
|
|
|
console.error(error);
|
2021-04-09 16:49:58 +02:00
|
|
|
}
|
|
|
|
}
|
2020-09-04 14:58:32 +02:00
|
|
|
if (data.errorMessage) {
|
|
|
|
this.setState({ errorMessage: data.errorMessage });
|
|
|
|
} else if (data.spreadsheet) {
|
2020-12-27 18:26:30 +02:00
|
|
|
this.setState({
|
|
|
|
pasteDialog: {
|
|
|
|
data: data.spreadsheet,
|
|
|
|
shown: true,
|
|
|
|
},
|
|
|
|
});
|
2020-06-06 13:09:04 -07:00
|
|
|
} else if (data.elements) {
|
2021-04-18 13:33:05 +02:00
|
|
|
this.addElementsFromPasteOrLibrary({
|
2021-07-04 22:23:35 +02:00
|
|
|
elements: data.elements,
|
2021-10-21 22:05:48 +02:00
|
|
|
files: data.files || null,
|
2021-04-18 13:33:05 +02:00
|
|
|
position: "cursor",
|
|
|
|
});
|
2020-03-28 15:43:09 -07:00
|
|
|
} else if (data.text) {
|
|
|
|
this.addTextFromPaste(data.text);
|
2020-03-22 10:24:50 -07:00
|
|
|
}
|
2022-03-29 17:10:19 +05:30
|
|
|
this.setActiveTool({ ...this.state.activeTool, type: "selection" });
|
2020-03-28 15:43:09 -07:00
|
|
|
event?.preventDefault();
|
2020-03-22 10:24:50 -07:00
|
|
|
},
|
|
|
|
);
|
|
|
|
|
2021-04-18 13:33:05 +02:00
|
|
|
private addElementsFromPasteOrLibrary = (opts: {
|
|
|
|
elements: readonly ExcalidrawElement[];
|
2021-10-21 22:05:48 +02:00
|
|
|
files: BinaryFiles | null;
|
2021-04-18 13:33:05 +02:00
|
|
|
position: { clientX: number; clientY: number } | "cursor" | "center";
|
|
|
|
}) => {
|
2021-07-04 22:23:35 +02:00
|
|
|
const elements = restoreElements(opts.elements, null);
|
2021-05-09 21:42:12 +02:00
|
|
|
const [minX, minY, maxX, maxY] = getCommonBounds(elements);
|
2020-03-22 10:24:50 -07:00
|
|
|
|
|
|
|
const elementsCenterX = distance(minX, maxX) / 2;
|
|
|
|
const elementsCenterY = distance(minY, maxY) / 2;
|
|
|
|
|
2021-04-18 13:33:05 +02:00
|
|
|
const clientX =
|
|
|
|
typeof opts.position === "object"
|
|
|
|
? opts.position.clientX
|
|
|
|
: opts.position === "cursor"
|
|
|
|
? cursorX
|
|
|
|
: this.state.width / 2 + this.state.offsetLeft;
|
|
|
|
const clientY =
|
|
|
|
typeof opts.position === "object"
|
|
|
|
? opts.position.clientY
|
|
|
|
: opts.position === "cursor"
|
|
|
|
? cursorY
|
|
|
|
: this.state.height / 2 + this.state.offsetTop;
|
|
|
|
|
2020-03-22 10:24:50 -07:00
|
|
|
const { x, y } = viewportCoordsToSceneCoords(
|
2020-07-10 02:20:23 -07:00
|
|
|
{ clientX, clientY },
|
2020-03-22 10:24:50 -07:00
|
|
|
this.state,
|
|
|
|
);
|
|
|
|
|
|
|
|
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-12-09 14:45:03 +01:00
|
|
|
const [gridX, gridY] = getGridPoint(dx, dy, this.state.gridSize);
|
|
|
|
|
2020-08-08 21:04:15 -07:00
|
|
|
const oldIdToDuplicatedId = new Map();
|
2021-05-09 21:42:12 +02:00
|
|
|
const newElements = elements.map((element) => {
|
2020-08-08 21:04:15 -07:00
|
|
|
const newElement = duplicateElement(
|
|
|
|
this.state.editingGroupId,
|
|
|
|
groupIdMap,
|
|
|
|
element,
|
|
|
|
{
|
2020-12-09 14:45:03 +01:00
|
|
|
x: element.x + gridX - minX,
|
|
|
|
y: element.y + gridY - minY,
|
2020-08-08 21:04:15 -07:00
|
|
|
},
|
|
|
|
);
|
|
|
|
oldIdToDuplicatedId.set(element.id, newElement.id);
|
|
|
|
return newElement;
|
2020-07-30 14:50:59 +05:30
|
|
|
});
|
2021-12-16 21:14:03 +05:30
|
|
|
bindTextToShapeAfterDuplication(newElements, elements, oldIdToDuplicatedId);
|
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
|
|
|
];
|
2021-05-09 21:42:12 +02:00
|
|
|
fixBindingsAfterDuplication(nextElements, elements, oldIdToDuplicatedId);
|
2020-08-08 21:04:15 -07:00
|
|
|
|
2021-10-21 22:05:48 +02:00
|
|
|
if (opts.files) {
|
|
|
|
this.files = { ...this.files, ...opts.files };
|
|
|
|
}
|
|
|
|
|
2020-08-08 21:04:15 -07:00
|
|
|
this.scene.replaceAllElements(nextElements);
|
2021-04-24 18:21:02 +05:30
|
|
|
this.history.resumeRecording();
|
2020-08-08 22:35:34 +02:00
|
|
|
this.setState(
|
|
|
|
selectGroupsForSelectedElements(
|
|
|
|
{
|
|
|
|
...this.state,
|
|
|
|
isLibraryOpen: false,
|
|
|
|
selectedElementIds: newElements.reduce((map, element) => {
|
2021-12-23 22:07:16 +05:30
|
|
|
if (!isBoundToContainer(element)) {
|
2021-12-16 21:14:03 +05:30
|
|
|
map[element.id] = true;
|
|
|
|
}
|
2020-08-08 22:35:34 +02:00
|
|
|
return map;
|
|
|
|
}, {} as any),
|
|
|
|
selectedGroupIds: {},
|
|
|
|
},
|
|
|
|
this.scene.getElements(),
|
|
|
|
),
|
2021-10-21 22:05:48 +02:00
|
|
|
() => {
|
|
|
|
if (opts.files) {
|
|
|
|
this.addNewImagesToImageCache();
|
|
|
|
}
|
|
|
|
},
|
2020-08-08 22:35:34 +02:00
|
|
|
);
|
2022-03-29 17:10:19 +05:30
|
|
|
this.setActiveTool({ ...this.state.activeTool, type: "selection" });
|
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,
|
|
|
|
);
|
|
|
|
|
|
|
|
const element = newTextElement({
|
2020-11-29 18:32:51 +02:00
|
|
|
x,
|
|
|
|
y,
|
2020-03-28 15:43:09 -07:00
|
|
|
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-11-29 18:32:51 +02:00
|
|
|
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 } });
|
2021-04-24 18:21:02 +05:30
|
|
|
this.history.resumeRecording();
|
2020-03-28 15:43:09 -07:00
|
|
|
}
|
|
|
|
|
2020-03-22 10:24:50 -07:00
|
|
|
// Collaboration
|
|
|
|
|
|
|
|
setAppState = (obj: any) => {
|
|
|
|
this.setState(obj);
|
|
|
|
};
|
|
|
|
|
2021-08-23 22:47:29 +02:00
|
|
|
removePointer = (event: React.PointerEvent<HTMLElement> | PointerEvent) => {
|
2022-02-09 20:42:02 +01:00
|
|
|
if (touchTimeout) {
|
|
|
|
this.resetContextMenuTimer();
|
2020-07-02 22:12:56 +01:00
|
|
|
}
|
|
|
|
|
2020-03-22 10:24:50 -07:00
|
|
|
gesture.pointers.delete(event.pointerId);
|
|
|
|
};
|
|
|
|
|
2022-03-28 14:46:40 +02:00
|
|
|
toggleLock = (source: "keyboard" | "ui" = "ui") => {
|
2022-03-29 17:10:19 +05:30
|
|
|
if (!this.state.activeTool.locked) {
|
2022-03-28 14:46:40 +02:00
|
|
|
trackEvent(
|
|
|
|
"toolbar",
|
|
|
|
"toggleLock",
|
|
|
|
`${source} (${this.deviceType.isMobile ? "mobile" : "desktop"})`,
|
|
|
|
);
|
|
|
|
}
|
2020-12-02 23:57:51 +02:00
|
|
|
this.setState((prevState) => {
|
|
|
|
return {
|
2022-03-29 17:10:19 +05:30
|
|
|
activeTool: {
|
|
|
|
...prevState.activeTool,
|
|
|
|
locked: !prevState.activeTool.locked,
|
|
|
|
type: prevState.activeTool.locked
|
|
|
|
? "selection"
|
|
|
|
: prevState.activeTool.type,
|
|
|
|
},
|
2020-12-02 23:57:51 +02:00
|
|
|
};
|
|
|
|
});
|
2020-03-07 10:20:38 -05:00
|
|
|
};
|
|
|
|
|
2022-02-02 14:31:38 +01:00
|
|
|
togglePenMode = () => {
|
|
|
|
this.setState((prevState) => {
|
|
|
|
return {
|
|
|
|
penMode: !prevState.penMode,
|
|
|
|
};
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2020-04-25 18:43:02 +05:30
|
|
|
toggleZenMode = () => {
|
2021-01-28 00:41:17 +05:30
|
|
|
this.actionManager.executeAction(actionToggleZenMode);
|
2020-04-25 18:43:02 +05:30
|
|
|
};
|
|
|
|
|
2020-12-07 18:35:16 +02:00
|
|
|
toggleStats = () => {
|
2021-01-29 02:21:10 +05:30
|
|
|
this.actionManager.executeAction(actionToggleStats);
|
2020-12-07 18:35:16 +02:00
|
|
|
};
|
|
|
|
|
2021-04-25 12:28:41 +02:00
|
|
|
scrollToContent = (
|
|
|
|
target:
|
|
|
|
| ExcalidrawElement
|
|
|
|
| readonly ExcalidrawElement[] = this.scene.getElements(),
|
|
|
|
) => {
|
2020-10-11 21:41:26 +05:30
|
|
|
this.setState({
|
2021-04-25 12:28:41 +02:00
|
|
|
...calculateScrollCenter(
|
|
|
|
Array.isArray(target) ? target : [target],
|
|
|
|
this.state,
|
|
|
|
this.canvas,
|
|
|
|
),
|
2020-10-11 21:41:26 +05:30
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2021-01-15 20:32:46 +05:30
|
|
|
clearToast = () => {
|
|
|
|
this.setState({ toastMessage: null });
|
|
|
|
};
|
|
|
|
|
2021-03-29 20:06:34 +05:30
|
|
|
setToastMessage = (toastMessage: string) => {
|
|
|
|
this.setState({ toastMessage });
|
|
|
|
};
|
|
|
|
|
2021-03-14 03:12:54 +05:30
|
|
|
restoreFileFromShare = async () => {
|
|
|
|
try {
|
|
|
|
const webShareTargetCache = await caches.open("web-share-target");
|
|
|
|
|
|
|
|
const file = await webShareTargetCache.match("shared-file");
|
|
|
|
if (file) {
|
|
|
|
const blob = await file.blob();
|
|
|
|
this.loadFileToCanvas(blob);
|
|
|
|
await webShareTargetCache.delete("shared-file");
|
|
|
|
window.history.replaceState(null, APP_NAME, window.location.pathname);
|
|
|
|
}
|
2021-10-11 19:11:26 +01:00
|
|
|
} catch (error: any) {
|
2021-03-14 03:12:54 +05:30
|
|
|
this.setState({ errorMessage: error.message });
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2021-10-21 22:05:48 +02:00
|
|
|
/** adds supplied files to existing files in the appState */
|
|
|
|
public addFiles: ExcalidrawImperativeAPI["addFiles"] = withBatchedUpdates(
|
|
|
|
(files) => {
|
|
|
|
const filesMap = files.reduce((acc, fileData) => {
|
|
|
|
acc.set(fileData.id, fileData);
|
|
|
|
return acc;
|
|
|
|
}, new Map<FileId, BinaryFileData>());
|
|
|
|
|
|
|
|
this.files = { ...this.files, ...Object.fromEntries(filesMap) };
|
|
|
|
|
|
|
|
this.scene.getElements().forEach((element) => {
|
|
|
|
if (
|
|
|
|
isInitializedImageElement(element) &&
|
|
|
|
filesMap.has(element.fileId)
|
|
|
|
) {
|
2021-10-23 14:17:04 +02:00
|
|
|
this.imageCache.delete(element.fileId);
|
|
|
|
invalidateShapeForElement(element);
|
2021-10-21 22:05:48 +02:00
|
|
|
}
|
|
|
|
});
|
|
|
|
this.scene.informMutation();
|
2021-10-23 14:17:04 +02:00
|
|
|
|
|
|
|
this.addNewImagesToImageCache();
|
2021-10-21 22:05:48 +02:00
|
|
|
},
|
|
|
|
);
|
|
|
|
|
2021-05-14 17:52:56 +05:30
|
|
|
public updateScene = withBatchedUpdates(
|
|
|
|
<K extends keyof AppState>(sceneData: {
|
|
|
|
elements?: SceneData["elements"];
|
|
|
|
appState?: Pick<AppState, K> | null;
|
|
|
|
collaborators?: SceneData["collaborators"];
|
|
|
|
commitToHistory?: SceneData["commitToHistory"];
|
2022-01-06 21:37:33 +05:30
|
|
|
libraryItems?: SceneData["libraryItems"];
|
2021-05-14 17:52:56 +05:30
|
|
|
}) => {
|
|
|
|
if (sceneData.commitToHistory) {
|
|
|
|
this.history.resumeRecording();
|
|
|
|
}
|
2020-10-11 21:41:26 +05:30
|
|
|
|
2021-05-14 17:52:56 +05:30
|
|
|
if (sceneData.appState) {
|
|
|
|
this.setState(sceneData.appState);
|
|
|
|
}
|
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
|
|
|
|
2021-05-14 17:52:56 +05:30
|
|
|
if (sceneData.elements) {
|
|
|
|
this.scene.replaceAllElements(sceneData.elements);
|
|
|
|
}
|
2020-04-28 09:49:00 -07:00
|
|
|
|
2021-05-14 17:52:56 +05:30
|
|
|
if (sceneData.collaborators) {
|
|
|
|
this.setState({ collaborators: sceneData.collaborators });
|
|
|
|
}
|
2022-01-06 21:37:33 +05:30
|
|
|
|
|
|
|
if (sceneData.libraryItems) {
|
|
|
|
this.library.saveLibrary(
|
|
|
|
restoreLibraryItems(sceneData.libraryItems, "unpublished"),
|
|
|
|
);
|
|
|
|
}
|
2021-05-14 17:52:56 +05:30
|
|
|
},
|
|
|
|
);
|
2020-10-06 04:34:40 +02: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) => {
|
2021-04-18 13:33:05 +02:00
|
|
|
cursorX = event.clientX;
|
|
|
|
cursorY = event.clientY;
|
2020-03-16 19:07:47 -07:00
|
|
|
},
|
|
|
|
);
|
2020-03-07 10:20:38 -05:00
|
|
|
|
2020-03-22 10:24:50 -07:00
|
|
|
// Input handling
|
|
|
|
|
2021-04-13 01:29:25 +05:30
|
|
|
private onKeyDown = withBatchedUpdates(
|
|
|
|
(event: React.KeyboardEvent | KeyboardEvent) => {
|
|
|
|
// normalize `event.key` when CapsLock is pressed #2372
|
|
|
|
if (
|
|
|
|
"Proxy" in window &&
|
|
|
|
((!event.shiftKey && /^[A-Z]$/.test(event.key)) ||
|
|
|
|
(event.shiftKey && /^[a-z]$/.test(event.key)))
|
|
|
|
) {
|
|
|
|
event = new Proxy(event, {
|
|
|
|
get(ev: any, prop) {
|
|
|
|
const value = ev[prop];
|
|
|
|
if (typeof value === "function") {
|
|
|
|
// fix for Proxies hijacking `this`
|
|
|
|
return value.bind(ev);
|
|
|
|
}
|
|
|
|
return prop === "key"
|
|
|
|
? // CapsLock inverts capitalization based on ShiftKey, so invert
|
|
|
|
// it back
|
|
|
|
event.shiftKey
|
|
|
|
? ev.key.toUpperCase()
|
|
|
|
: ev.key.toLowerCase()
|
|
|
|
: value;
|
|
|
|
},
|
|
|
|
});
|
|
|
|
}
|
2020-03-07 10:20:38 -05:00
|
|
|
|
2021-04-13 01:29:25 +05:30
|
|
|
if (
|
2022-01-13 19:53:22 +01:00
|
|
|
(isWritableElement(event.target) && event.key !== KEYS.ESCAPE) ||
|
2021-04-13 01:29:25 +05:30
|
|
|
// case: using arrows to move between buttons
|
|
|
|
(isArrowKey(event.key) && isInputLike(event.target))
|
|
|
|
) {
|
|
|
|
return;
|
|
|
|
}
|
2020-04-05 15:58:00 +03:00
|
|
|
|
2021-04-13 01:29:25 +05:30
|
|
|
if (event.key === KEYS.QUESTION_MARK) {
|
|
|
|
this.setState({
|
|
|
|
showHelpDialog: true,
|
|
|
|
});
|
|
|
|
}
|
2020-06-24 00:24:52 +09:00
|
|
|
|
2021-04-13 01:29:25 +05:30
|
|
|
if (this.actionManager.handleKeyDown(event)) {
|
|
|
|
return;
|
|
|
|
}
|
2020-03-07 10:20:38 -05:00
|
|
|
|
2021-04-13 01:29:25 +05:30
|
|
|
if (this.state.viewModeEnabled) {
|
|
|
|
return;
|
|
|
|
}
|
2021-02-02 02:26:42 +05:30
|
|
|
|
2021-04-13 01:29:25 +05:30
|
|
|
if (event[KEYS.CTRL_OR_CMD] && this.state.isBindingEnabled) {
|
|
|
|
this.setState({ isBindingEnabled: false });
|
|
|
|
}
|
2020-07-10 02:20:23 -07:00
|
|
|
|
2021-10-21 22:05:48 +02:00
|
|
|
if (event.code === CODES.ZERO) {
|
2021-04-13 01:29:25 +05:30
|
|
|
this.setState({ isLibraryOpen: !this.state.isLibraryOpen });
|
2020-08-29 15:12:28 +03:00
|
|
|
}
|
|
|
|
|
2021-04-13 01:29:25 +05:30
|
|
|
if (isArrowKey(event.key)) {
|
|
|
|
const step =
|
|
|
|
(this.state.gridSize &&
|
|
|
|
(event.shiftKey
|
|
|
|
? ELEMENT_TRANSLATE_AMOUNT
|
|
|
|
: this.state.gridSize)) ||
|
|
|
|
(event.shiftKey
|
|
|
|
? ELEMENT_SHIFT_TRANSLATE_AMOUNT
|
|
|
|
: ELEMENT_TRANSLATE_AMOUNT);
|
|
|
|
|
2021-12-23 01:47:14 +05:30
|
|
|
const selectedElements = getSelectedElements(
|
|
|
|
this.scene.getElements(),
|
|
|
|
this.state,
|
|
|
|
true,
|
|
|
|
);
|
2021-04-13 01:29:25 +05:30
|
|
|
|
|
|
|
let offsetX = 0;
|
|
|
|
let offsetY = 0;
|
|
|
|
|
|
|
|
if (event.key === KEYS.ARROW_LEFT) {
|
|
|
|
offsetX = -step;
|
|
|
|
} else if (event.key === KEYS.ARROW_RIGHT) {
|
|
|
|
offsetX = step;
|
|
|
|
} else if (event.key === KEYS.ARROW_UP) {
|
|
|
|
offsetY = -step;
|
|
|
|
} else if (event.key === KEYS.ARROW_DOWN) {
|
|
|
|
offsetY = step;
|
|
|
|
}
|
|
|
|
|
|
|
|
selectedElements.forEach((element) => {
|
|
|
|
mutateElement(element, {
|
|
|
|
x: element.x + offsetX,
|
|
|
|
y: element.y + offsetY,
|
|
|
|
});
|
2020-08-29 15:12:28 +03:00
|
|
|
|
2021-04-13 01:29:25 +05:30
|
|
|
updateBoundElements(element, {
|
|
|
|
simultaneouslyUpdated: selectedElements,
|
|
|
|
});
|
2020-08-29 15:12:28 +03:00
|
|
|
});
|
|
|
|
|
2021-04-13 01:29:25 +05:30
|
|
|
this.maybeSuggestBindingForAll(selectedElements);
|
2020-09-13 18:17:16 +01:00
|
|
|
|
2021-04-13 01:29:25 +05:30
|
|
|
event.preventDefault();
|
|
|
|
} else if (event.key === KEYS.ENTER) {
|
|
|
|
const selectedElements = getSelectedElements(
|
|
|
|
this.scene.getElements(),
|
|
|
|
this.state,
|
|
|
|
);
|
2020-03-26 01:12:51 +09:00
|
|
|
|
2020-06-01 11:35:44 +02:00
|
|
|
if (
|
2021-04-13 01:29:25 +05:30
|
|
|
selectedElements.length === 1 &&
|
|
|
|
isLinearElement(selectedElements[0])
|
2020-06-01 11:35:44 +02:00
|
|
|
) {
|
2021-04-13 01:29:25 +05:30
|
|
|
if (
|
|
|
|
!this.state.editingLinearElement ||
|
|
|
|
this.state.editingLinearElement.elementId !== selectedElements[0].id
|
|
|
|
) {
|
2021-04-24 18:21:02 +05:30
|
|
|
this.history.resumeRecording();
|
2021-04-13 01:29:25 +05:30
|
|
|
this.setState({
|
|
|
|
editingLinearElement: new LinearElementEditor(
|
|
|
|
selectedElements[0],
|
|
|
|
this.scene,
|
|
|
|
),
|
|
|
|
});
|
|
|
|
}
|
|
|
|
} else if (
|
|
|
|
selectedElements.length === 1 &&
|
|
|
|
!isLinearElement(selectedElements[0])
|
|
|
|
) {
|
|
|
|
const selectedElement = selectedElements[0];
|
2021-12-16 21:14:03 +05:30
|
|
|
|
2021-04-13 01:29:25 +05:30
|
|
|
this.startTextEditing({
|
|
|
|
sceneX: selectedElement.x + selectedElement.width / 2,
|
|
|
|
sceneY: selectedElement.y + selectedElement.height / 2,
|
2021-12-16 21:14:03 +05:30
|
|
|
shouldBind: true,
|
2020-06-01 11:35:44 +02:00
|
|
|
});
|
2021-04-13 01:29:25 +05:30
|
|
|
event.preventDefault();
|
|
|
|
return;
|
2020-06-01 11:35:44 +02:00
|
|
|
}
|
|
|
|
} else if (
|
2021-04-13 01:29:25 +05:30
|
|
|
!event.ctrlKey &&
|
|
|
|
!event.altKey &&
|
|
|
|
!event.metaKey &&
|
|
|
|
this.state.draggingElement === null
|
2020-03-26 01:12:51 +09:00
|
|
|
) {
|
2021-04-13 01:29:25 +05:30
|
|
|
const shape = findShapeByKey(event.key);
|
|
|
|
if (shape) {
|
2022-03-28 14:46:40 +02:00
|
|
|
if (this.state.activeTool.type !== shape) {
|
|
|
|
trackEvent(
|
|
|
|
"toolbar",
|
|
|
|
shape,
|
|
|
|
`keyboard (${this.deviceType.isMobile ? "mobile" : "desktop"})`,
|
|
|
|
);
|
|
|
|
}
|
2022-03-29 17:10:19 +05:30
|
|
|
this.setActiveTool({ ...this.state.activeTool, type: shape });
|
2021-04-13 01:29:25 +05:30
|
|
|
} else if (event.key === KEYS.Q) {
|
2022-03-28 14:46:40 +02:00
|
|
|
this.toggleLock("keyboard");
|
2021-04-13 01:29:25 +05:30
|
|
|
}
|
2020-03-26 01:12:51 +09:00
|
|
|
}
|
2021-04-13 01:29:25 +05:30
|
|
|
if (event.key === KEYS.SPACE && gesture.pointers.size === 0) {
|
|
|
|
isHoldingSpace = true;
|
|
|
|
setCursor(this.canvas, CURSOR_TYPE.GRABBING);
|
2021-12-27 14:18:11 +01:00
|
|
|
event.preventDefault();
|
2020-03-22 10:24:50 -07:00
|
|
|
}
|
2021-05-28 17:22:42 +05:30
|
|
|
|
|
|
|
if (event.key === KEYS.G || event.key === KEYS.S) {
|
2021-07-10 23:05:00 +02:00
|
|
|
const selectedElements = getSelectedElements(
|
|
|
|
this.scene.getElements(),
|
|
|
|
this.state,
|
|
|
|
);
|
|
|
|
if (
|
2022-03-25 20:46:01 +05:30
|
|
|
this.state.activeTool.type === "selection" &&
|
2021-07-10 23:05:00 +02:00
|
|
|
!selectedElements.length
|
|
|
|
) {
|
2021-07-04 22:27:33 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (
|
|
|
|
event.key === KEYS.G &&
|
2022-03-25 20:46:01 +05:30
|
|
|
(hasBackground(this.state.activeTool.type) ||
|
2021-07-10 23:05:00 +02:00
|
|
|
selectedElements.some((element) => hasBackground(element.type)))
|
2021-07-04 22:27:33 +02:00
|
|
|
) {
|
|
|
|
this.setState({ openPopup: "backgroundColorPicker" });
|
|
|
|
}
|
|
|
|
if (event.key === KEYS.S) {
|
|
|
|
this.setState({ openPopup: "strokeColorPicker" });
|
2021-05-28 17:22:42 +05:30
|
|
|
}
|
|
|
|
}
|
2021-04-13 01:29:25 +05:30
|
|
|
},
|
|
|
|
);
|
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) {
|
2021-07-09 18:00:13 -04:00
|
|
|
if (this.state.viewModeEnabled) {
|
|
|
|
setCursor(this.canvas, CURSOR_TYPE.GRAB);
|
2022-03-25 20:46:01 +05:30
|
|
|
} else if (this.state.activeTool.type === "selection") {
|
2021-03-03 14:04:02 +01:00
|
|
|
resetCursor(this.canvas);
|
2020-03-22 10:24:50 -07:00
|
|
|
} else {
|
2022-03-15 20:56:39 +05:30
|
|
|
setCursorForShape(this.canvas, this.state);
|
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-09-13 18:17:16 +01:00
|
|
|
if (isArrowKey(event.key)) {
|
|
|
|
const selectedElements = getSelectedElements(
|
|
|
|
this.scene.getElements(),
|
|
|
|
this.state,
|
|
|
|
);
|
|
|
|
isBindingEnabled(this.state)
|
|
|
|
? bindOrUnbindSelectedElements(selectedElements)
|
|
|
|
: unbindLinearElements(selectedElements);
|
|
|
|
this.setState({ suggestedBindings: [] });
|
|
|
|
}
|
2020-03-22 10:24:50 -07:00
|
|
|
});
|
2020-03-07 10:20:38 -05:00
|
|
|
|
2022-03-25 20:46:01 +05:30
|
|
|
private setActiveTool(tool: AppState["activeTool"]) {
|
2020-03-07 10:20:38 -05:00
|
|
|
if (!isHoldingSpace) {
|
2022-03-15 20:56:39 +05:30
|
|
|
setCursorForShape(this.canvas, this.state);
|
2020-03-07 10:20:38 -05:00
|
|
|
}
|
|
|
|
if (isToolIcon(document.activeElement)) {
|
2021-04-13 01:29:25 +05:30
|
|
|
this.focusContainer();
|
2020-03-07 10:20:38 -05:00
|
|
|
}
|
2022-03-25 20:46:01 +05:30
|
|
|
if (!isLinearElementType(tool.type)) {
|
2020-08-08 21:04:15 -07:00
|
|
|
this.setState({ suggestedBindings: [] });
|
|
|
|
}
|
2022-03-25 20:46:01 +05:30
|
|
|
if (tool.type === "image") {
|
2021-10-21 22:05:48 +02:00
|
|
|
this.onImageAction();
|
|
|
|
}
|
2022-03-25 20:46:01 +05:30
|
|
|
if (tool.type !== "selection") {
|
2020-05-26 13:07:46 -07:00
|
|
|
this.setState({
|
2022-03-25 20:46:01 +05:30
|
|
|
activeTool: tool,
|
2020-05-26 13:07:46 -07:00
|
|
|
selectedElementIds: {},
|
|
|
|
selectedGroupIds: {},
|
|
|
|
editingGroupId: null,
|
|
|
|
});
|
2020-03-08 10:20:55 -07:00
|
|
|
} else {
|
2022-03-25 20:46:01 +05:30
|
|
|
this.setState({ activeTool: tool });
|
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-12-07 19:22:20 +01:00
|
|
|
this.setState({
|
|
|
|
selectedElementIds: {},
|
|
|
|
});
|
|
|
|
gesture.initialScale = this.state.zoom.value;
|
2020-03-16 19:07:47 -07:00
|
|
|
});
|
|
|
|
|
|
|
|
private onGestureChange = withBatchedUpdates((event: GestureEvent) => {
|
2020-03-07 10:20:38 -05:00
|
|
|
event.preventDefault();
|
2020-12-20 12:13:15 -08:00
|
|
|
|
|
|
|
// onGestureChange only has zoom factor but not the center.
|
|
|
|
// If we're on iPad or iPhone, then we recognize multi-touch and will
|
|
|
|
// zoom in at the right location on the touchMove handler already.
|
|
|
|
// On Macbook, we don't have those events so will zoom in at the
|
|
|
|
// current location instead.
|
2022-02-06 16:56:52 +01:00
|
|
|
if (gesture.pointers.size >= 2) {
|
2020-12-20 12:13:15 -08:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2020-12-20 12:07:58 -08:00
|
|
|
const initialScale = gesture.initialScale;
|
|
|
|
if (initialScale) {
|
2022-01-29 21:12:44 +01:00
|
|
|
this.setState((state) => ({
|
|
|
|
...getStateForZoom(
|
|
|
|
{
|
|
|
|
viewportX: cursorX,
|
|
|
|
viewportY: cursorY,
|
|
|
|
nextZoom: getNormalizedZoom(initialScale * event.scale),
|
|
|
|
},
|
|
|
|
state,
|
2020-12-20 12:07:58 -08:00
|
|
|
),
|
|
|
|
}));
|
|
|
|
}
|
2020-03-16 19:07:47 -07:00
|
|
|
});
|
|
|
|
|
|
|
|
private onGestureEnd = withBatchedUpdates((event: GestureEvent) => {
|
2020-03-07 10:20:38 -05:00
|
|
|
event.preventDefault();
|
2020-12-07 19:22:20 +01:00
|
|
|
this.setState({
|
|
|
|
previousSelectedElementIds: {},
|
|
|
|
selectedElementIds: this.state.previousSelectedElementIds,
|
|
|
|
});
|
|
|
|
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
|
|
|
) {
|
2021-12-16 21:14:03 +05:30
|
|
|
const updateElement = (
|
|
|
|
text: string,
|
|
|
|
originalText: string,
|
2022-01-11 16:36:08 +05:30
|
|
|
isDeleted: boolean,
|
2021-12-16 21:14:03 +05:30
|
|
|
) => {
|
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)) {
|
2022-01-17 17:35:35 +05:30
|
|
|
return updateTextElement(_element, {
|
|
|
|
text,
|
|
|
|
isDeleted,
|
|
|
|
originalText,
|
|
|
|
});
|
2020-04-03 14:16:14 +02:00
|
|
|
}
|
|
|
|
return _element;
|
|
|
|
}),
|
|
|
|
]);
|
|
|
|
};
|
|
|
|
|
2020-04-12 15:57:57 +02:00
|
|
|
textWysiwyg({
|
|
|
|
id: element.id,
|
2021-02-11 12:24:26 +01:00
|
|
|
canvas: this.canvas,
|
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,
|
|
|
|
);
|
2021-12-17 16:08:18 +05:30
|
|
|
return [
|
|
|
|
viewportX - this.state.offsetLeft,
|
|
|
|
viewportY - this.state.offsetTop,
|
|
|
|
];
|
2020-06-25 21:21:27 +02:00
|
|
|
},
|
2020-04-03 14:16:14 +02:00
|
|
|
onChange: withBatchedUpdates((text) => {
|
2022-01-17 17:35:35 +05:30
|
|
|
updateElement(text, text, false);
|
2020-09-08 12:03:49 -04:00
|
|
|
if (isNonDeletedElement(element)) {
|
|
|
|
updateBoundElements(element);
|
|
|
|
}
|
2020-04-03 14:16:14 +02:00
|
|
|
}),
|
2021-12-16 21:14:03 +05:30
|
|
|
onSubmit: withBatchedUpdates(({ text, viaKeyboard, originalText }) => {
|
2020-07-14 14:56:45 +03:00
|
|
|
const isDeleted = !text.trim();
|
2022-01-17 17:35:35 +05:30
|
|
|
updateElement(text, originalText, isDeleted);
|
2021-03-16 18:04:53 +01:00
|
|
|
// select the created text element only if submitting via keyboard
|
|
|
|
// (when submitting via click it should act as signal to deselect)
|
|
|
|
if (!isDeleted && viaKeyboard) {
|
2021-12-16 21:14:03 +05:30
|
|
|
const elementIdToSelect = element.containerId
|
|
|
|
? element.containerId
|
|
|
|
: element.id;
|
2020-07-14 14:56:45 +03:00
|
|
|
this.setState((prevState) => ({
|
|
|
|
selectedElementIds: {
|
|
|
|
...prevState.selectedElementIds,
|
2021-12-16 21:14:03 +05:30
|
|
|
[elementIdToSelect]: true,
|
2020-07-14 14:56:45 +03:00
|
|
|
},
|
|
|
|
}));
|
2021-06-21 08:31:49 -04:00
|
|
|
}
|
|
|
|
if (isDeleted) {
|
2020-08-08 21:04:15 -07:00
|
|
|
fixBindingsAfterDeletion(this.scene.getElements(), [element]);
|
2020-04-03 14:16:14 +02:00
|
|
|
}
|
2020-07-14 14:56:45 +03:00
|
|
|
if (!isDeleted || isExistingElement) {
|
2021-04-24 18:21:02 +05:30
|
|
|
this.history.resumeRecording();
|
2020-04-03 14:16:14 +02:00
|
|
|
}
|
2020-07-14 14:56:45 +03:00
|
|
|
|
|
|
|
this.setState({
|
|
|
|
draggingElement: null,
|
|
|
|
editingElement: null,
|
|
|
|
});
|
2022-03-29 17:10:19 +05:30
|
|
|
if (this.state.activeTool.locked) {
|
2022-03-15 20:56:39 +05:30
|
|
|
setCursorForShape(this.canvas, this.state);
|
2020-07-14 14:56:45 +03:00
|
|
|
}
|
2021-04-13 01:29:25 +05:30
|
|
|
|
|
|
|
this.focusContainer();
|
2020-04-03 14:16:14 +02:00
|
|
|
}),
|
2020-07-30 14:50:59 +05:30
|
|
|
element,
|
2021-04-23 21:11:18 +05:30
|
|
|
excalidrawContainer: this.excalidrawContainerRef.current,
|
2022-01-13 19:53:22 +01:00
|
|
|
app: this,
|
2020-04-03 14:16:14 +02:00
|
|
|
});
|
2020-04-11 17:10:56 +01:00
|
|
|
// deselect all other elements when inserting text
|
2021-06-16 19:01:16 +02:00
|
|
|
this.deselectElements();
|
|
|
|
|
|
|
|
// 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)
|
2022-01-17 17:35:35 +05:30
|
|
|
updateElement(element.text, element.originalText, false);
|
2021-06-16 19:01:16 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
private deselectElements() {
|
2020-05-26 13:07:46 -07:00
|
|
|
this.setState({
|
|
|
|
selectedElementIds: {},
|
|
|
|
selectedGroupIds: {},
|
|
|
|
editingGroupId: null,
|
|
|
|
});
|
2020-04-03 14:16:14 +02:00
|
|
|
}
|
|
|
|
|
2020-06-25 21:21:27 +02:00
|
|
|
private getTextElementAtPosition(
|
|
|
|
x: number,
|
|
|
|
y: number,
|
|
|
|
): NonDeleted<ExcalidrawTextElement> | null {
|
2021-12-16 21:14:03 +05:30
|
|
|
const element = this.getElementAtPosition(x, y, {
|
|
|
|
includeBoundTextElement: true,
|
|
|
|
});
|
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,
|
2021-05-29 22:33:53 +02:00
|
|
|
opts?: {
|
|
|
|
/** if true, returns the first selected element (with highest z-index)
|
|
|
|
of all hit elements */
|
|
|
|
preferSelected?: boolean;
|
2021-12-16 21:14:03 +05:30
|
|
|
includeBoundTextElement?: boolean;
|
2021-05-29 22:33:53 +02:00
|
|
|
},
|
2020-08-08 21:04:15 -07:00
|
|
|
): NonDeleted<ExcalidrawElement> | null {
|
2021-12-16 21:14:03 +05:30
|
|
|
const allHitElements = this.getElementsAtPosition(
|
|
|
|
x,
|
|
|
|
y,
|
|
|
|
opts?.includeBoundTextElement,
|
|
|
|
);
|
2020-08-26 17:37:44 +01:00
|
|
|
if (allHitElements.length > 1) {
|
2021-05-29 22:33:53 +02:00
|
|
|
if (opts?.preferSelected) {
|
|
|
|
for (let index = allHitElements.length - 1; index > -1; index--) {
|
|
|
|
if (this.state.selectedElementIds[allHitElements[index].id]) {
|
|
|
|
return allHitElements[index];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2020-08-26 17:37:44 +01:00
|
|
|
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,
|
2021-12-16 21:14:03 +05:30
|
|
|
includeBoundTextElement: boolean = false,
|
2020-08-26 17:37:44 +01:00
|
|
|
): NonDeleted<ExcalidrawElement>[] {
|
2021-12-16 21:14:03 +05:30
|
|
|
const elements = includeBoundTextElement
|
|
|
|
? this.scene.getElements()
|
|
|
|
: this.scene
|
|
|
|
.getElements()
|
|
|
|
.filter(
|
|
|
|
(element) => !(isTextElement(element) && element.containerId),
|
|
|
|
);
|
2022-02-03 20:34:59 +05:30
|
|
|
|
2021-12-16 21:14:03 +05:30
|
|
|
return getElementsAtPosition(elements, (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,
|
2021-12-16 21:14:03 +05:30
|
|
|
shouldBind,
|
2020-06-25 21:21:27 +02:00
|
|
|
insertAtParentCenter = true,
|
|
|
|
}: {
|
|
|
|
/** X position to insert text at */
|
|
|
|
sceneX: number;
|
|
|
|
/** Y position to insert text at */
|
|
|
|
sceneY: number;
|
2021-12-16 21:14:03 +05:30
|
|
|
shouldBind: boolean;
|
2020-06-25 21:21:27 +02:00
|
|
|
/** whether to attempt to insert at element center if applicable */
|
|
|
|
insertAtParentCenter?: boolean;
|
|
|
|
}) => {
|
2021-12-21 19:00:01 +05:30
|
|
|
let parentCenterPosition =
|
2020-06-25 21:21:27 +02:00
|
|
|
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
|
|
|
|
2022-03-02 17:04:09 +01:00
|
|
|
let existingTextElement: NonDeleted<ExcalidrawTextElement> | null = null;
|
|
|
|
let container: ExcalidrawTextContainer | null = null;
|
2021-12-16 21:14:03 +05:30
|
|
|
|
2022-03-02 17:04:09 +01:00
|
|
|
const selectedElements = getSelectedElements(
|
|
|
|
this.scene.getElements(),
|
|
|
|
this.state,
|
|
|
|
);
|
2021-12-16 21:14:03 +05:30
|
|
|
|
2022-03-02 17:04:09 +01:00
|
|
|
if (selectedElements.length === 1) {
|
|
|
|
if (isTextElement(selectedElements[0])) {
|
|
|
|
existingTextElement = selectedElements[0];
|
|
|
|
} else if (isTextBindableContainer(selectedElements[0])) {
|
|
|
|
container = selectedElements[0];
|
|
|
|
existingTextElement = getBoundTextElement(container);
|
2021-12-16 21:14:03 +05:30
|
|
|
}
|
|
|
|
}
|
2022-03-02 17:04:09 +01:00
|
|
|
|
|
|
|
existingTextElement =
|
|
|
|
existingTextElement ?? this.getTextElementAtPosition(sceneX, sceneY);
|
|
|
|
|
|
|
|
// bind to container when shouldBind is true or
|
|
|
|
// clicked on center of container
|
|
|
|
if (
|
|
|
|
!container &&
|
|
|
|
!existingTextElement &&
|
|
|
|
(shouldBind || parentCenterPosition)
|
|
|
|
) {
|
|
|
|
container = getTextBindableContainerAtPosition(
|
2022-03-22 15:32:49 +05:30
|
|
|
this.scene
|
|
|
|
.getElements()
|
|
|
|
.filter(
|
|
|
|
(ele) => isTextBindableContainer(ele) && !getBoundTextElement(ele),
|
|
|
|
),
|
2022-03-02 17:04:09 +01:00
|
|
|
sceneX,
|
|
|
|
sceneY,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2021-12-16 21:14:03 +05:30
|
|
|
if (!existingTextElement && container) {
|
|
|
|
const fontString = {
|
|
|
|
fontSize: this.state.currentItemFontSize,
|
|
|
|
fontFamily: this.state.currentItemFontFamily,
|
|
|
|
};
|
|
|
|
const minWidth = getApproxMinLineWidth(getFontString(fontString));
|
|
|
|
const minHeight = getApproxMinLineHeight(getFontString(fontString));
|
|
|
|
const newHeight = Math.max(container.height, minHeight);
|
|
|
|
const newWidth = Math.max(container.width, minWidth);
|
|
|
|
mutateElement(container, { height: newHeight, width: newWidth });
|
|
|
|
sceneX = container.x + newWidth / 2;
|
|
|
|
sceneY = container.y + newHeight / 2;
|
2021-12-21 19:00:01 +05:30
|
|
|
if (parentCenterPosition) {
|
|
|
|
parentCenterPosition = this.getTextWysiwygSnappedToCenterPosition(
|
|
|
|
sceneX,
|
|
|
|
sceneY,
|
|
|
|
this.state,
|
|
|
|
this.canvas,
|
|
|
|
window.devicePixelRatio,
|
|
|
|
);
|
|
|
|
}
|
2021-12-16 21:14:03 +05:30
|
|
|
}
|
|
|
|
|
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
|
2022-03-02 20:06:07 +05:30
|
|
|
? VERTICAL_ALIGN.MIDDLE
|
2020-06-25 21:21:27 +02:00
|
|
|
: DEFAULT_VERTICAL_ALIGN,
|
2021-12-16 21:14:03 +05:30
|
|
|
containerId: container?.id ?? undefined,
|
2021-12-31 14:55:02 +01:00
|
|
|
groupIds: container?.groupIds ?? [],
|
2020-06-25 21:21:27 +02:00
|
|
|
});
|
2020-03-07 10:20:38 -05:00
|
|
|
|
2020-06-25 21:21:27 +02:00
|
|
|
this.setState({ editingElement: element });
|
|
|
|
|
2022-03-02 20:06:07 +05:30
|
|
|
if (!existingTextElement) {
|
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
|
|
|
|
2022-03-02 01:07:12 -05:00
|
|
|
// case: creating new text not centered to parent element → offset Y
|
2020-11-05 19:06:18 +02:00
|
|
|
// so that the text is centered to cursor position
|
2020-06-25 21:21:27 +02:00
|
|
|
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
|
2020-11-05 19:06:18 +02:00
|
|
|
// text and enter multiElement mode
|
2020-03-26 01:12:51 +09:00
|
|
|
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
|
2022-03-25 20:46:01 +05:30
|
|
|
if (this.state.activeTool.type !== "selection") {
|
2020-07-27 14:05:52 -07:00
|
|
|
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
|
|
|
|
) {
|
2021-04-24 18:21:02 +05:30
|
|
|
this.history.resumeRecording();
|
2020-06-01 11:35:44 +02:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2021-03-03 14:04:02 +01:00
|
|
|
resetCursor(this.canvas);
|
2020-06-01 11:35:44 +02:00
|
|
|
|
2021-12-16 21:14:03 +05:30
|
|
|
let { x: sceneX, y: sceneY } = viewportCoordsToSceneCoords(
|
2020-03-26 01:12:51 +09:00
|
|
|
event,
|
|
|
|
this.state,
|
|
|
|
);
|
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-03-03 14:04:02 +01:00
|
|
|
resetCursor(this.canvas);
|
2021-03-23 16:06:16 +02:00
|
|
|
if (!event[KEYS.CTRL_OR_CMD] && !this.state.viewModeEnabled) {
|
2021-12-16 21:14:03 +05:30
|
|
|
const selectedElements = getSelectedElements(
|
|
|
|
this.scene.getElements(),
|
|
|
|
this.state,
|
|
|
|
);
|
|
|
|
if (selectedElements.length === 1) {
|
|
|
|
const selectedElement = selectedElements[0];
|
|
|
|
const canBindText = hasBoundTextElement(selectedElement);
|
|
|
|
if (canBindText) {
|
|
|
|
sceneX = selectedElement.x + selectedElement.width / 2;
|
|
|
|
sceneY = selectedElement.y + selectedElement.height / 2;
|
|
|
|
}
|
|
|
|
}
|
2020-08-27 20:59:46 +02:00
|
|
|
this.startTextEditing({
|
|
|
|
sceneX,
|
|
|
|
sceneY,
|
2021-12-16 21:14:03 +05:30
|
|
|
shouldBind: false,
|
2020-08-27 20:59:46 +02:00
|
|
|
insertAtParentCenter: !event.altKey,
|
|
|
|
});
|
|
|
|
}
|
2020-03-26 01:12:51 +09:00
|
|
|
};
|
|
|
|
|
2022-02-03 20:34:59 +05:30
|
|
|
private getElementLinkAtPosition = (
|
|
|
|
scenePointer: Readonly<{ x: number; y: number }>,
|
|
|
|
hitElement: NonDeletedExcalidrawElement | null,
|
|
|
|
): ExcalidrawElement | undefined => {
|
|
|
|
// Reversing so we traverse the elements in decreasing order
|
|
|
|
// of z-index
|
|
|
|
const elements = this.scene.getElements().slice().reverse();
|
|
|
|
let hitElementIndex = Infinity;
|
|
|
|
|
|
|
|
return elements.find((element, index) => {
|
|
|
|
if (hitElement && element.id === hitElement.id) {
|
|
|
|
hitElementIndex = index;
|
|
|
|
}
|
|
|
|
return (
|
|
|
|
element.link &&
|
2022-03-27 22:50:41 +01:00
|
|
|
index <= hitElementIndex &&
|
2022-02-07 19:54:39 +05:30
|
|
|
isPointHittingLinkIcon(
|
|
|
|
element,
|
|
|
|
this.state,
|
|
|
|
[scenePointer.x, scenePointer.y],
|
2022-03-16 15:59:30 +01:00
|
|
|
this.deviceType.isMobile,
|
2022-03-27 22:50:41 +01:00
|
|
|
)
|
2022-02-03 20:34:59 +05:30
|
|
|
);
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2022-02-10 10:22:33 +01:00
|
|
|
private redirectToLink = (
|
|
|
|
event: React.PointerEvent<HTMLCanvasElement>,
|
|
|
|
isTouchScreen: boolean,
|
|
|
|
) => {
|
|
|
|
const draggedDistance = distance2d(
|
|
|
|
this.lastPointerDown!.clientX,
|
|
|
|
this.lastPointerDown!.clientY,
|
|
|
|
this.lastPointerUp!.clientX,
|
|
|
|
this.lastPointerUp!.clientY,
|
|
|
|
);
|
2022-02-07 19:54:39 +05:30
|
|
|
if (
|
2022-02-08 11:25:35 +01:00
|
|
|
!this.hitLinkElement ||
|
2022-02-10 10:22:33 +01:00
|
|
|
// For touch screen allow dragging threshold else strict check
|
|
|
|
(isTouchScreen && draggedDistance > DRAGGING_THRESHOLD) ||
|
|
|
|
(!isTouchScreen && draggedDistance !== 0)
|
2022-02-07 19:54:39 +05:30
|
|
|
) {
|
|
|
|
return;
|
|
|
|
}
|
2022-02-03 20:34:59 +05:30
|
|
|
const lastPointerDownCoords = viewportCoordsToSceneCoords(
|
|
|
|
this.lastPointerDown!,
|
|
|
|
this.state,
|
|
|
|
);
|
|
|
|
const lastPointerDownHittingLinkIcon = isPointHittingLinkIcon(
|
2022-03-27 22:50:41 +01:00
|
|
|
this.hitLinkElement,
|
2022-02-03 20:34:59 +05:30
|
|
|
this.state,
|
|
|
|
[lastPointerDownCoords.x, lastPointerDownCoords.y],
|
2022-03-16 15:59:30 +01:00
|
|
|
this.deviceType.isMobile,
|
2022-02-03 20:34:59 +05:30
|
|
|
);
|
|
|
|
const lastPointerUpCoords = viewportCoordsToSceneCoords(
|
|
|
|
this.lastPointerUp!,
|
|
|
|
this.state,
|
|
|
|
);
|
2022-02-15 16:31:14 +05:30
|
|
|
const lastPointerUpHittingLinkIcon = isPointHittingLinkIcon(
|
2022-03-27 22:50:41 +01:00
|
|
|
this.hitLinkElement,
|
2022-02-03 20:34:59 +05:30
|
|
|
this.state,
|
|
|
|
[lastPointerUpCoords.x, lastPointerUpCoords.y],
|
2022-03-16 15:59:30 +01:00
|
|
|
this.deviceType.isMobile,
|
2022-02-03 20:34:59 +05:30
|
|
|
);
|
2022-02-15 16:31:14 +05:30
|
|
|
if (lastPointerDownHittingLinkIcon && lastPointerUpHittingLinkIcon) {
|
2022-02-08 11:25:35 +01:00
|
|
|
const url = this.hitLinkElement.link;
|
2022-02-03 20:34:59 +05:30
|
|
|
if (url) {
|
2022-02-08 11:25:35 +01:00
|
|
|
let customEvent;
|
|
|
|
if (this.props.onLinkOpen) {
|
|
|
|
customEvent = wrapEvent(EVENT.EXCALIDRAW_LINK, event.nativeEvent);
|
|
|
|
this.props.onLinkOpen(this.hitLinkElement, customEvent);
|
|
|
|
}
|
|
|
|
if (!customEvent?.defaultPrevented) {
|
|
|
|
const target = isLocalLink(url) ? "_self" : "_blank";
|
|
|
|
const newWindow = window.open(undefined, target);
|
|
|
|
// https://mathiasbynens.github.io/rel-noopener/
|
|
|
|
if (newWindow) {
|
|
|
|
newWindow.opener = null;
|
|
|
|
newWindow.location = normalizeLink(url);
|
|
|
|
}
|
2022-02-03 20:34:59 +05:30
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
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-12-20 12:07:58 -08:00
|
|
|
const initialScale = gesture.initialScale;
|
|
|
|
if (
|
|
|
|
gesture.pointers.size === 2 &&
|
|
|
|
gesture.lastCenter &&
|
|
|
|
initialScale &&
|
|
|
|
gesture.initialDistance
|
|
|
|
) {
|
2020-03-08 18:09:45 -07:00
|
|
|
const center = getCenter(gesture.pointers);
|
2020-12-20 12:07:58 -08:00
|
|
|
const deltaX = center.x - gesture.lastCenter.x;
|
|
|
|
const deltaY = center.y - gesture.lastCenter.y;
|
2020-03-08 18:09:45 -07:00
|
|
|
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()));
|
2022-02-02 14:31:38 +01:00
|
|
|
const scaleFactor =
|
2022-03-25 20:46:01 +05:30
|
|
|
this.state.activeTool.type === "freedraw" && this.state.penMode
|
2022-02-02 14:31:38 +01:00
|
|
|
? 1
|
|
|
|
: distance / gesture.initialDistance;
|
2020-03-07 10:20:38 -05:00
|
|
|
|
2022-01-29 21:12:44 +01:00
|
|
|
const nextZoom = scaleFactor
|
|
|
|
? getNormalizedZoom(initialScale * scaleFactor)
|
|
|
|
: this.state.zoom.value;
|
|
|
|
|
|
|
|
this.setState((state) => {
|
|
|
|
const zoomState = getStateForZoom(
|
|
|
|
{
|
|
|
|
viewportX: center.x,
|
|
|
|
viewportY: center.y,
|
|
|
|
nextZoom,
|
|
|
|
},
|
|
|
|
state,
|
|
|
|
);
|
|
|
|
|
|
|
|
return {
|
|
|
|
zoom: zoomState.zoom,
|
|
|
|
scrollX: zoomState.scrollX + deltaX / nextZoom,
|
|
|
|
scrollY: zoomState.scrollY + deltaY / nextZoom,
|
|
|
|
shouldCacheIgnoreZoom: true,
|
|
|
|
};
|
|
|
|
});
|
2020-03-28 16:59:36 -07:00
|
|
|
this.resetShouldCacheIgnoreZoomDebounced();
|
2020-03-08 18:09:45 -07:00
|
|
|
} else {
|
2021-11-01 15:24:05 +02:00
|
|
|
gesture.lastCenter =
|
|
|
|
gesture.initialDistance =
|
|
|
|
gesture.initialScale =
|
|
|
|
null;
|
2020-03-08 18:09:45 -07:00
|
|
|
}
|
2020-03-07 10:20:38 -05:00
|
|
|
|
2020-03-08 18:09:45 -07:00
|
|
|
if (isHoldingSpace || isPanning || isDraggingScrollBar) {
|
|
|
|
return;
|
|
|
|
}
|
2021-02-04 13:07:29 +01:00
|
|
|
|
2020-07-09 14:15:42 -07:00
|
|
|
const isPointerOverScrollBars = isOverScrollBars(
|
|
|
|
currentScrollBars,
|
2021-02-04 13:07:29 +01:00
|
|
|
event.clientX - this.state.offsetLeft,
|
|
|
|
event.clientY - this.state.offsetTop,
|
2020-07-09 14:15:42 -07:00
|
|
|
);
|
|
|
|
const isOverScrollBar = isPointerOverScrollBars.isOverEither;
|
2020-03-08 18:09:45 -07:00
|
|
|
if (!this.state.draggingElement && !this.state.multiElement) {
|
|
|
|
if (isOverScrollBar) {
|
2021-03-03 14:04:02 +01:00
|
|
|
resetCursor(this.canvas);
|
2020-03-08 18:09:45 -07:00
|
|
|
} else {
|
2022-03-15 20:56:39 +05:30
|
|
|
setCursorForShape(this.canvas, this.state);
|
2020-03-08 18:09:45 -07:00
|
|
|
}
|
|
|
|
}
|
2020-03-07 10:20:38 -05:00
|
|
|
|
2020-11-04 17:49:15 +00:00
|
|
|
const scenePointer = viewportCoordsToSceneCoords(event, this.state);
|
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: [] });
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-03-25 20:46:01 +05:30
|
|
|
if (isBindingElementType(this.state.activeTool.type)) {
|
2020-08-08 21:04:15 -07:00
|
|
|
// Hovering with a selected tool or creating new linear element via click
|
|
|
|
// and point
|
|
|
|
const { draggingElement } = this.state;
|
|
|
|
if (isBindingElement(draggingElement)) {
|
2021-12-13 13:35:07 +01:00
|
|
|
this.maybeSuggestBindingsForLinearElementAtCoords(
|
2020-08-08 21:04:15 -07:00
|
|
|
draggingElement,
|
2021-12-13 13:35:07 +01:00
|
|
|
[scenePointer],
|
2020-08-08 21:04:15 -07:00
|
|
|
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];
|
|
|
|
|
2022-03-15 20:56:39 +05:30
|
|
|
setCursorForShape(this.canvas, this.state);
|
2020-03-18 16:43:06 +01:00
|
|
|
|
|
|
|
if (lastPoint === lastCommittedPoint) {
|
|
|
|
// if we haven't yet created a temp point and we're beyond commit-zone
|
2020-11-05 19:06:18 +02:00
|
|
|
// threshold, add a point
|
2020-03-18 16:43:06 +01:00
|
|
|
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 {
|
2021-03-03 14:04:02 +01:00
|
|
|
setCursor(this.canvas, CURSOR_TYPE.POINTER);
|
2020-03-18 16:43:06 +01:00
|
|
|
// in this branch, we're inside the commit zone, and no uncommitted
|
2020-11-05 19:06:18 +02:00
|
|
|
// point exists. Thus do nothing (don't add/remove points).
|
2020-03-18 16:43:06 +01:00
|
|
|
}
|
2020-11-06 22:06:30 +02:00
|
|
|
} else if (
|
|
|
|
points.length > 2 &&
|
|
|
|
lastCommittedPoint &&
|
|
|
|
distance2d(
|
|
|
|
scenePointerX - rx,
|
|
|
|
scenePointerY - ry,
|
|
|
|
lastCommittedPoint[0],
|
|
|
|
lastCommittedPoint[1],
|
|
|
|
) < LINE_CONFIRM_THRESHOLD
|
|
|
|
) {
|
2021-03-03 14:04:02 +01:00
|
|
|
setCursor(this.canvas, CURSOR_TYPE.POINTER);
|
2020-11-06 22:06:30 +02:00
|
|
|
mutateElement(multiElement, {
|
|
|
|
points: points.slice(0, -1),
|
|
|
|
});
|
2020-03-18 16:43:06 +01:00
|
|
|
} else {
|
2021-02-14 14:43:23 +01:00
|
|
|
if (isPathALoop(points, this.state.zoom.value)) {
|
2021-03-03 14:04:02 +01:00
|
|
|
setCursor(this.canvas, CURSOR_TYPE.POINTER);
|
2020-03-18 16:43:06 +01:00
|
|
|
}
|
2020-11-06 22:06:30 +02:00
|
|
|
// update last uncommitted point
|
|
|
|
mutateElement(multiElement, {
|
|
|
|
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 ||
|
2022-03-25 20:46:01 +05:30
|
|
|
(this.state.activeTool.type !== "selection" &&
|
|
|
|
this.state.activeTool.type !== "text" &&
|
|
|
|
this.state.activeTool.type !== "eraser")
|
2020-04-06 22:26:54 +02:00
|
|
|
) {
|
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
|
|
|
|
) {
|
2021-03-03 14:04:02 +01:00
|
|
|
setCursor(
|
|
|
|
this.canvas,
|
|
|
|
getCursorForResizingElement(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) {
|
2021-03-03 14:04:02 +01:00
|
|
|
setCursor(
|
|
|
|
this.canvas,
|
|
|
|
getCursorForResizingElement({
|
|
|
|
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,
|
|
|
|
);
|
2022-02-03 20:34:59 +05:30
|
|
|
this.hitLinkElement = this.getElementLinkAtPosition(
|
|
|
|
scenePointer,
|
|
|
|
hitElement,
|
|
|
|
);
|
2022-03-15 20:56:39 +05:30
|
|
|
if (isEraserActive(this.state)) {
|
|
|
|
return;
|
|
|
|
}
|
2022-02-03 20:34:59 +05:30
|
|
|
if (
|
|
|
|
this.hitLinkElement &&
|
|
|
|
!this.state.selectedElementIds[this.hitLinkElement.id]
|
|
|
|
) {
|
|
|
|
setCursor(this.canvas, CURSOR_TYPE.POINTER);
|
|
|
|
showHyperlinkTooltip(this.hitLinkElement, this.state);
|
|
|
|
} else {
|
|
|
|
hideHyperlinkToolip();
|
2021-12-13 13:35:07 +01:00
|
|
|
if (
|
2022-02-03 20:34:59 +05:30
|
|
|
hitElement &&
|
|
|
|
hitElement.link &&
|
|
|
|
this.state.selectedElementIds[hitElement.id] &&
|
|
|
|
!this.contextMenuOpen &&
|
|
|
|
!this.state.showHyperlinkPopup
|
|
|
|
) {
|
|
|
|
this.setState({ showHyperlinkPopup: "info" });
|
2022-03-25 20:46:01 +05:30
|
|
|
} else if (this.state.activeTool.type === "text") {
|
2022-02-03 20:34:59 +05:30
|
|
|
setCursor(
|
|
|
|
this.canvas,
|
|
|
|
isTextElement(hitElement) ? CURSOR_TYPE.TEXT : CURSOR_TYPE.CROSSHAIR,
|
|
|
|
);
|
|
|
|
} else if (this.state.viewModeEnabled) {
|
|
|
|
setCursor(this.canvas, CURSOR_TYPE.GRAB);
|
|
|
|
} else if (isOverScrollBar) {
|
|
|
|
setCursor(this.canvas, CURSOR_TYPE.AUTO);
|
|
|
|
} else if (this.state.editingLinearElement) {
|
|
|
|
const element = LinearElementEditor.getElement(
|
|
|
|
this.state.editingLinearElement.elementId,
|
|
|
|
);
|
|
|
|
|
|
|
|
if (
|
|
|
|
element &&
|
|
|
|
isHittingElementNotConsideringBoundingBox(element, this.state, [
|
|
|
|
scenePointer.x,
|
|
|
|
scenePointer.y,
|
|
|
|
])
|
|
|
|
) {
|
|
|
|
setCursor(this.canvas, CURSOR_TYPE.MOVE);
|
|
|
|
} else {
|
|
|
|
setCursor(this.canvas, CURSOR_TYPE.AUTO);
|
|
|
|
}
|
|
|
|
} else if (
|
|
|
|
// if using cmd/ctrl, we're not dragging
|
|
|
|
!event[KEYS.CTRL_OR_CMD] &&
|
|
|
|
(hitElement ||
|
|
|
|
this.isHittingCommonBoundingBoxOfSelectedElements(
|
|
|
|
scenePointer,
|
|
|
|
selectedElements,
|
|
|
|
))
|
2021-12-13 13:35:07 +01:00
|
|
|
) {
|
|
|
|
setCursor(this.canvas, CURSOR_TYPE.MOVE);
|
|
|
|
} else {
|
|
|
|
setCursor(this.canvas, 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
|
|
|
|
2022-03-11 19:53:42 +05:30
|
|
|
private handleEraser = (
|
|
|
|
event: PointerEvent,
|
|
|
|
pointerDownState: PointerDownState,
|
|
|
|
scenePointer: { x: number; y: number },
|
|
|
|
) => {
|
|
|
|
const updateElementIds = (elements: ExcalidrawElement[]) => {
|
|
|
|
elements.forEach((element) => {
|
|
|
|
idsToUpdate.push(element.id);
|
|
|
|
if (event.altKey) {
|
2022-03-22 16:40:28 +05:30
|
|
|
if (
|
|
|
|
pointerDownState.elementIdsToErase[element.id] &&
|
|
|
|
pointerDownState.elementIdsToErase[element.id].erase
|
|
|
|
) {
|
|
|
|
pointerDownState.elementIdsToErase[element.id].erase = false;
|
2022-03-11 19:53:42 +05:30
|
|
|
}
|
2022-03-22 16:40:28 +05:30
|
|
|
} else if (!pointerDownState.elementIdsToErase[element.id]) {
|
|
|
|
pointerDownState.elementIdsToErase[element.id] = {
|
|
|
|
erase: true,
|
|
|
|
opacity: element.opacity,
|
|
|
|
};
|
2022-03-11 19:53:42 +05:30
|
|
|
}
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
const idsToUpdate: Array<string> = [];
|
|
|
|
|
|
|
|
const distance = distance2d(
|
|
|
|
pointerDownState.lastCoords.x,
|
|
|
|
pointerDownState.lastCoords.y,
|
|
|
|
scenePointer.x,
|
|
|
|
scenePointer.y,
|
|
|
|
);
|
|
|
|
const threshold = 10 / this.state.zoom.value;
|
|
|
|
const point = { ...pointerDownState.lastCoords };
|
|
|
|
let samplingInterval = 0;
|
|
|
|
while (samplingInterval <= distance) {
|
|
|
|
const hitElements = this.getElementsAtPosition(point.x, point.y);
|
|
|
|
updateElementIds(hitElements);
|
|
|
|
|
|
|
|
// Exit since we reached current point
|
|
|
|
if (samplingInterval === distance) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Calculate next point in the line at a distance of sampling interval
|
|
|
|
samplingInterval = Math.min(samplingInterval + threshold, distance);
|
|
|
|
|
|
|
|
const distanceRatio = samplingInterval / distance;
|
|
|
|
const nextX =
|
|
|
|
(1 - distanceRatio) * point.x + distanceRatio * scenePointer.x;
|
|
|
|
const nextY =
|
|
|
|
(1 - distanceRatio) * point.y + distanceRatio * scenePointer.y;
|
|
|
|
point.x = nextX;
|
|
|
|
point.y = nextY;
|
|
|
|
}
|
|
|
|
|
|
|
|
const elements = this.scene.getElements().map((ele) => {
|
|
|
|
const id =
|
|
|
|
isBoundToContainer(ele) && idsToUpdate.includes(ele.containerId)
|
|
|
|
? ele.containerId
|
|
|
|
: ele.id;
|
|
|
|
if (idsToUpdate.includes(id)) {
|
|
|
|
if (event.altKey) {
|
2022-03-22 16:40:28 +05:30
|
|
|
if (
|
|
|
|
pointerDownState.elementIdsToErase[id] &&
|
|
|
|
pointerDownState.elementIdsToErase[id].erase === false
|
|
|
|
) {
|
2022-03-11 19:53:42 +05:30
|
|
|
return newElementWith(ele, {
|
2022-03-22 16:40:28 +05:30
|
|
|
opacity: pointerDownState.elementIdsToErase[id].opacity,
|
2022-03-11 19:53:42 +05:30
|
|
|
});
|
|
|
|
}
|
|
|
|
} else {
|
2022-03-22 16:40:28 +05:30
|
|
|
return newElementWith(ele, {
|
|
|
|
opacity: ELEMENT_READY_TO_ERASE_OPACITY,
|
|
|
|
});
|
2022-03-11 19:53:42 +05:30
|
|
|
}
|
|
|
|
}
|
|
|
|
return ele;
|
|
|
|
});
|
|
|
|
|
|
|
|
this.scene.replaceAllElements(elements);
|
|
|
|
|
|
|
|
pointerDownState.lastCoords.x = scenePointer.x;
|
|
|
|
pointerDownState.lastCoords.y = scenePointer.y;
|
|
|
|
};
|
2020-07-02 22:12:56 +01:00
|
|
|
// set touch moving for mobile context menu
|
|
|
|
private handleTouchMove = (event: React.TouchEvent<HTMLCanvasElement>) => {
|
2020-12-05 15:06:50 -08:00
|
|
|
invalidateContextMenu = true;
|
2020-07-02 22:12:56 +01:00
|
|
|
};
|
|
|
|
|
2020-03-08 18:09:45 -07:00
|
|
|
private handleCanvasPointerDown = (
|
|
|
|
event: React.PointerEvent<HTMLCanvasElement>,
|
|
|
|
) => {
|
2021-03-19 18:36:23 +01:00
|
|
|
// remove any active selection when we start to interact with canvas
|
|
|
|
// (mainly, we care about removing selection outside the component which
|
|
|
|
// would prevent our copy handling otherwise)
|
|
|
|
const selection = document.getSelection();
|
|
|
|
if (selection?.anchorNode) {
|
|
|
|
selection.removeAllRanges();
|
|
|
|
}
|
2020-07-08 22:07:51 -07:00
|
|
|
this.maybeOpenContextMenuAfterPointerDownOnTouchDevices(event);
|
|
|
|
this.maybeCleanupAfterMissingPointerUp(event);
|
2020-03-07 10:20:38 -05:00
|
|
|
|
2022-02-02 14:31:38 +01:00
|
|
|
//fires only once, if pen is detected, penMode is enabled
|
|
|
|
//the user can disable this by toggling the penMode button
|
|
|
|
if (!this.state.penDetected && event.pointerType === "pen") {
|
|
|
|
this.setState((prevState) => {
|
|
|
|
return {
|
|
|
|
penMode: true,
|
|
|
|
penDetected: true,
|
|
|
|
};
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2022-03-16 15:59:30 +01:00
|
|
|
if (
|
|
|
|
!this.deviceType.isTouchScreen &&
|
|
|
|
["pen", "touch"].includes(event.pointerType)
|
|
|
|
) {
|
|
|
|
this.deviceType = updateObject(this.deviceType, { isTouchScreen: true });
|
|
|
|
}
|
|
|
|
|
2020-03-08 18:09:45 -07:00
|
|
|
if (isPanning) {
|
|
|
|
return;
|
|
|
|
}
|
2022-03-11 19:53:42 +05:30
|
|
|
|
2022-02-03 20:34:59 +05:30
|
|
|
this.lastPointerDown = event;
|
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
|
|
|
// 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
|
|
|
|
2022-02-03 20:34:59 +05:30
|
|
|
// Since context menu closes on pointer down so setting to false
|
|
|
|
this.contextMenuOpen = false;
|
2020-07-09 09:30:38 -07:00
|
|
|
this.clearSelectionIfNotUsingSelection();
|
2020-09-03 16:12:01 +02:00
|
|
|
this.updateBindingEnabledOnPointerMove(event);
|
2020-06-02 18:41:40 +02:00
|
|
|
|
2020-07-09 09:30:38 -07:00
|
|
|
if (this.handleSelectionOnPointerDown(event, pointerDownState)) {
|
|
|
|
return;
|
2022-02-02 14:31:38 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
const allowOnPointerDown =
|
|
|
|
!this.state.penMode ||
|
|
|
|
event.pointerType !== "touch" ||
|
2022-03-25 20:46:01 +05:30
|
|
|
this.state.activeTool.type === "selection" ||
|
|
|
|
this.state.activeTool.type === "text" ||
|
|
|
|
this.state.activeTool.type === "image";
|
2022-02-02 14:31:38 +01:00
|
|
|
|
|
|
|
if (!allowOnPointerDown) {
|
|
|
|
return;
|
2020-03-08 18:09:45 -07:00
|
|
|
}
|
2020-03-07 10:20:38 -05:00
|
|
|
|
2022-03-25 20:46:01 +05:30
|
|
|
if (this.state.activeTool.type === "text") {
|
2020-07-09 09:30:38 -07:00
|
|
|
this.handleTextOnPointerDown(event, pointerDownState);
|
2020-03-08 18:09:45 -07:00
|
|
|
return;
|
|
|
|
} else if (
|
2022-03-25 20:46:01 +05:30
|
|
|
this.state.activeTool.type === "arrow" ||
|
|
|
|
this.state.activeTool.type === "line"
|
2020-03-08 18:09:45 -07:00
|
|
|
) {
|
2020-07-09 09:30:38 -07:00
|
|
|
this.handleLinearElementOnPointerDown(
|
|
|
|
event,
|
2022-03-25 20:46:01 +05:30
|
|
|
this.state.activeTool.type,
|
2020-07-09 09:30:38 -07:00
|
|
|
pointerDownState,
|
|
|
|
);
|
2022-03-25 20:46:01 +05:30
|
|
|
} else if (this.state.activeTool.type === "image") {
|
2021-10-21 22:05:48 +02:00
|
|
|
// reset image preview on pointerdown
|
|
|
|
setCursor(this.canvas, CURSOR_TYPE.CROSSHAIR);
|
|
|
|
|
|
|
|
if (!this.state.pendingImageElement) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.setState({
|
|
|
|
draggingElement: this.state.pendingImageElement,
|
|
|
|
editingElement: this.state.pendingImageElement,
|
|
|
|
pendingImageElement: null,
|
|
|
|
multiElement: null,
|
|
|
|
});
|
|
|
|
|
|
|
|
const { x, y } = viewportCoordsToSceneCoords(event, this.state);
|
|
|
|
mutateElement(this.state.pendingImageElement, {
|
|
|
|
x,
|
|
|
|
y,
|
|
|
|
});
|
2022-03-25 20:46:01 +05:30
|
|
|
} else if (this.state.activeTool.type === "freedraw") {
|
2021-05-09 16:42:10 +01:00
|
|
|
this.handleFreeDrawElementOnPointerDown(
|
|
|
|
event,
|
2022-03-25 20:46:01 +05:30
|
|
|
this.state.activeTool.type,
|
2021-05-09 16:42:10 +01:00
|
|
|
pointerDownState,
|
|
|
|
);
|
2022-03-25 20:46:01 +05:30
|
|
|
} else if (this.state.activeTool.type !== "eraser") {
|
2020-07-09 09:30:38 -07:00
|
|
|
this.createGenericElementOnPointerDown(
|
2022-03-25 20:46:01 +05:30
|
|
|
this.state.activeTool.type,
|
2020-07-09 09:30:38 -07:00
|
|
|
pointerDownState,
|
|
|
|
);
|
2020-03-08 18:09:45 -07:00
|
|
|
}
|
2020-03-07 10:20:38 -05:00
|
|
|
|
2021-11-01 15:24:05 +02:00
|
|
|
const onPointerMove =
|
|
|
|
this.onPointerMoveFromPointerDownHandler(pointerDownState);
|
2020-07-09 14:15:42 -07:00
|
|
|
|
2021-11-01 15:24:05 +02:00
|
|
|
const onPointerUp =
|
|
|
|
this.onPointerUpFromPointerDownHandler(pointerDownState);
|
2020-07-09 14:15:42 -07:00
|
|
|
|
2020-12-06 22:39:31 +00:00
|
|
|
const onKeyDown = this.onKeyDownFromPointerDownHandler(pointerDownState);
|
|
|
|
const onKeyUp = this.onKeyUpFromPointerDownHandler(pointerDownState);
|
|
|
|
|
2020-07-09 14:15:42 -07:00
|
|
|
lastPointerUp = onPointerUp;
|
|
|
|
|
2021-02-02 02:26:42 +05:30
|
|
|
if (!this.state.viewModeEnabled) {
|
|
|
|
window.addEventListener(EVENT.POINTER_MOVE, onPointerMove);
|
|
|
|
window.addEventListener(EVENT.POINTER_UP, onPointerUp);
|
|
|
|
window.addEventListener(EVENT.KEYDOWN, onKeyDown);
|
|
|
|
window.addEventListener(EVENT.KEYUP, onKeyUp);
|
|
|
|
pointerDownState.eventListeners.onMove = onPointerMove;
|
|
|
|
pointerDownState.eventListeners.onUp = onPointerUp;
|
|
|
|
pointerDownState.eventListeners.onKeyUp = onKeyUp;
|
|
|
|
pointerDownState.eventListeners.onKeyDown = onKeyDown;
|
|
|
|
}
|
2020-07-09 14:15:42 -07:00
|
|
|
};
|
|
|
|
|
2022-02-07 13:20:19 +01:00
|
|
|
private handleCanvasPointerUp = (
|
|
|
|
event: React.PointerEvent<HTMLCanvasElement>,
|
|
|
|
) => {
|
|
|
|
this.lastPointerUp = event;
|
2022-03-16 15:59:30 +01:00
|
|
|
if (this.deviceType.isTouchScreen) {
|
2022-03-11 15:45:59 +01:00
|
|
|
const scenePointer = viewportCoordsToSceneCoords(
|
|
|
|
{ clientX: event.clientX, clientY: event.clientY },
|
|
|
|
this.state,
|
|
|
|
);
|
|
|
|
const hitElement = this.getElementAtPosition(
|
|
|
|
scenePointer.x,
|
|
|
|
scenePointer.y,
|
|
|
|
);
|
|
|
|
this.hitLinkElement = this.getElementLinkAtPosition(
|
|
|
|
scenePointer,
|
|
|
|
hitElement,
|
|
|
|
);
|
|
|
|
}
|
2022-02-07 13:20:19 +01:00
|
|
|
if (
|
|
|
|
this.hitLinkElement &&
|
|
|
|
!this.state.selectedElementIds[this.hitLinkElement.id]
|
|
|
|
) {
|
2022-03-16 15:59:30 +01:00
|
|
|
this.redirectToLink(event, this.deviceType.isTouchScreen);
|
2022-02-07 13:20:19 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
this.removePointer(event);
|
|
|
|
};
|
|
|
|
|
2020-07-09 14:15:42 -07:00
|
|
|
private maybeOpenContextMenuAfterPointerDownOnTouchDevices = (
|
|
|
|
event: React.PointerEvent<HTMLCanvasElement>,
|
|
|
|
): void => {
|
|
|
|
// deal with opening context menu on touch devices
|
|
|
|
if (event.pointerType === "touch") {
|
2020-12-05 15:06:50 -08:00
|
|
|
invalidateContextMenu = false;
|
|
|
|
|
|
|
|
if (touchTimeout) {
|
|
|
|
// If there's already a touchTimeout, this means that there's another
|
|
|
|
// touch down and we are doing another touch, so we shouldn't open the
|
|
|
|
// context menu.
|
|
|
|
invalidateContextMenu = true;
|
|
|
|
} else {
|
|
|
|
// open the context menu with the first touch's clientX and clientY
|
|
|
|
// if the touch is not moving
|
|
|
|
touchTimeout = window.setTimeout(() => {
|
|
|
|
touchTimeout = 0;
|
|
|
|
if (!invalidateContextMenu) {
|
2021-03-26 16:41:57 +01:00
|
|
|
this.handleCanvasContextMenu(event);
|
2020-12-05 15:06:50 -08:00
|
|
|
}
|
|
|
|
}, TOUCH_CTX_MENU_TIMEOUT);
|
|
|
|
}
|
2020-07-09 14:15:42 -07:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2022-02-09 20:42:02 +01:00
|
|
|
private resetContextMenuTimer = () => {
|
|
|
|
clearTimeout(touchTimeout);
|
|
|
|
touchTimeout = 0;
|
|
|
|
invalidateContextMenu = false;
|
|
|
|
};
|
|
|
|
|
2020-07-09 14:15:42 -07:00
|
|
|
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 ||
|
2021-02-02 02:26:42 +05:30
|
|
|
(event.button === POINTER_BUTTON.MAIN && isHoldingSpace) ||
|
|
|
|
this.state.viewModeEnabled)
|
2022-01-09 04:20:25 +05:30
|
|
|
) ||
|
|
|
|
isTextElement(this.state.editingElement)
|
2020-07-09 14:15:42 -07:00
|
|
|
) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
isPanning = true;
|
2021-12-27 14:18:11 +01:00
|
|
|
event.preventDefault();
|
2020-07-09 14:15:42 -07:00
|
|
|
|
|
|
|
let nextPastePrevented = false;
|
|
|
|
const isLinux = /Linux/.test(window.navigator.platform);
|
2020-04-11 13:37:43 +02:00
|
|
|
|
2021-03-03 14:04:02 +01:00
|
|
|
setCursor(this.canvas, CURSOR_TYPE.GRABBING);
|
2020-07-09 14:15:42 -07:00
|
|
|
let { clientX: lastX, clientY: lastY } = event;
|
2022-02-06 17:45:37 +01:00
|
|
|
const onPointerMove = withBatchedUpdatesThrottled((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:
|
2022-03-02 01:07:12 -05:00
|
|
|
* - right click paste
|
2020-07-09 14:15:42 -07:00
|
|
|
* - 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({
|
2021-01-31 10:47:43 +01:00
|
|
|
scrollX: this.state.scrollX - deltaX / this.state.zoom.value,
|
|
|
|
scrollY: this.state.scrollY - deltaY / this.state.zoom.value,
|
2020-07-09 14:15:42 -07:00
|
|
|
});
|
|
|
|
});
|
|
|
|
const teardown = withBatchedUpdates(
|
|
|
|
(lastPointerUp = () => {
|
|
|
|
lastPointerUp = null;
|
|
|
|
isPanning = false;
|
|
|
|
if (!isHoldingSpace) {
|
2021-07-09 18:00:13 -04:00
|
|
|
if (this.state.viewModeEnabled) {
|
|
|
|
setCursor(this.canvas, CURSOR_TYPE.GRAB);
|
|
|
|
} else {
|
2022-03-15 20:56:39 +05:30
|
|
|
setCursorForShape(this.canvas, this.state);
|
2021-07-09 18:00:13 -04:00
|
|
|
}
|
2020-07-09 14:15:42 -07:00
|
|
|
}
|
|
|
|
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);
|
2022-02-06 17:45:37 +01:00
|
|
|
onPointerMove.flush();
|
2020-07-09 14:15:42 -07:00
|
|
|
}),
|
|
|
|
);
|
|
|
|
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);
|
2020-11-04 17:49:15 +00:00
|
|
|
gesture.initialScale = this.state.zoom.value;
|
2020-07-09 14:15:42 -07:00
|
|
|
gesture.initialDistance = getDistance(
|
|
|
|
Array.from(gesture.pointers.values()),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private initialPointerDownState(
|
|
|
|
event: React.PointerEvent<HTMLCanvasElement>,
|
|
|
|
): PointerDownState {
|
2020-11-04 17:49:15 +00:00
|
|
|
const origin = viewportCoordsToSceneCoords(event, this.state);
|
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,
|
2021-05-20 22:28:34 +02:00
|
|
|
withCmdOrCtrl: event[KEYS.CTRL_OR_CMD],
|
2020-07-09 14:15:42 -07:00
|
|
|
originInGrid: tupleToCoors(
|
|
|
|
getGridPoint(origin.x, origin.y, this.state.gridSize),
|
|
|
|
),
|
|
|
|
scrollbars: isOverScrollBars(
|
|
|
|
currentScrollBars,
|
2021-02-04 13:07:29 +01:00
|
|
|
event.clientX - this.state.offsetLeft,
|
|
|
|
event.clientY - this.state.offsetTop,
|
2020-07-09 14:15:42 -07:00
|
|
|
),
|
|
|
|
// we need to duplicate because we'll be updating this state
|
|
|
|
lastCoords: { ...origin },
|
2020-09-11 17:22:40 +02:00
|
|
|
originalElements: this.scene.getElements().reduce((acc, element) => {
|
2020-12-06 22:39:31 +00:00
|
|
|
acc.set(element.id, deepCopyElement(element));
|
2020-09-11 17:22:40 +02:00
|
|
|
return acc;
|
|
|
|
}, new Map() as PointerDownState["originalElements"]),
|
2020-07-09 14:15:42 -07:00
|
|
|
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 },
|
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,
|
2021-11-01 15:24:05 +02:00
|
|
|
hasHitCommonBoundingBoxOfSelectedElements:
|
|
|
|
this.isHittingCommonBoundingBoxOfSelectedElements(
|
|
|
|
origin,
|
|
|
|
selectedElements,
|
|
|
|
),
|
2021-12-13 13:35:07 +01:00
|
|
|
hasHitElementInside: false,
|
2020-07-09 14:15:42 -07:00
|
|
|
},
|
|
|
|
drag: {
|
|
|
|
hasOccurred: false,
|
|
|
|
offset: null,
|
|
|
|
},
|
|
|
|
eventListeners: {
|
|
|
|
onMove: null,
|
|
|
|
onUp: null,
|
2020-12-06 22:39:31 +00:00
|
|
|
onKeyUp: null,
|
|
|
|
onKeyDown: null,
|
2020-07-09 14:15:42 -07:00
|
|
|
},
|
2021-12-13 13:35:07 +01:00
|
|
|
boxSelection: {
|
|
|
|
hasOccurred: false,
|
|
|
|
},
|
2022-03-11 19:53:42 +05:30
|
|
|
elementIdsToErase: {},
|
2020-07-09 14:15:42 -07:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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;
|
2022-02-06 17:45:37 +01:00
|
|
|
const onPointerMove = withBatchedUpdatesThrottled((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;
|
2022-03-15 20:56:39 +05:30
|
|
|
setCursorForShape(this.canvas, this.state);
|
2020-07-09 14:15:42 -07:00
|
|
|
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);
|
2022-02-06 17:45:37 +01:00
|
|
|
onPointerMove.flush();
|
2020-07-09 14:15:42 -07:00
|
|
|
});
|
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 => {
|
2022-03-25 20:46:01 +05:30
|
|
|
if (this.state.activeTool.type !== "selection") {
|
2020-07-09 14:15:42 -07:00
|
|
|
this.setState({
|
|
|
|
selectedElementIds: {},
|
|
|
|
selectedGroupIds: {},
|
|
|
|
editingGroupId: null,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2020-08-27 20:59:46 +02:00
|
|
|
/**
|
|
|
|
* @returns whether the pointer event has been completely handled
|
|
|
|
*/
|
2020-07-09 14:15:42 -07:00
|
|
|
private handleSelectionOnPointerDown = (
|
|
|
|
event: React.PointerEvent<HTMLCanvasElement>,
|
|
|
|
pointerDownState: PointerDownState,
|
|
|
|
): boolean => {
|
2022-03-25 20:46:01 +05:30
|
|
|
if (this.state.activeTool.type === "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) {
|
2021-11-01 15:24:05 +02:00
|
|
|
const elementWithTransformHandleType =
|
|
|
|
getElementWithTransformHandleType(
|
|
|
|
elements,
|
|
|
|
this.state,
|
|
|
|
pointerDownState.origin.x,
|
|
|
|
pointerDownState.origin.y,
|
|
|
|
this.state.zoom,
|
|
|
|
event.pointerType,
|
|
|
|
);
|
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) {
|
2021-03-03 14:04:02 +01:00
|
|
|
setCursor(
|
|
|
|
this.canvas,
|
|
|
|
getCursorForResizingElement({
|
|
|
|
transformHandleType: pointerDownState.resize.handleType,
|
|
|
|
}),
|
|
|
|
);
|
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),
|
2021-04-24 18:21:02 +05:30
|
|
|
this.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
|
|
|
|
2021-12-13 13:35:07 +01:00
|
|
|
if (pointerDownState.hit.element) {
|
2022-02-03 20:34:59 +05:30
|
|
|
// Early return if pointer is hitting link icon
|
|
|
|
if (
|
2022-02-07 19:54:39 +05:30
|
|
|
isPointHittingLinkIcon(
|
|
|
|
pointerDownState.hit.element,
|
|
|
|
this.state,
|
|
|
|
[pointerDownState.origin.x, pointerDownState.origin.y],
|
2022-03-16 15:59:30 +01:00
|
|
|
this.deviceType.isMobile,
|
2022-02-07 19:54:39 +05:30
|
|
|
)
|
2022-02-03 20:34:59 +05:30
|
|
|
) {
|
|
|
|
return false;
|
|
|
|
}
|
2021-12-13 13:35:07 +01:00
|
|
|
pointerDownState.hit.hasHitElementInside =
|
|
|
|
isHittingElementNotConsideringBoundingBox(
|
|
|
|
pointerDownState.hit.element,
|
|
|
|
this.state,
|
|
|
|
[pointerDownState.origin.x, pointerDownState.origin.y],
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
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;
|
2021-11-01 15:24:05 +02:00
|
|
|
const someHitElementIsSelected =
|
|
|
|
pointerDownState.hit.allHitElements.some((element) =>
|
|
|
|
this.isASelectedElement(element),
|
|
|
|
);
|
2020-08-26 17:37:44 +01:00
|
|
|
if (
|
|
|
|
(hitElement === null || !someHitElementIsSelected) &&
|
|
|
|
!event.shiftKey &&
|
|
|
|
!pointerDownState.hit.hasHitCommonBoundingBoxOfSelectedElements
|
|
|
|
) {
|
|
|
|
this.clearSelection(hitElement);
|
|
|
|
}
|
|
|
|
|
2021-12-13 13:35:07 +01:00
|
|
|
if (this.state.editingLinearElement) {
|
|
|
|
this.setState({
|
|
|
|
selectedElementIds: {
|
|
|
|
[this.state.editingLinearElement.elementId]: true,
|
|
|
|
},
|
|
|
|
});
|
|
|
|
// If we click on something
|
|
|
|
} else if (hitElement != null) {
|
2020-08-27 20:59:46 +02:00
|
|
|
// on CMD/CTRL, drill down to hit element regardless of groups etc.
|
|
|
|
if (event[KEYS.CTRL_OR_CMD]) {
|
2021-05-20 22:28:34 +02:00
|
|
|
if (!this.state.selectedElementIds[hitElement.id]) {
|
|
|
|
pointerDownState.hit.wasAddedToSelection = true;
|
|
|
|
}
|
2020-08-27 20:59:46 +02:00
|
|
|
this.setState((prevState) => ({
|
|
|
|
...editGroupForSelectedElement(prevState, hitElement),
|
|
|
|
previousSelectedElementIds: this.state.selectedElementIds,
|
|
|
|
}));
|
|
|
|
// mark as not completely handled so as to allow dragging etc.
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2020-07-09 14:15:42 -07:00
|
|
|
// 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]) {
|
2020-12-26 02:34:47 +08:00
|
|
|
// if we are currently editing a group, exiting editing mode and deselect the group.
|
2020-07-09 14:15:42 -07:00
|
|
|
if (
|
|
|
|
this.state.editingGroupId &&
|
|
|
|
!isElementInGroup(hitElement, this.state.editingGroupId)
|
|
|
|
) {
|
|
|
|
this.setState({
|
|
|
|
selectedElementIds: {},
|
|
|
|
selectedGroupIds: {},
|
|
|
|
editingGroupId: null,
|
|
|
|
});
|
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
|
2020-11-05 19:06:18 +02:00
|
|
|
// SHIFT the previously selected element(s) were deselected above
|
|
|
|
// (make sure you use setState updater to use latest state)
|
2020-08-26 17:37:44 +01:00
|
|
|
if (
|
|
|
|
!someHitElementIsSelected &&
|
|
|
|
!pointerDownState.hit.hasHitCommonBoundingBoxOfSelectedElements
|
|
|
|
) {
|
|
|
|
this.setState((prevState) => {
|
|
|
|
return selectGroupsForSelectedElements(
|
|
|
|
{
|
|
|
|
...prevState,
|
|
|
|
selectedElementIds: {
|
|
|
|
...prevState.selectedElementIds,
|
2020-08-27 20:59:46 +02:00
|
|
|
[hitElement.id]: true,
|
2020-08-26 17:37:44 +01:00
|
|
|
},
|
2022-02-03 20:34:59 +05:30
|
|
|
showHyperlinkPopup: hitElement.link ? "info" : false,
|
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
|
|
|
this.setState({
|
2020-08-27 20:59:46 +02:00
|
|
|
previousSelectedElementIds: this.state.selectedElementIds,
|
2020-07-09 14:15:42 -07:00
|
|
|
});
|
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
|
2020-11-04 17:49:15 +00:00
|
|
|
const threshold = 10 / this.state.zoom.value;
|
2020-08-26 17:37:44 +01:00
|
|
|
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
|
2020-11-05 19:06:18 +02:00
|
|
|
// should only finalize it, not create another (irrespective
|
2022-03-29 17:10:19 +05:30
|
|
|
// of state.activeTool.locked)
|
2021-12-16 21:14:03 +05:30
|
|
|
if (isTextElement(this.state.editingElement)) {
|
2020-07-09 14:15:42 -07:00
|
|
|
return;
|
|
|
|
}
|
2021-12-16 21:14:03 +05:30
|
|
|
let sceneX = pointerDownState.origin.x;
|
|
|
|
let sceneY = pointerDownState.origin.y;
|
2020-03-08 18:09:45 -07:00
|
|
|
|
2021-12-16 21:14:03 +05:30
|
|
|
const element = this.getElementAtPosition(sceneX, sceneY, {
|
|
|
|
includeBoundTextElement: true,
|
|
|
|
});
|
|
|
|
|
|
|
|
const canBindText = hasBoundTextElement(element);
|
|
|
|
if (canBindText) {
|
|
|
|
sceneX = element.x + element.width / 2;
|
|
|
|
sceneY = element.y + element.height / 2;
|
|
|
|
}
|
2020-07-09 14:15:42 -07:00
|
|
|
this.startTextEditing({
|
2021-12-16 21:14:03 +05:30
|
|
|
sceneX,
|
|
|
|
sceneY,
|
|
|
|
shouldBind: false,
|
2020-07-09 14:15:42 -07:00
|
|
|
insertAtParentCenter: !event.altKey,
|
2020-03-16 19:07:47 -07:00
|
|
|
});
|
2020-03-08 18:09:45 -07:00
|
|
|
|
2021-03-03 14:04:02 +01:00
|
|
|
resetCursor(this.canvas);
|
2022-03-29 17:10:19 +05:30
|
|
|
if (!this.state.activeTool.locked) {
|
2020-03-08 18:09:45 -07:00
|
|
|
this.setState({
|
2022-03-29 17:10:19 +05:30
|
|
|
activeTool: { ...this.state.activeTool, type: "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
|
|
|
|
2021-05-09 16:42:10 +01:00
|
|
|
private handleFreeDrawElementOnPointerDown = (
|
|
|
|
event: React.PointerEvent<HTMLCanvasElement>,
|
|
|
|
elementType: ExcalidrawFreeDrawElement["type"],
|
|
|
|
pointerDownState: PointerDownState,
|
|
|
|
) => {
|
|
|
|
// Begin a mark capture. This does not have to update state yet.
|
|
|
|
const [gridX, gridY] = getGridPoint(
|
|
|
|
pointerDownState.origin.x,
|
|
|
|
pointerDownState.origin.y,
|
|
|
|
null,
|
|
|
|
);
|
|
|
|
|
|
|
|
const element = newFreeDrawElement({
|
|
|
|
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,
|
|
|
|
strokeSharpness: this.state.currentItemLinearStrokeSharpness,
|
|
|
|
simulatePressure: event.pressure === 0.5,
|
|
|
|
});
|
|
|
|
|
|
|
|
this.setState((prevState) => ({
|
|
|
|
selectedElementIds: {
|
|
|
|
...prevState.selectedElementIds,
|
|
|
|
[element.id]: false,
|
|
|
|
},
|
|
|
|
}));
|
|
|
|
|
|
|
|
const pressures = element.simulatePressure
|
|
|
|
? element.pressures
|
|
|
|
: [...element.pressures, event.pressure];
|
|
|
|
|
|
|
|
mutateElement(element, {
|
|
|
|
points: [[0, 0]],
|
|
|
|
pressures,
|
|
|
|
});
|
|
|
|
|
|
|
|
const boundElement = getHoveredElementForBinding(
|
|
|
|
pointerDownState.origin,
|
|
|
|
this.scene,
|
|
|
|
);
|
|
|
|
this.scene.replaceAllElements([
|
|
|
|
...this.scene.getElementsIncludingDeleted(),
|
|
|
|
element,
|
|
|
|
]);
|
|
|
|
this.setState({
|
|
|
|
draggingElement: element,
|
|
|
|
editingElement: element,
|
|
|
|
startBoundElement: boundElement,
|
|
|
|
suggestedBindings: [],
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2021-10-21 22:05:48 +02:00
|
|
|
private createImageElement = ({
|
|
|
|
sceneX,
|
|
|
|
sceneY,
|
|
|
|
}: {
|
|
|
|
sceneX: number;
|
|
|
|
sceneY: number;
|
|
|
|
}) => {
|
|
|
|
const [gridX, gridY] = getGridPoint(sceneX, sceneY, this.state.gridSize);
|
|
|
|
|
|
|
|
const element = newImageElement({
|
|
|
|
type: "image",
|
|
|
|
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,
|
|
|
|
strokeSharpness: this.state.currentItemLinearStrokeSharpness,
|
|
|
|
});
|
|
|
|
|
|
|
|
return element;
|
|
|
|
};
|
|
|
|
|
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
|
2021-02-14 14:43:23 +01:00
|
|
|
if (
|
|
|
|
multiElement.type === "line" &&
|
|
|
|
isPathALoop(multiElement.points, this.state.zoom.value)
|
|
|
|
) {
|
2020-07-09 14:15:42 -07:00
|
|
|
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
|
2020-11-05 19:06:18 +02:00
|
|
|
// point
|
2020-07-09 14:15:42 -07:00
|
|
|
mutateElement(multiElement, {
|
|
|
|
lastCommittedPoint: multiElement.points[multiElement.points.length - 1],
|
2020-07-08 22:07:51 -07:00
|
|
|
});
|
2021-03-03 14:04:02 +01:00
|
|
|
setCursor(this.canvas, CURSOR_TYPE.POINTER);
|
2020-07-09 14:15:42 -07:00
|
|
|
} else {
|
|
|
|
const [gridX, gridY] = getGridPoint(
|
|
|
|
pointerDownState.origin.x,
|
|
|
|
pointerDownState.origin.y,
|
2021-05-09 16:42:10 +01:00
|
|
|
this.state.gridSize,
|
2020-07-09 14:15:42 -07:00
|
|
|
);
|
2020-12-08 15:02:55 +00:00
|
|
|
|
2020-12-12 16:42:30 +00:00
|
|
|
/* If arrow is pre-arrowheads, it will have undefined for both start and end arrowheads.
|
2020-12-14 14:14:56 +00:00
|
|
|
If so, we want it to be null for start and "arrow" for end. If the linear item is not
|
|
|
|
an arrow, we want it to be null for both. Otherwise, we want it to use the
|
|
|
|
values from appState. */
|
2020-12-12 16:42:30 +00:00
|
|
|
|
|
|
|
const { currentItemStartArrowhead, currentItemEndArrowhead } = this.state;
|
2020-12-08 15:02:55 +00:00
|
|
|
const [startArrowhead, endArrowhead] =
|
2020-12-12 16:42:30 +00:00
|
|
|
elementType === "arrow"
|
|
|
|
? [currentItemStartArrowhead, currentItemEndArrowhead]
|
|
|
|
: [null, null];
|
2020-12-08 15:02:55 +00:00
|
|
|
|
2020-07-09 14:15:42 -07:00
|
|
|
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-12-08 15:02:55 +00:00
|
|
|
startArrowhead,
|
|
|
|
endArrowhead,
|
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-12-06 22:39:31 +00:00
|
|
|
private onKeyDownFromPointerDownHandler(
|
|
|
|
pointerDownState: PointerDownState,
|
|
|
|
): (event: KeyboardEvent) => void {
|
|
|
|
return withBatchedUpdates((event: KeyboardEvent) => {
|
|
|
|
if (this.maybeHandleResize(pointerDownState, event)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
this.maybeDragNewGenericElement(pointerDownState, event);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
private onKeyUpFromPointerDownHandler(
|
|
|
|
pointerDownState: PointerDownState,
|
|
|
|
): (event: KeyboardEvent) => void {
|
|
|
|
return withBatchedUpdates((event: KeyboardEvent) => {
|
|
|
|
// Prevents focus from escaping excalidraw tab
|
|
|
|
event.key === KEYS.ALT && event.preventDefault();
|
|
|
|
if (this.maybeHandleResize(pointerDownState, event)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
this.maybeDragNewGenericElement(pointerDownState, event);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2020-07-09 14:15:42 -07:00
|
|
|
private onPointerMoveFromPointerDownHandler(
|
2020-07-09 09:30:38 -07:00
|
|
|
pointerDownState: PointerDownState,
|
2022-02-06 17:45:37 +01:00
|
|
|
) {
|
|
|
|
return withBatchedUpdatesThrottled((event: PointerEvent) => {
|
2020-07-09 14:15:42 -07:00
|
|
|
// 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-11-04 17:49:15 +00:00
|
|
|
const pointerCoords = viewportCoordsToSceneCoords(event, this.state);
|
2022-03-11 19:53:42 +05:30
|
|
|
|
|
|
|
if (isEraserActive(this.state)) {
|
|
|
|
this.handleEraser(event, pointerDownState, pointerCoords);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
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
|
2020-11-05 19:06:18 +02:00
|
|
|
// 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)
|
2020-07-09 14:15:42 -07:00
|
|
|
if (
|
|
|
|
!pointerDownState.drag.hasOccurred &&
|
2022-03-25 20:46:01 +05:30
|
|
|
(this.state.activeTool.type === "arrow" ||
|
|
|
|
this.state.activeTool.type === "line")
|
2020-07-09 14:15:42 -07:00
|
|
|
) {
|
|
|
|
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) {
|
2020-12-06 22:39:31 +00:00
|
|
|
pointerDownState.lastCoords.x = pointerCoords.x;
|
|
|
|
pointerDownState.lastCoords.y = pointerCoords.y;
|
|
|
|
if (this.maybeHandleResize(pointerDownState, event)) {
|
|
|
|
return true;
|
2020-07-09 14:15:42 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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,
|
2021-12-13 13:35:07 +01:00
|
|
|
(element, pointsSceneCoords) => {
|
|
|
|
this.maybeSuggestBindingsForLinearElementAtCoords(
|
2020-08-08 21:04:15 -07:00
|
|
|
element,
|
2021-12-13 13:35:07 +01:00
|
|
|
pointsSceneCoords,
|
2020-08-08 21:04:15 -07:00
|
|
|
);
|
|
|
|
},
|
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),
|
|
|
|
);
|
2021-05-09 16:42:10 +01:00
|
|
|
|
2020-08-26 17:37:44 +01:00
|
|
|
if (
|
2021-12-13 13:35:07 +01:00
|
|
|
(hasHitASelectedElement ||
|
|
|
|
pointerDownState.hit.hasHitCommonBoundingBoxOfSelectedElements) &&
|
|
|
|
// this allows for box-selecting points when clicking inside the
|
|
|
|
// line's bounding box
|
|
|
|
(!this.state.editingLinearElement || !event.shiftKey) &&
|
|
|
|
// box-selecting without shift when editing line, not clicking on a line
|
|
|
|
(!this.state.editingLinearElement ||
|
|
|
|
this.state.editingLinearElement?.elementId !==
|
|
|
|
pointerDownState.hit.element?.id ||
|
|
|
|
pointerDownState.hit.hasHitElementInside)
|
2020-08-26 17:37:44 +01:00
|
|
|
) {
|
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,
|
|
|
|
);
|
2021-05-20 22:28:34 +02:00
|
|
|
// prevent dragging even if we're no longer holding cmd/ctrl otherwise
|
|
|
|
// it would have weird results (stuff jumping all over the screen)
|
|
|
|
if (selectedElements.length > 0 && !pointerDownState.withCmdOrCtrl) {
|
2020-07-09 14:15:42 -07:00
|
|
|
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-09-11 17:22:40 +02:00
|
|
|
|
|
|
|
const [dragDistanceX, dragDistanceY] = [
|
|
|
|
Math.abs(pointerCoords.x - pointerDownState.origin.x),
|
|
|
|
Math.abs(pointerCoords.y - pointerDownState.origin.y),
|
|
|
|
];
|
|
|
|
|
|
|
|
// We only drag in one direction if shift is pressed
|
|
|
|
const lockDirection = event.shiftKey;
|
|
|
|
|
|
|
|
dragSelectedElements(
|
|
|
|
pointerDownState,
|
|
|
|
selectedElements,
|
|
|
|
dragX,
|
|
|
|
dragY,
|
|
|
|
lockDirection,
|
|
|
|
dragDistanceX,
|
|
|
|
dragDistanceY,
|
2021-12-22 19:16:49 +05:30
|
|
|
this.state,
|
2020-09-11 17:22:40 +02:00
|
|
|
);
|
2020-08-08 21:04:15 -07:00
|
|
|
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;
|
2021-12-16 21:14:03 +05:30
|
|
|
const elements = this.scene.getElementsIncludingDeleted();
|
|
|
|
const selectedElementIds: Array<ExcalidrawElement["id"]> =
|
|
|
|
getSelectedElements(elements, this.state, true).map(
|
|
|
|
(element) => element.id,
|
|
|
|
);
|
|
|
|
|
|
|
|
for (const element of elements) {
|
2020-07-09 14:15:42 -07:00
|
|
|
if (
|
2021-12-16 21:14:03 +05:30
|
|
|
selectedElementIds.includes(element.id) ||
|
2020-07-09 14:15:42 -07:00
|
|
|
// case: the state.selectedElementIds might not have been
|
2020-11-05 19:06:18 +02:00
|
|
|
// 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];
|
2021-12-16 21:14:03 +05:30
|
|
|
bindTextToShapeAfterDuplication(
|
|
|
|
nextElements,
|
|
|
|
elementsToAppend,
|
|
|
|
oldIdToDuplicatedId,
|
|
|
|
);
|
2020-08-08 21:04:15 -07:00
|
|
|
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
|
|
|
|
2021-05-09 16:42:10 +01:00
|
|
|
if (draggingElement.type === "freedraw") {
|
|
|
|
const points = draggingElement.points;
|
|
|
|
const dx = pointerCoords.x - draggingElement.x;
|
|
|
|
const dy = pointerCoords.y - draggingElement.y;
|
|
|
|
|
2022-02-06 17:45:23 +01:00
|
|
|
const lastPoint = points.length > 0 && points[points.length - 1];
|
|
|
|
const discardPoint =
|
|
|
|
lastPoint && lastPoint[0] === dx && lastPoint[1] === dy;
|
2021-05-09 16:42:10 +01:00
|
|
|
|
2022-02-06 17:45:23 +01:00
|
|
|
if (!discardPoint) {
|
|
|
|
const pressures = draggingElement.simulatePressure
|
|
|
|
? draggingElement.pressures
|
|
|
|
: [...draggingElement.pressures, event.pressure];
|
|
|
|
|
|
|
|
mutateElement(draggingElement, {
|
|
|
|
points: [...points, [dx, dy]],
|
|
|
|
pressures,
|
|
|
|
});
|
|
|
|
}
|
2021-05-09 16:42:10 +01:00
|
|
|
} else if (isLinearElement(draggingElement)) {
|
2020-07-09 14:15:42 -07:00
|
|
|
pointerDownState.drag.hasOccurred = true;
|
|
|
|
const points = draggingElement.points;
|
2021-05-09 16:42:10 +01:00
|
|
|
let dx = gridX - draggingElement.x;
|
|
|
|
let dy = gridY - draggingElement.y;
|
2020-07-08 22:07:51 -07:00
|
|
|
|
2021-10-21 22:05:48 +02:00
|
|
|
if (shouldRotateWithDiscreteAngle(event) && points.length === 2) {
|
2020-07-09 14:15:42 -07:00
|
|
|
({ width: dx, height: dy } = getPerfectElementSize(
|
2022-03-25 20:46:01 +05:30
|
|
|
this.state.activeTool.type,
|
2020-07-09 14:15:42 -07:00
|
|
|
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) {
|
2021-05-09 16:42:10 +01:00
|
|
|
mutateElement(draggingElement, {
|
|
|
|
points: [...points.slice(0, -1), [dx, dy]],
|
|
|
|
});
|
2020-07-09 14:15:42 -07:00
|
|
|
}
|
2021-05-09 16:42:10 +01:00
|
|
|
|
2020-08-08 21:04:15 -07:00
|
|
|
if (isBindingElement(draggingElement)) {
|
|
|
|
// When creating a linear element by dragging
|
2021-12-13 13:35:07 +01:00
|
|
|
this.maybeSuggestBindingsForLinearElementAtCoords(
|
2020-08-08 21:04:15 -07:00
|
|
|
draggingElement,
|
2021-12-13 13:35:07 +01:00
|
|
|
[pointerCoords],
|
2020-08-08 21:04:15 -07:00
|
|
|
this.state.startBoundElement,
|
|
|
|
);
|
|
|
|
}
|
2020-07-09 14:15:42 -07:00
|
|
|
} else {
|
2020-12-06 22:39:31 +00:00
|
|
|
pointerDownState.lastCoords.x = pointerCoords.x;
|
|
|
|
pointerDownState.lastCoords.y = pointerCoords.y;
|
|
|
|
this.maybeDragNewGenericElement(pointerDownState, event);
|
2020-07-09 14:15:42 -07:00
|
|
|
}
|
|
|
|
|
2022-03-25 20:46:01 +05:30
|
|
|
if (this.state.activeTool.type === "selection") {
|
2021-12-13 13:35:07 +01:00
|
|
|
pointerDownState.boxSelection.hasOccurred = true;
|
|
|
|
|
2020-07-30 14:50:59 +05:30
|
|
|
const elements = this.scene.getElements();
|
2021-12-13 13:35:07 +01:00
|
|
|
if (
|
|
|
|
!event.shiftKey &&
|
|
|
|
// allows for box-selecting points (without shift)
|
|
|
|
!this.state.editingLinearElement &&
|
|
|
|
isSomeElementSelected(elements, this.state)
|
|
|
|
) {
|
2021-05-20 22:28:34 +02:00
|
|
|
if (pointerDownState.withCmdOrCtrl && pointerDownState.hit.element) {
|
|
|
|
this.setState((prevState) =>
|
|
|
|
selectGroupsForSelectedElements(
|
|
|
|
{
|
|
|
|
...prevState,
|
|
|
|
selectedElementIds: {
|
|
|
|
[pointerDownState.hit.element!.id]: true,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
this.scene.getElements(),
|
|
|
|
),
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
this.setState({
|
|
|
|
selectedElementIds: {},
|
|
|
|
selectedGroupIds: {},
|
|
|
|
editingGroupId: null,
|
|
|
|
});
|
|
|
|
}
|
2020-07-09 09:30:38 -07:00
|
|
|
}
|
2021-12-13 13:35:07 +01:00
|
|
|
// box-select line editor points
|
|
|
|
if (this.state.editingLinearElement) {
|
|
|
|
LinearElementEditor.handleBoxSelection(
|
|
|
|
event,
|
|
|
|
this.state,
|
|
|
|
this.setState.bind(this),
|
|
|
|
);
|
|
|
|
// regular box-select
|
|
|
|
} else {
|
|
|
|
const elementsWithinSelection = getElementsWithinSelection(
|
|
|
|
elements,
|
|
|
|
draggingElement,
|
|
|
|
);
|
|
|
|
this.setState((prevState) =>
|
|
|
|
selectGroupsForSelectedElements(
|
|
|
|
{
|
|
|
|
...prevState,
|
|
|
|
selectedElementIds: {
|
|
|
|
...prevState.selectedElementIds,
|
|
|
|
...elementsWithinSelection.reduce((map, element) => {
|
|
|
|
map[element.id] = true;
|
|
|
|
return map;
|
|
|
|
}, {} as any),
|
|
|
|
...(pointerDownState.hit.element
|
|
|
|
? {
|
|
|
|
// if using ctrl/cmd, select the hitElement only if we
|
|
|
|
// haven't box-selected anything else
|
|
|
|
[pointerDownState.hit.element.id]:
|
|
|
|
!elementsWithinSelection.length,
|
|
|
|
}
|
|
|
|
: null),
|
|
|
|
},
|
2022-02-03 20:34:59 +05:30
|
|
|
showHyperlinkPopup:
|
|
|
|
elementsWithinSelection.length === 1 &&
|
|
|
|
elementsWithinSelection[0].link
|
|
|
|
? "info"
|
|
|
|
: false,
|
2020-07-09 14:15:42 -07:00
|
|
|
},
|
2021-12-13 13:35:07 +01:00
|
|
|
this.scene.getElements(),
|
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|
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({
|
2021-01-31 10:47:43 +01:00
|
|
|
scrollX: this.state.scrollX - dx / this.state.zoom.value,
|
2020-08-08 21:04:15 -07:00
|
|
|
});
|
|
|
|
pointerDownState.lastCoords.x = x;
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (pointerDownState.scrollbars.isOverVertical) {
|
|
|
|
const y = event.clientY;
|
|
|
|
const dy = y - pointerDownState.lastCoords.y;
|
|
|
|
this.setState({
|
2021-01-31 10:47:43 +01:00
|
|
|
scrollY: this.state.scrollY - dy / this.state.zoom.value,
|
2020-08-08 21:04:15 -07:00
|
|
|
});
|
|
|
|
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,
|
2022-03-25 20:46:01 +05:30
|
|
|
activeTool,
|
2020-08-08 21:04:15 -07:00
|
|
|
isResizing,
|
2020-08-29 17:56:03 +02:00
|
|
|
isRotating,
|
2020-07-09 14:15:42 -07:00
|
|
|
} = this.state;
|
|
|
|
this.setState({
|
|
|
|
isResizing: false,
|
|
|
|
isRotating: false,
|
|
|
|
resizingElement: null,
|
|
|
|
selectionElement: null,
|
|
|
|
cursorButton: "up",
|
|
|
|
// text elements are reset on finalize, and resetting on pointerup
|
2020-11-05 19:06:18 +02:00
|
|
|
// may cause issues with double taps
|
2020-07-09 14:15:42 -07:00
|
|
|
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) {
|
2021-12-13 13:35:07 +01:00
|
|
|
if (
|
|
|
|
!pointerDownState.boxSelection.hasOccurred &&
|
|
|
|
(pointerDownState.hit?.element?.id !==
|
|
|
|
this.state.editingLinearElement.elementId ||
|
|
|
|
!pointerDownState.hit.hasHitElementInside)
|
|
|
|
) {
|
|
|
|
this.actionManager.executeAction(actionFinalize);
|
|
|
|
} else {
|
|
|
|
const editingLinearElement = LinearElementEditor.handlePointerUp(
|
|
|
|
childEvent,
|
|
|
|
this.state.editingLinearElement,
|
|
|
|
this.state,
|
|
|
|
);
|
|
|
|
if (editingLinearElement !== this.state.editingLinearElement) {
|
|
|
|
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;
|
|
|
|
|
2022-02-06 17:45:37 +01:00
|
|
|
if (pointerDownState.eventListeners.onMove) {
|
|
|
|
pointerDownState.eventListeners.onMove.flush();
|
|
|
|
}
|
|
|
|
|
2020-07-09 14:15:42 -07:00
|
|
|
window.removeEventListener(
|
|
|
|
EVENT.POINTER_MOVE,
|
|
|
|
pointerDownState.eventListeners.onMove!,
|
|
|
|
);
|
|
|
|
window.removeEventListener(
|
|
|
|
EVENT.POINTER_UP,
|
|
|
|
pointerDownState.eventListeners.onUp!,
|
|
|
|
);
|
2020-12-06 22:39:31 +00:00
|
|
|
window.removeEventListener(
|
|
|
|
EVENT.KEYDOWN,
|
|
|
|
pointerDownState.eventListeners.onKeyDown!,
|
|
|
|
);
|
|
|
|
window.removeEventListener(
|
|
|
|
EVENT.KEYUP,
|
|
|
|
pointerDownState.eventListeners.onKeyUp!,
|
|
|
|
);
|
2020-07-09 14:15:42 -07:00
|
|
|
|
2021-10-21 22:05:48 +02:00
|
|
|
if (this.state.pendingImageElement) {
|
|
|
|
this.setState({ pendingImageElement: null });
|
|
|
|
}
|
|
|
|
|
2021-05-09 16:42:10 +01:00
|
|
|
if (draggingElement?.type === "freedraw") {
|
|
|
|
const pointerCoords = viewportCoordsToSceneCoords(
|
|
|
|
childEvent,
|
|
|
|
this.state,
|
|
|
|
);
|
|
|
|
|
|
|
|
const points = draggingElement.points;
|
|
|
|
let dx = pointerCoords.x - draggingElement.x;
|
|
|
|
let dy = pointerCoords.y - draggingElement.y;
|
|
|
|
|
|
|
|
// Allows dots to avoid being flagged as infinitely small
|
|
|
|
if (dx === points[0][0] && dy === points[0][1]) {
|
|
|
|
dy += 0.0001;
|
|
|
|
dx += 0.0001;
|
|
|
|
}
|
|
|
|
|
|
|
|
const pressures = draggingElement.simulatePressure
|
|
|
|
? []
|
|
|
|
: [...draggingElement.pressures, childEvent.pressure];
|
|
|
|
|
|
|
|
mutateElement(draggingElement, {
|
|
|
|
points: [...points, [dx, dy]],
|
|
|
|
pressures,
|
2021-10-11 19:11:26 +01:00
|
|
|
lastCommittedPoint: [dx, dy],
|
2021-05-09 16:42:10 +01:00
|
|
|
});
|
|
|
|
|
2020-07-09 14:15:42 -07:00
|
|
|
this.actionManager.executeAction(actionFinalize);
|
2021-05-09 16:42:10 +01:00
|
|
|
|
2020-07-09 14:15:42 -07:00
|
|
|
return;
|
|
|
|
}
|
2021-10-21 22:05:48 +02:00
|
|
|
if (isImageElement(draggingElement)) {
|
|
|
|
const imageElement = draggingElement;
|
|
|
|
try {
|
|
|
|
this.initializeImageDimensions(imageElement);
|
|
|
|
this.setState(
|
|
|
|
{ selectedElementIds: { [imageElement.id]: true } },
|
|
|
|
() => {
|
|
|
|
this.actionManager.executeAction(actionFinalize);
|
|
|
|
},
|
|
|
|
);
|
2021-11-02 14:24:16 +02:00
|
|
|
} catch (error: any) {
|
2021-10-21 22:05:48 +02:00
|
|
|
console.error(error);
|
|
|
|
this.scene.replaceAllElements(
|
|
|
|
this.scene
|
|
|
|
.getElementsIncludingDeleted()
|
|
|
|
.filter((el) => el.id !== imageElement.id),
|
|
|
|
);
|
|
|
|
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) {
|
2021-04-24 18:21:02 +05:30
|
|
|
this.history.resumeRecording();
|
2020-07-09 14:15:42 -07:00
|
|
|
}
|
2020-08-08 21:04:15 -07:00
|
|
|
const pointerCoords = viewportCoordsToSceneCoords(
|
|
|
|
childEvent,
|
|
|
|
this.state,
|
|
|
|
);
|
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 });
|
2022-03-29 17:10:19 +05:30
|
|
|
if (!activeTool.locked) {
|
2021-03-03 14:04:02 +01:00
|
|
|
resetCursor(this.canvas);
|
2020-07-09 14:15:42 -07:00
|
|
|
this.setState((prevState) => ({
|
|
|
|
draggingElement: null,
|
2022-03-29 17:10:19 +05:30
|
|
|
activeTool: { ...prevState.activeTool, type: "selection" },
|
2020-07-09 14:15:42 -07:00
|
|
|
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-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 (
|
2022-03-25 20:46:01 +05:30
|
|
|
activeTool.type !== "selection" &&
|
2020-07-09 14:15:42 -07:00
|
|
|
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-07-09 09:30:38 -07:00
|
|
|
|
2020-07-09 14:15:42 -07:00
|
|
|
if (resizingElement) {
|
2021-04-24 18:21:02 +05:30
|
|
|
this.history.resumeRecording();
|
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 && 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;
|
2022-03-11 19:53:42 +05:30
|
|
|
if (isEraserActive(this.state)) {
|
2022-03-14 14:59:55 +05:30
|
|
|
const draggedDistance = distance2d(
|
|
|
|
this.lastPointerDown!.clientX,
|
|
|
|
this.lastPointerDown!.clientY,
|
|
|
|
this.lastPointerUp!.clientX,
|
|
|
|
this.lastPointerUp!.clientY,
|
|
|
|
);
|
|
|
|
|
|
|
|
if (draggedDistance === 0) {
|
|
|
|
const scenePointer = viewportCoordsToSceneCoords(
|
|
|
|
{
|
|
|
|
clientX: this.lastPointerUp!.clientX,
|
|
|
|
clientY: this.lastPointerUp!.clientY,
|
|
|
|
},
|
|
|
|
this.state,
|
|
|
|
);
|
2022-03-17 21:03:59 +05:30
|
|
|
const hitElements = this.getElementsAtPosition(
|
2022-03-14 14:59:55 +05:30
|
|
|
scenePointer.x,
|
|
|
|
scenePointer.y,
|
|
|
|
);
|
2022-03-17 21:03:59 +05:30
|
|
|
hitElements.forEach(
|
|
|
|
(hitElement) =>
|
2022-03-22 16:40:28 +05:30
|
|
|
(pointerDownState.elementIdsToErase[hitElement.id] = {
|
|
|
|
erase: true,
|
|
|
|
opacity: hitElement.opacity,
|
|
|
|
}),
|
2022-03-17 21:03:59 +05:30
|
|
|
);
|
2022-03-14 14:59:55 +05:30
|
|
|
}
|
2022-03-11 19:53:42 +05:30
|
|
|
this.eraseElements(pointerDownState);
|
|
|
|
return;
|
2022-03-22 17:14:07 +05:30
|
|
|
} else if (Object.keys(pointerDownState.elementIdsToErase).length) {
|
|
|
|
this.restoreReadyToEraseElements(pointerDownState);
|
2022-03-11 19:53:42 +05:30
|
|
|
}
|
|
|
|
|
2020-07-09 14:15:42 -07:00
|
|
|
if (
|
|
|
|
hitElement &&
|
|
|
|
!pointerDownState.drag.hasOccurred &&
|
2021-12-13 13:35:07 +01:00
|
|
|
!pointerDownState.hit.wasAddedToSelection &&
|
|
|
|
// if we're editing a line, pointerup shouldn't switch selection if
|
|
|
|
// box selected
|
|
|
|
(!this.state.editingLinearElement ||
|
|
|
|
!pointerDownState.boxSelection.hasOccurred)
|
2020-07-09 14:15:42 -07:00
|
|
|
) {
|
2021-12-13 13:35:07 +01:00
|
|
|
// when inside line editor, shift selects points instead
|
|
|
|
if (childEvent.shiftKey && !this.state.editingLinearElement) {
|
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
|
2020-11-05 19:06:18 +02:00
|
|
|
// as well as all elements that are part of the groups
|
|
|
|
// hitElement is part of
|
2020-08-26 17:37:44 +01:00
|
|
|
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
|
2021-05-20 22:28:34 +02:00
|
|
|
this.setState((prevState) =>
|
|
|
|
selectGroupsForSelectedElements(
|
|
|
|
{
|
|
|
|
...prevState,
|
|
|
|
selectedElementIds: {
|
|
|
|
...prevState.selectedElementIds,
|
|
|
|
[hitElement!.id]: false,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
this.scene.getElements(),
|
|
|
|
),
|
|
|
|
);
|
2020-08-26 17:37:44 +01:00
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// add element to selection while
|
|
|
|
// keeping prev elements selected
|
2021-12-16 21:14:03 +05:30
|
|
|
|
2020-08-26 17:37:44 +01:00
|
|
|
this.setState((_prevState) => ({
|
|
|
|
selectedElementIds: {
|
|
|
|
..._prevState.selectedElementIds,
|
|
|
|
[hitElement!.id]: true,
|
|
|
|
},
|
|
|
|
}));
|
|
|
|
}
|
2020-07-09 14:15:42 -07:00
|
|
|
} else {
|
2020-08-27 20:32:10 +02:00
|
|
|
this.setState((prevState) => ({
|
|
|
|
...selectGroupsForSelectedElements(
|
|
|
|
{
|
|
|
|
...prevState,
|
|
|
|
selectedElementIds: { [hitElement.id]: true },
|
2020-08-26 17:37:44 +01:00
|
|
|
},
|
2020-08-27 20:32:10 +02:00
|
|
|
this.scene.getElements(),
|
|
|
|
),
|
|
|
|
}));
|
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
|
2020-07-09 14:15:42 -07:00
|
|
|
this.setState({
|
|
|
|
selectedElementIds: {},
|
|
|
|
selectedGroupIds: {},
|
|
|
|
editingGroupId: null,
|
2020-07-09 09:30:38 -07:00
|
|
|
});
|
2020-08-30 12:10:07 +01:00
|
|
|
|
2020-07-09 09:30:38 -07:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2022-03-29 17:10:19 +05:30
|
|
|
if (
|
|
|
|
!activeTool.locked &&
|
|
|
|
activeTool.type !== "freedraw" &&
|
|
|
|
draggingElement
|
|
|
|
) {
|
2020-07-09 14:15:42 -07:00
|
|
|
this.setState((prevState) => ({
|
|
|
|
selectedElementIds: {
|
|
|
|
...prevState.selectedElementIds,
|
|
|
|
[draggingElement.id]: true,
|
|
|
|
},
|
|
|
|
}));
|
|
|
|
}
|
2020-07-09 09:30:38 -07:00
|
|
|
|
|
|
|
if (
|
2022-03-25 20:46:01 +05:30
|
|
|
activeTool.type !== "selection" ||
|
2020-07-30 14:50:59 +05:30
|
|
|
isSomeElementSelected(this.scene.getElements(), this.state)
|
2020-07-09 09:30:38 -07:00
|
|
|
) {
|
2021-04-24 18:21:02 +05:30
|
|
|
this.history.resumeRecording();
|
2020-07-09 09:30:38 -07:00
|
|
|
}
|
|
|
|
|
2020-08-29 17:56:03 +02:00
|
|
|
if (pointerDownState.drag.hasOccurred || isResizing || isRotating) {
|
2020-08-08 21:04:15 -07:00
|
|
|
(isBindingEnabled(this.state)
|
|
|
|
? bindOrUnbindSelectedElements
|
|
|
|
: unbindLinearElements)(
|
|
|
|
getSelectedElements(this.scene.getElements(), this.state),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2022-03-29 17:10:19 +05:30
|
|
|
if (!activeTool.locked && activeTool.type !== "freedraw") {
|
2021-03-03 14:04:02 +01:00
|
|
|
resetCursor(this.canvas);
|
2020-07-09 14:15:42 -07:00
|
|
|
this.setState({
|
|
|
|
draggingElement: null,
|
2020-08-08 21:04:15 -07:00
|
|
|
suggestedBindings: [],
|
2022-03-29 17:10:19 +05:30
|
|
|
activeTool: { ...activeTool, type: "selection" },
|
2020-07-09 14:15:42 -07:00
|
|
|
});
|
|
|
|
} 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
|
|
|
|
2022-03-22 17:14:07 +05:30
|
|
|
private restoreReadyToEraseElements = (
|
|
|
|
pointerDownState: PointerDownState,
|
|
|
|
) => {
|
|
|
|
const elements = this.scene.getElements().map((ele) => {
|
|
|
|
if (
|
|
|
|
pointerDownState.elementIdsToErase[ele.id] &&
|
|
|
|
pointerDownState.elementIdsToErase[ele.id].erase
|
|
|
|
) {
|
|
|
|
return newElementWith(ele, {
|
|
|
|
opacity: pointerDownState.elementIdsToErase[ele.id].opacity,
|
|
|
|
});
|
|
|
|
} else if (
|
|
|
|
isBoundToContainer(ele) &&
|
|
|
|
pointerDownState.elementIdsToErase[ele.containerId] &&
|
|
|
|
pointerDownState.elementIdsToErase[ele.containerId].erase
|
|
|
|
) {
|
|
|
|
return newElementWith(ele, {
|
|
|
|
opacity: pointerDownState.elementIdsToErase[ele.containerId].opacity,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
return ele;
|
|
|
|
});
|
|
|
|
|
|
|
|
this.scene.replaceAllElements(elements);
|
|
|
|
};
|
|
|
|
|
2022-03-11 19:53:42 +05:30
|
|
|
private eraseElements = (pointerDownState: PointerDownState) => {
|
|
|
|
const elements = this.scene.getElements().map((ele) => {
|
2022-03-22 16:40:28 +05:30
|
|
|
if (
|
|
|
|
pointerDownState.elementIdsToErase[ele.id] &&
|
|
|
|
pointerDownState.elementIdsToErase[ele.id].erase
|
|
|
|
) {
|
2022-03-11 19:53:42 +05:30
|
|
|
return newElementWith(ele, { isDeleted: true });
|
|
|
|
} else if (
|
|
|
|
isBoundToContainer(ele) &&
|
2022-03-22 16:40:28 +05:30
|
|
|
pointerDownState.elementIdsToErase[ele.containerId] &&
|
|
|
|
pointerDownState.elementIdsToErase[ele.containerId].erase
|
2022-03-11 19:53:42 +05:30
|
|
|
) {
|
|
|
|
return newElementWith(ele, { isDeleted: true });
|
|
|
|
}
|
|
|
|
return ele;
|
|
|
|
});
|
|
|
|
|
|
|
|
this.history.resumeRecording();
|
|
|
|
this.scene.replaceAllElements(elements);
|
|
|
|
};
|
|
|
|
|
2021-10-21 22:05:48 +02:00
|
|
|
private initializeImage = async ({
|
|
|
|
imageFile,
|
|
|
|
imageElement: _imageElement,
|
|
|
|
showCursorImagePreview = false,
|
|
|
|
}: {
|
|
|
|
imageFile: File;
|
|
|
|
imageElement: ExcalidrawImageElement;
|
|
|
|
showCursorImagePreview?: boolean;
|
|
|
|
}) => {
|
|
|
|
// at this point this should be guaranteed image file, but we do this check
|
|
|
|
// to satisfy TS down the line
|
|
|
|
if (!isSupportedImageFile(imageFile)) {
|
|
|
|
throw new Error(t("errors.unsupportedFileType"));
|
|
|
|
}
|
|
|
|
const mimeType = imageFile.type;
|
|
|
|
|
|
|
|
setCursor(this.canvas, "wait");
|
|
|
|
|
|
|
|
if (mimeType === MIME_TYPES.svg) {
|
|
|
|
try {
|
|
|
|
imageFile = SVGStringToFile(
|
|
|
|
await normalizeSVG(await imageFile.text()),
|
|
|
|
imageFile.name,
|
|
|
|
);
|
2021-11-02 14:24:16 +02:00
|
|
|
} catch (error: any) {
|
2021-10-21 22:05:48 +02:00
|
|
|
console.warn(error);
|
|
|
|
throw new Error(t("errors.svgImageInsertError"));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// generate image id (by default the file digest) before any
|
|
|
|
// resizing/compression takes place to keep it more portable
|
|
|
|
const fileId = await ((this.props.generateIdForFile?.(
|
|
|
|
imageFile,
|
|
|
|
) as Promise<FileId>) || generateIdFromFile(imageFile));
|
|
|
|
|
|
|
|
if (!fileId) {
|
|
|
|
console.warn(
|
|
|
|
"Couldn't generate file id or the supplied `generateIdForFile` didn't resolve to one.",
|
|
|
|
);
|
|
|
|
throw new Error(t("errors.imageInsertError"));
|
|
|
|
}
|
|
|
|
|
|
|
|
const existingFileData = this.files[fileId];
|
|
|
|
if (!existingFileData?.dataURL) {
|
|
|
|
try {
|
2021-11-26 11:46:13 +01:00
|
|
|
imageFile = await resizeImageFile(imageFile, {
|
|
|
|
maxWidthOrHeight: DEFAULT_MAX_IMAGE_WIDTH_OR_HEIGHT,
|
|
|
|
});
|
2021-11-02 14:24:16 +02:00
|
|
|
} catch (error: any) {
|
2021-10-21 22:05:48 +02:00
|
|
|
console.error("error trying to resing image file on insertion", error);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (imageFile.size > MAX_ALLOWED_FILE_BYTES) {
|
|
|
|
throw new Error(
|
|
|
|
t("errors.fileTooBig", {
|
|
|
|
maxSize: `${Math.trunc(MAX_ALLOWED_FILE_BYTES / 1024 / 1024)}MB`,
|
|
|
|
}),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (showCursorImagePreview) {
|
|
|
|
const dataURL = this.files[fileId]?.dataURL;
|
|
|
|
// optimization so that we don't unnecessarily resize the original
|
|
|
|
// full-size file for cursor preview
|
|
|
|
// (it's much faster to convert the resized dataURL to File)
|
|
|
|
const resizedFile = dataURL && dataURLToFile(dataURL);
|
|
|
|
|
|
|
|
this.setImagePreviewCursor(resizedFile || imageFile);
|
|
|
|
}
|
|
|
|
|
|
|
|
const dataURL =
|
|
|
|
this.files[fileId]?.dataURL || (await getDataURL(imageFile));
|
|
|
|
|
|
|
|
const imageElement = mutateElement(
|
|
|
|
_imageElement,
|
|
|
|
{
|
|
|
|
fileId,
|
|
|
|
},
|
|
|
|
false,
|
|
|
|
) as NonDeleted<InitializedExcalidrawImageElement>;
|
|
|
|
|
|
|
|
return new Promise<NonDeleted<InitializedExcalidrawImageElement>>(
|
|
|
|
async (resolve, reject) => {
|
|
|
|
try {
|
|
|
|
this.files = {
|
|
|
|
...this.files,
|
|
|
|
[fileId]: {
|
|
|
|
mimeType,
|
|
|
|
id: fileId,
|
|
|
|
dataURL,
|
|
|
|
created: Date.now(),
|
|
|
|
},
|
|
|
|
};
|
|
|
|
const cachedImageData = this.imageCache.get(fileId);
|
|
|
|
if (!cachedImageData) {
|
|
|
|
this.addNewImagesToImageCache();
|
|
|
|
await this.updateImageCache([imageElement]);
|
|
|
|
}
|
|
|
|
if (cachedImageData?.image instanceof Promise) {
|
|
|
|
await cachedImageData.image;
|
|
|
|
}
|
|
|
|
if (
|
|
|
|
this.state.pendingImageElement?.id !== imageElement.id &&
|
|
|
|
this.state.draggingElement?.id !== imageElement.id
|
|
|
|
) {
|
|
|
|
this.initializeImageDimensions(imageElement, true);
|
|
|
|
}
|
|
|
|
resolve(imageElement);
|
2021-11-02 14:24:16 +02:00
|
|
|
} catch (error: any) {
|
2021-10-21 22:05:48 +02:00
|
|
|
console.error(error);
|
|
|
|
reject(new Error(t("errors.imageInsertError")));
|
|
|
|
} finally {
|
|
|
|
if (!showCursorImagePreview) {
|
|
|
|
resetCursor(this.canvas);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* inserts image into elements array and rerenders
|
|
|
|
*/
|
|
|
|
private insertImageElement = async (
|
|
|
|
imageElement: ExcalidrawImageElement,
|
|
|
|
imageFile: File,
|
|
|
|
showCursorImagePreview?: boolean,
|
|
|
|
) => {
|
|
|
|
this.scene.replaceAllElements([
|
|
|
|
...this.scene.getElementsIncludingDeleted(),
|
|
|
|
imageElement,
|
|
|
|
]);
|
|
|
|
|
|
|
|
try {
|
|
|
|
await this.initializeImage({
|
|
|
|
imageFile,
|
|
|
|
imageElement,
|
|
|
|
showCursorImagePreview,
|
|
|
|
});
|
2021-11-02 14:24:16 +02:00
|
|
|
} catch (error: any) {
|
2021-10-21 22:05:48 +02:00
|
|
|
mutateElement(imageElement, {
|
|
|
|
isDeleted: true,
|
|
|
|
});
|
|
|
|
this.actionManager.executeAction(actionFinalize);
|
|
|
|
this.setState({
|
|
|
|
errorMessage: error.message || t("errors.imageInsertError"),
|
|
|
|
});
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
private setImagePreviewCursor = async (imageFile: File) => {
|
|
|
|
// mustn't be larger than 128 px
|
|
|
|
// https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Basic_User_Interface/Using_URL_values_for_the_cursor_property
|
|
|
|
const cursorImageSizePx = 96;
|
|
|
|
|
2021-11-26 11:46:13 +01:00
|
|
|
const imagePreview = await resizeImageFile(imageFile, {
|
|
|
|
maxWidthOrHeight: cursorImageSizePx,
|
|
|
|
});
|
2021-10-21 22:05:48 +02:00
|
|
|
|
|
|
|
let previewDataURL = await getDataURL(imagePreview);
|
|
|
|
|
|
|
|
// SVG cannot be resized via `resizeImageFile` so we resize by rendering to
|
|
|
|
// a small canvas
|
|
|
|
if (imageFile.type === MIME_TYPES.svg) {
|
|
|
|
const img = await loadHTMLImageElement(previewDataURL);
|
|
|
|
|
|
|
|
let height = Math.min(img.height, cursorImageSizePx);
|
|
|
|
let width = height * (img.width / img.height);
|
|
|
|
|
|
|
|
if (width > cursorImageSizePx) {
|
|
|
|
width = cursorImageSizePx;
|
|
|
|
height = width * (img.height / img.width);
|
|
|
|
}
|
|
|
|
|
|
|
|
const canvas = document.createElement("canvas");
|
|
|
|
canvas.height = height;
|
|
|
|
canvas.width = width;
|
|
|
|
const context = canvas.getContext("2d")!;
|
|
|
|
|
|
|
|
context.drawImage(img, 0, 0, width, height);
|
|
|
|
|
|
|
|
previewDataURL = canvas.toDataURL(MIME_TYPES.svg) as DataURL;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (this.state.pendingImageElement) {
|
|
|
|
setCursor(this.canvas, `url(${previewDataURL}) 4 4, auto`);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
private onImageAction = async (
|
|
|
|
{ insertOnCanvasDirectly } = { insertOnCanvasDirectly: false },
|
|
|
|
) => {
|
|
|
|
try {
|
|
|
|
const clientX = this.state.width / 2 + this.state.offsetLeft;
|
|
|
|
const clientY = this.state.height / 2 + this.state.offsetTop;
|
|
|
|
|
|
|
|
const { x, y } = viewportCoordsToSceneCoords(
|
|
|
|
{ clientX, clientY },
|
|
|
|
this.state,
|
|
|
|
);
|
|
|
|
|
|
|
|
const imageFile = await fileOpen({
|
|
|
|
description: "Image",
|
|
|
|
extensions: ["jpg", "png", "svg", "gif"],
|
|
|
|
});
|
|
|
|
|
|
|
|
const imageElement = this.createImageElement({
|
|
|
|
sceneX: x,
|
|
|
|
sceneY: y,
|
|
|
|
});
|
|
|
|
|
|
|
|
if (insertOnCanvasDirectly) {
|
|
|
|
this.insertImageElement(imageElement, imageFile);
|
|
|
|
this.initializeImageDimensions(imageElement);
|
|
|
|
this.setState(
|
|
|
|
{
|
|
|
|
selectedElementIds: { [imageElement.id]: true },
|
|
|
|
},
|
|
|
|
() => {
|
|
|
|
this.actionManager.executeAction(actionFinalize);
|
|
|
|
},
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
this.setState(
|
|
|
|
{
|
|
|
|
pendingImageElement: imageElement,
|
|
|
|
},
|
|
|
|
() => {
|
|
|
|
this.insertImageElement(
|
|
|
|
imageElement,
|
|
|
|
imageFile,
|
|
|
|
/* showCursorImagePreview */ true,
|
|
|
|
);
|
|
|
|
},
|
|
|
|
);
|
|
|
|
}
|
2021-11-02 14:24:16 +02:00
|
|
|
} catch (error: any) {
|
2021-10-21 22:05:48 +02:00
|
|
|
if (error.name !== "AbortError") {
|
|
|
|
console.error(error);
|
2021-11-19 10:54:23 +01:00
|
|
|
} else {
|
|
|
|
console.warn(error);
|
2021-10-21 22:05:48 +02:00
|
|
|
}
|
|
|
|
this.setState(
|
|
|
|
{
|
|
|
|
pendingImageElement: null,
|
|
|
|
editingElement: null,
|
2022-03-29 17:10:19 +05:30
|
|
|
activeTool: { ...this.state.activeTool, type: "selection" },
|
2021-10-21 22:05:48 +02:00
|
|
|
},
|
|
|
|
() => {
|
|
|
|
this.actionManager.executeAction(actionFinalize);
|
|
|
|
},
|
|
|
|
);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
private initializeImageDimensions = (
|
|
|
|
imageElement: ExcalidrawImageElement,
|
|
|
|
forceNaturalSize = false,
|
|
|
|
) => {
|
|
|
|
const image =
|
|
|
|
isInitializedImageElement(imageElement) &&
|
|
|
|
this.imageCache.get(imageElement.fileId)?.image;
|
|
|
|
|
|
|
|
if (!image || image instanceof Promise) {
|
|
|
|
if (
|
|
|
|
imageElement.width < DRAGGING_THRESHOLD / this.state.zoom.value &&
|
|
|
|
imageElement.height < DRAGGING_THRESHOLD / this.state.zoom.value
|
|
|
|
) {
|
|
|
|
const placeholderSize = 100 / this.state.zoom.value;
|
|
|
|
mutateElement(imageElement, {
|
|
|
|
x: imageElement.x - placeholderSize / 2,
|
|
|
|
y: imageElement.y - placeholderSize / 2,
|
|
|
|
width: placeholderSize,
|
|
|
|
height: placeholderSize,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (
|
|
|
|
forceNaturalSize ||
|
|
|
|
// if user-created bounding box is below threshold, assume the
|
|
|
|
// intention was to click instead of drag, and use the image's
|
|
|
|
// intrinsic size
|
|
|
|
(imageElement.width < DRAGGING_THRESHOLD / this.state.zoom.value &&
|
|
|
|
imageElement.height < DRAGGING_THRESHOLD / this.state.zoom.value)
|
|
|
|
) {
|
|
|
|
const minHeight = Math.max(this.state.height - 120, 160);
|
|
|
|
// max 65% of canvas height, clamped to <300px, vh - 120px>
|
|
|
|
const maxHeight = Math.min(
|
|
|
|
minHeight,
|
|
|
|
Math.floor(this.state.height * 0.5) / this.state.zoom.value,
|
|
|
|
);
|
|
|
|
|
|
|
|
const height = Math.min(image.naturalHeight, maxHeight);
|
|
|
|
const width = height * (image.naturalWidth / image.naturalHeight);
|
|
|
|
|
|
|
|
// add current imageElement width/height to account for previous centering
|
2022-03-02 01:07:12 -05:00
|
|
|
// of the placeholder image
|
2021-10-21 22:05:48 +02:00
|
|
|
const x = imageElement.x + imageElement.width / 2 - width / 2;
|
|
|
|
const y = imageElement.y + imageElement.height / 2 - height / 2;
|
|
|
|
|
|
|
|
mutateElement(imageElement, { x, y, width, height });
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
/** updates image cache, refreshing updated elements and/or setting status
|
|
|
|
to error for images that fail during <img> element creation */
|
|
|
|
private updateImageCache = async (
|
|
|
|
elements: readonly InitializedExcalidrawImageElement[],
|
|
|
|
files = this.files,
|
|
|
|
) => {
|
|
|
|
const { updatedFiles, erroredFiles } = await _updateImageCache({
|
|
|
|
imageCache: this.imageCache,
|
|
|
|
fileIds: elements.map((element) => element.fileId),
|
|
|
|
files,
|
|
|
|
});
|
|
|
|
if (updatedFiles.size || erroredFiles.size) {
|
|
|
|
for (const element of elements) {
|
|
|
|
if (updatedFiles.has(element.fileId)) {
|
|
|
|
invalidateShapeForElement(element);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2021-11-01 10:44:57 +01:00
|
|
|
if (erroredFiles.size) {
|
|
|
|
this.scene.replaceAllElements(
|
|
|
|
this.scene.getElementsIncludingDeleted().map((element) => {
|
|
|
|
if (
|
|
|
|
isInitializedImageElement(element) &&
|
|
|
|
erroredFiles.has(element.fileId)
|
|
|
|
) {
|
|
|
|
return newElementWith(element, {
|
|
|
|
status: "error",
|
|
|
|
});
|
|
|
|
}
|
|
|
|
return element;
|
|
|
|
}),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2021-10-21 22:05:48 +02:00
|
|
|
return { updatedFiles, erroredFiles };
|
|
|
|
};
|
|
|
|
|
|
|
|
/** adds new images to imageCache and re-renders if needed */
|
|
|
|
private addNewImagesToImageCache = async (
|
|
|
|
imageElements: InitializedExcalidrawImageElement[] = getInitializedImageElements(
|
|
|
|
this.scene.getElements(),
|
|
|
|
),
|
|
|
|
files: BinaryFiles = this.files,
|
|
|
|
) => {
|
|
|
|
const uncachedImageElements = imageElements.filter(
|
|
|
|
(element) => !element.isDeleted && !this.imageCache.has(element.fileId),
|
|
|
|
);
|
|
|
|
|
|
|
|
if (uncachedImageElements.length) {
|
|
|
|
const { updatedFiles } = await this.updateImageCache(
|
|
|
|
uncachedImageElements,
|
|
|
|
files,
|
|
|
|
);
|
|
|
|
if (updatedFiles.size) {
|
|
|
|
this.scene.informMutation();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
/** generally you should use `addNewImagesToImageCache()` directly if you need
|
|
|
|
* to render new images. This is just a failsafe */
|
|
|
|
private scheduleImageRefresh = throttle(() => {
|
|
|
|
this.addNewImagesToImageCache();
|
|
|
|
}, IMAGE_RENDER_TIMEOUT);
|
|
|
|
|
2020-09-03 16:12:01 +02:00
|
|
|
private updateBindingEnabledOnPointerMove = (
|
|
|
|
event: React.PointerEvent<HTMLCanvasElement>,
|
|
|
|
) => {
|
|
|
|
const shouldEnableBinding = shouldEnableBindingForPointerEvent(event);
|
|
|
|
if (this.state.isBindingEnabled !== shouldEnableBinding) {
|
|
|
|
this.setState({ isBindingEnabled: shouldEnableBinding });
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
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] : [],
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2021-12-13 13:35:07 +01:00
|
|
|
private maybeSuggestBindingsForLinearElementAtCoords = (
|
2020-08-08 21:04:15 -07:00
|
|
|
linearElement: NonDeleted<ExcalidrawLinearElement>,
|
2021-12-13 13:35:07 +01:00
|
|
|
/** scene coords */
|
2020-08-08 21:04:15 -07:00
|
|
|
pointerCoords: {
|
|
|
|
x: number;
|
|
|
|
y: number;
|
2021-12-13 13:35:07 +01:00
|
|
|
}[],
|
2020-08-08 21:04:15 -07:00
|
|
|
// During line creation the start binding hasn't been written yet
|
|
|
|
// into `linearElement`
|
|
|
|
oppositeBindingBoundElement?: ExcalidrawBindableElement | null,
|
|
|
|
): void => {
|
2021-12-13 13:35:07 +01:00
|
|
|
if (!pointerCoords.length) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const suggestedBindings = pointerCoords.reduce(
|
|
|
|
(acc: NonDeleted<ExcalidrawBindableElement>[], coords) => {
|
|
|
|
const hoveredBindableElement = getHoveredElementForBinding(
|
|
|
|
coords,
|
|
|
|
this.scene,
|
|
|
|
);
|
|
|
|
if (
|
|
|
|
hoveredBindableElement != null &&
|
|
|
|
!isLinearElementSimpleAndAlreadyBound(
|
|
|
|
linearElement,
|
|
|
|
oppositeBindingBoundElement?.id,
|
|
|
|
hoveredBindableElement,
|
|
|
|
)
|
|
|
|
) {
|
|
|
|
acc.push(hoveredBindableElement);
|
|
|
|
}
|
|
|
|
return acc;
|
|
|
|
},
|
|
|
|
[],
|
2020-08-08 21:04:15 -07:00
|
|
|
);
|
2021-12-13 13:35:07 +01:00
|
|
|
|
|
|
|
this.setState({ suggestedBindings });
|
2020-08-08 21:04:15 -07:00
|
|
|
};
|
|
|
|
|
|
|
|
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,
|
|
|
|
}));
|
|
|
|
this.setState({
|
|
|
|
selectedElementIds: {},
|
2020-08-27 20:59:46 +02:00
|
|
|
previousSelectedElementIds: this.state.selectedElementIds,
|
2020-07-08 22:07:51 -07:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
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
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2021-04-06 22:17:00 +07:00
|
|
|
private handleAppOnDrop = async (event: React.DragEvent<HTMLDivElement>) => {
|
2020-10-13 14:47:07 +02:00
|
|
|
try {
|
2022-03-09 10:51:13 +00:00
|
|
|
const file = event.dataTransfer.files.item(0);
|
2021-10-21 22:05:48 +02:00
|
|
|
|
|
|
|
if (isSupportedImageFile(file)) {
|
|
|
|
// first attempt to decode scene from the image if it's embedded
|
|
|
|
// ---------------------------------------------------------------------
|
|
|
|
|
|
|
|
if (file?.type === MIME_TYPES.png || file?.type === MIME_TYPES.svg) {
|
2021-07-15 09:54:26 -04:00
|
|
|
try {
|
2021-10-21 22:05:48 +02:00
|
|
|
if (nativeFileSystemSupported) {
|
|
|
|
try {
|
|
|
|
// This will only work as of Chrome 86,
|
|
|
|
// but can be safely ignored on older releases.
|
|
|
|
const item = event.dataTransfer.items[0];
|
2021-11-01 15:24:05 +02:00
|
|
|
(file as any).handle = await (
|
|
|
|
item as any
|
|
|
|
).getAsFileSystemHandle();
|
2021-10-21 22:05:48 +02:00
|
|
|
} catch (error: any) {
|
|
|
|
console.warn(error.name, error.message);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const scene = await loadFromBlob(
|
|
|
|
file,
|
|
|
|
this.state,
|
|
|
|
this.scene.getElementsIncludingDeleted(),
|
|
|
|
);
|
|
|
|
this.syncActionResult({
|
|
|
|
...scene,
|
|
|
|
appState: {
|
|
|
|
...(scene.appState || this.state),
|
|
|
|
isLoading: false,
|
|
|
|
},
|
|
|
|
replaceFiles: true,
|
|
|
|
commitToHistory: true,
|
|
|
|
});
|
|
|
|
return;
|
2021-10-11 19:11:26 +01:00
|
|
|
} catch (error: any) {
|
2021-10-21 22:05:48 +02:00
|
|
|
if (error.name !== "EncodingError") {
|
|
|
|
throw error;
|
|
|
|
}
|
2021-07-15 09:54:26 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-10-21 22:05:48 +02:00
|
|
|
// if no scene is embedded or we fail for whatever reason, fall back
|
|
|
|
// to importing as regular image
|
|
|
|
// ---------------------------------------------------------------------
|
|
|
|
|
|
|
|
const { x: sceneX, y: sceneY } = viewportCoordsToSceneCoords(
|
|
|
|
event,
|
2021-07-04 22:23:35 +02:00
|
|
|
this.state,
|
|
|
|
);
|
2021-10-21 22:05:48 +02:00
|
|
|
|
|
|
|
const imageElement = this.createImageElement({ sceneX, sceneY });
|
|
|
|
this.insertImageElement(imageElement, file);
|
|
|
|
this.initializeImageDimensions(imageElement);
|
|
|
|
this.setState({ selectedElementIds: { [imageElement.id]: true } });
|
|
|
|
|
2020-10-13 14:47:07 +02:00
|
|
|
return;
|
|
|
|
}
|
2021-10-11 19:11:26 +01:00
|
|
|
} catch (error: any) {
|
2020-10-13 14:47:07 +02:00
|
|
|
return this.setState({
|
|
|
|
isLoading: false,
|
|
|
|
errorMessage: error.message,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2020-10-30 21:01:41 +01:00
|
|
|
const libraryShapes = event.dataTransfer.getData(MIME_TYPES.excalidrawlib);
|
2020-07-10 02:20:23 -07:00
|
|
|
if (libraryShapes !== "") {
|
2021-04-18 13:33:05 +02:00
|
|
|
this.addElementsFromPasteOrLibrary({
|
|
|
|
elements: JSON.parse(libraryShapes),
|
|
|
|
position: event,
|
2021-10-21 22:05:48 +02:00
|
|
|
files: null,
|
2021-04-18 13:33:05 +02:00
|
|
|
});
|
2020-07-10 02:20:23 -07:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2022-03-09 10:51:13 +00:00
|
|
|
const file = event.dataTransfer?.files.item(0);
|
2020-04-04 15:27:53 +02:00
|
|
|
if (
|
2021-03-26 22:12:02 +01:00
|
|
|
file?.type === MIME_TYPES.excalidrawlib ||
|
|
|
|
file?.name?.endsWith(".excalidrawlib")
|
2020-04-04 15:27:53 +02:00
|
|
|
) {
|
2021-04-21 23:38:24 +05:30
|
|
|
this.library
|
|
|
|
.importLibrary(file)
|
2021-03-26 22:12:02 +01:00
|
|
|
.then(() => {
|
2021-04-04 19:06:10 +07:00
|
|
|
// Close and then open to get the libraries updated
|
2021-03-26 22:12:02 +01:00
|
|
|
this.setState({ isLibraryOpen: false });
|
2021-04-04 19:06:10 +07:00
|
|
|
this.setState({ isLibraryOpen: true });
|
2021-03-26 22:12:02 +01:00
|
|
|
})
|
|
|
|
.catch((error) =>
|
|
|
|
this.setState({ isLoading: false, errorMessage: error.message }),
|
|
|
|
);
|
|
|
|
// default: assume an Excalidraw file regardless of extension/MimeType
|
2022-03-09 10:51:13 +00:00
|
|
|
} else if (file) {
|
2020-04-04 15:27:53 +02:00
|
|
|
this.setState({ isLoading: true });
|
2021-10-07 13:19:40 +02:00
|
|
|
if (nativeFileSystemSupported) {
|
2020-08-31 13:11:14 +02:00
|
|
|
try {
|
|
|
|
// This will only work as of Chrome 86,
|
|
|
|
// but can be safely ignored on older releases.
|
|
|
|
const item = event.dataTransfer.items[0];
|
2020-10-19 10:53:37 +02:00
|
|
|
(file as any).handle = await (item as any).getAsFileSystemHandle();
|
2021-10-11 19:11:26 +01:00
|
|
|
} catch (error: any) {
|
2020-08-31 13:11:14 +02:00
|
|
|
console.warn(error.name, error.message);
|
|
|
|
}
|
|
|
|
}
|
2021-03-26 22:12:02 +01:00
|
|
|
await this.loadFileToCanvas(file);
|
2020-04-04 15:27:53 +02:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2021-03-14 03:12:54 +05:30
|
|
|
loadFileToCanvas = (file: Blob) => {
|
2021-07-05 13:43:53 +02:00
|
|
|
loadFromBlob(file, this.state, this.scene.getElementsIncludingDeleted())
|
2021-10-21 22:05:48 +02:00
|
|
|
.then((scene) => {
|
2021-03-14 03:12:54 +05:30
|
|
|
this.syncActionResult({
|
2021-10-21 22:05:48 +02:00
|
|
|
...scene,
|
2021-03-14 03:12:54 +05:30
|
|
|
appState: {
|
2021-10-21 22:05:48 +02:00
|
|
|
...(scene.appState || this.state),
|
2021-03-14 03:12:54 +05:30
|
|
|
isLoading: false,
|
|
|
|
},
|
2021-10-21 22:05:48 +02:00
|
|
|
replaceFiles: true,
|
2021-03-14 03:12:54 +05:30
|
|
|
commitToHistory: true,
|
2021-10-21 22:05:48 +02:00
|
|
|
});
|
|
|
|
})
|
2021-03-14 03:12:54 +05:30
|
|
|
.catch((error) => {
|
|
|
|
this.setState({ isLoading: false, errorMessage: error.message });
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2020-03-21 09:03:17 -07:00
|
|
|
private handleCanvasContextMenu = (
|
|
|
|
event: React.PointerEvent<HTMLCanvasElement>,
|
|
|
|
) => {
|
|
|
|
event.preventDefault();
|
2021-03-26 16:41:57 +01:00
|
|
|
|
2022-02-07 20:01:36 +01:00
|
|
|
if (
|
|
|
|
(event.nativeEvent.pointerType === "touch" ||
|
|
|
|
(event.nativeEvent.pointerType === "pen" &&
|
|
|
|
// always allow if user uses a pen secondary button
|
|
|
|
event.button !== POINTER_BUTTON.SECONDARY)) &&
|
2022-03-25 20:46:01 +05:30
|
|
|
this.state.activeTool.type !== "selection"
|
2022-02-07 20:01:36 +01:00
|
|
|
) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2021-03-26 16:41:57 +01:00
|
|
|
const { x, y } = viewportCoordsToSceneCoords(event, this.state);
|
2021-05-29 22:33:53 +02:00
|
|
|
const element = this.getElementAtPosition(x, y, { preferSelected: true });
|
2021-03-26 16:41:57 +01:00
|
|
|
|
|
|
|
const type = element ? "element" : "canvas";
|
|
|
|
|
2021-04-05 17:26:37 +02:00
|
|
|
const container = this.excalidrawContainerRef.current!;
|
2021-11-01 15:24:05 +02:00
|
|
|
const { top: offsetTop, left: offsetLeft } =
|
|
|
|
container.getBoundingClientRect();
|
2021-04-05 17:26:37 +02:00
|
|
|
const left = event.clientX - offsetLeft;
|
|
|
|
const top = event.clientY - offsetTop;
|
|
|
|
|
2021-03-26 16:41:57 +01:00
|
|
|
if (element && !this.state.selectedElementIds[element.id]) {
|
|
|
|
this.setState({ selectedElementIds: { [element.id]: true } }, () => {
|
2021-04-05 17:26:37 +02:00
|
|
|
this._openContextMenu({ top, left }, type);
|
2021-03-26 16:41:57 +01:00
|
|
|
});
|
|
|
|
} else {
|
2021-04-05 17:26:37 +02:00
|
|
|
this._openContextMenu({ top, left }, type);
|
2021-03-26 16:41:57 +01:00
|
|
|
}
|
2020-07-02 22:12:56 +01:00
|
|
|
};
|
2020-03-21 09:03:17 -07:00
|
|
|
|
2020-12-06 22:39:31 +00:00
|
|
|
private maybeDragNewGenericElement = (
|
|
|
|
pointerDownState: PointerDownState,
|
|
|
|
event: MouseEvent | KeyboardEvent,
|
|
|
|
): void => {
|
|
|
|
const draggingElement = this.state.draggingElement;
|
|
|
|
const pointerCoords = pointerDownState.lastCoords;
|
|
|
|
if (!draggingElement) {
|
|
|
|
return;
|
|
|
|
}
|
2022-03-25 20:46:01 +05:30
|
|
|
if (
|
|
|
|
draggingElement.type === "selection" &&
|
|
|
|
this.state.activeTool.type !== "eraser"
|
|
|
|
) {
|
2020-12-06 22:39:31 +00:00
|
|
|
dragNewElement(
|
|
|
|
draggingElement,
|
2022-03-25 20:46:01 +05:30
|
|
|
this.state.activeTool.type,
|
2020-12-06 22:39:31 +00:00
|
|
|
pointerDownState.origin.x,
|
|
|
|
pointerDownState.origin.y,
|
|
|
|
pointerCoords.x,
|
|
|
|
pointerCoords.y,
|
|
|
|
distance(pointerDownState.origin.x, pointerCoords.x),
|
|
|
|
distance(pointerDownState.origin.y, pointerCoords.y),
|
2021-10-21 22:05:48 +02:00
|
|
|
shouldMaintainAspectRatio(event),
|
|
|
|
shouldResizeFromCenter(event),
|
2020-12-06 22:39:31 +00:00
|
|
|
);
|
|
|
|
} else {
|
|
|
|
const [gridX, gridY] = getGridPoint(
|
|
|
|
pointerCoords.x,
|
|
|
|
pointerCoords.y,
|
|
|
|
this.state.gridSize,
|
|
|
|
);
|
2021-10-21 22:05:48 +02:00
|
|
|
|
|
|
|
const image =
|
|
|
|
isInitializedImageElement(draggingElement) &&
|
|
|
|
this.imageCache.get(draggingElement.fileId)?.image;
|
|
|
|
const aspectRatio =
|
|
|
|
image && !(image instanceof Promise)
|
|
|
|
? image.width / image.height
|
|
|
|
: null;
|
|
|
|
|
2020-12-06 22:39:31 +00:00
|
|
|
dragNewElement(
|
|
|
|
draggingElement,
|
2022-03-25 20:46:01 +05:30
|
|
|
this.state.activeTool.type,
|
2020-12-06 22:39:31 +00:00
|
|
|
pointerDownState.originInGrid.x,
|
|
|
|
pointerDownState.originInGrid.y,
|
|
|
|
gridX,
|
|
|
|
gridY,
|
|
|
|
distance(pointerDownState.originInGrid.x, gridX),
|
|
|
|
distance(pointerDownState.originInGrid.y, gridY),
|
2021-10-21 22:05:48 +02:00
|
|
|
isImageElement(draggingElement)
|
|
|
|
? !shouldMaintainAspectRatio(event)
|
|
|
|
: shouldMaintainAspectRatio(event),
|
|
|
|
shouldResizeFromCenter(event),
|
|
|
|
aspectRatio,
|
2020-12-06 22:39:31 +00:00
|
|
|
);
|
2021-10-21 22:05:48 +02:00
|
|
|
|
2020-12-06 22:39:31 +00:00
|
|
|
this.maybeSuggestBindingForAll([draggingElement]);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
private maybeHandleResize = (
|
|
|
|
pointerDownState: PointerDownState,
|
|
|
|
event: MouseEvent | KeyboardEvent,
|
|
|
|
): boolean => {
|
|
|
|
const selectedElements = getSelectedElements(
|
|
|
|
this.scene.getElements(),
|
|
|
|
this.state,
|
|
|
|
);
|
|
|
|
const transformHandleType = pointerDownState.resize.handleType;
|
|
|
|
this.setState({
|
|
|
|
// TODO: rename this state field to "isScaling" to distinguish
|
|
|
|
// it from the generic "isResizing" which includes scaling and
|
|
|
|
// rotating
|
|
|
|
isResizing: transformHandleType && transformHandleType !== "rotation",
|
|
|
|
isRotating: transformHandleType === "rotation",
|
|
|
|
});
|
|
|
|
const pointerCoords = pointerDownState.lastCoords;
|
|
|
|
const [resizeX, resizeY] = getGridPoint(
|
|
|
|
pointerCoords.x - pointerDownState.resize.offset.x,
|
|
|
|
pointerCoords.y - pointerDownState.resize.offset.y,
|
|
|
|
this.state.gridSize,
|
|
|
|
);
|
|
|
|
if (
|
|
|
|
transformElements(
|
|
|
|
pointerDownState,
|
|
|
|
transformHandleType,
|
|
|
|
selectedElements,
|
|
|
|
pointerDownState.resize.arrowDirection,
|
2021-10-21 22:05:48 +02:00
|
|
|
shouldRotateWithDiscreteAngle(event),
|
|
|
|
shouldResizeFromCenter(event),
|
|
|
|
selectedElements.length === 1 && isImageElement(selectedElements[0])
|
|
|
|
? !shouldMaintainAspectRatio(event)
|
|
|
|
: shouldMaintainAspectRatio(event),
|
2020-12-06 22:39:31 +00:00
|
|
|
resizeX,
|
|
|
|
resizeY,
|
|
|
|
pointerDownState.resize.center.x,
|
|
|
|
pointerDownState.resize.center.y,
|
|
|
|
)
|
|
|
|
) {
|
|
|
|
this.maybeSuggestBindingForAll(selectedElements);
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
};
|
|
|
|
|
2021-03-26 16:41:57 +01:00
|
|
|
/** @private use this.handleCanvasContextMenu */
|
|
|
|
private _openContextMenu = (
|
|
|
|
{
|
2021-04-05 17:26:37 +02:00
|
|
|
left,
|
|
|
|
top,
|
2021-03-26 16:41:57 +01:00
|
|
|
}: {
|
2021-04-05 17:26:37 +02:00
|
|
|
left: number;
|
|
|
|
top: number;
|
2021-03-26 16:41:57 +01:00
|
|
|
},
|
|
|
|
type: "canvas" | "element",
|
|
|
|
) => {
|
2022-02-03 20:34:59 +05:30
|
|
|
if (this.state.showHyperlinkPopup) {
|
|
|
|
this.setState({ showHyperlinkPopup: false });
|
|
|
|
}
|
|
|
|
this.contextMenuOpen = true;
|
2021-01-28 00:41:17 +05:30
|
|
|
const maybeGroupAction = actionGroup.contextItemPredicate!(
|
|
|
|
this.actionManager.getElementsIncludingDeleted(),
|
|
|
|
this.actionManager.getAppState(),
|
|
|
|
);
|
|
|
|
|
|
|
|
const maybeUngroupAction = actionUngroup.contextItemPredicate!(
|
|
|
|
this.actionManager.getElementsIncludingDeleted(),
|
|
|
|
this.actionManager.getAppState(),
|
|
|
|
);
|
|
|
|
|
2021-03-26 11:45:08 -04:00
|
|
|
const maybeFlipHorizontal = actionFlipHorizontal.contextItemPredicate!(
|
|
|
|
this.actionManager.getElementsIncludingDeleted(),
|
|
|
|
this.actionManager.getAppState(),
|
|
|
|
);
|
|
|
|
|
|
|
|
const maybeFlipVertical = actionFlipVertical.contextItemPredicate!(
|
|
|
|
this.actionManager.getElementsIncludingDeleted(),
|
|
|
|
this.actionManager.getAppState(),
|
|
|
|
);
|
|
|
|
|
2022-03-21 17:54:54 +05:30
|
|
|
const mayBeAllowUnbinding = actionUnbindText.contextItemPredicate(
|
|
|
|
this.actionManager.getElementsIncludingDeleted(),
|
|
|
|
this.actionManager.getAppState(),
|
|
|
|
);
|
|
|
|
|
|
|
|
const mayBeAllowBinding = actionBindText.contextItemPredicate(
|
|
|
|
this.actionManager.getElementsIncludingDeleted(),
|
|
|
|
this.actionManager.getAppState(),
|
|
|
|
);
|
|
|
|
|
2021-01-28 00:41:17 +05:30
|
|
|
const separator = "separator";
|
|
|
|
|
2020-07-30 14:50:59 +05:30
|
|
|
const elements = this.scene.getElements();
|
2021-03-26 16:41:57 +01:00
|
|
|
|
2022-04-05 15:31:19 +02:00
|
|
|
const isTextNodesOnly = elements.every((element) => isTextElement(element));
|
|
|
|
|
2021-02-02 02:26:42 +05:30
|
|
|
const options: ContextMenuOption[] = [];
|
|
|
|
if (probablySupportsClipboardBlob && elements.length > 0) {
|
|
|
|
options.push(actionCopyAsPng);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (probablySupportsClipboardWriteText && elements.length > 0) {
|
|
|
|
options.push(actionCopyAsSvg);
|
|
|
|
}
|
2022-04-05 15:31:19 +02:00
|
|
|
|
|
|
|
if (
|
|
|
|
probablySupportsClipboardWriteText &&
|
|
|
|
elements.length > 0 &&
|
|
|
|
isTextNodesOnly
|
|
|
|
) {
|
|
|
|
options.push(copyAllTextNodesAsText);
|
|
|
|
}
|
2021-03-26 16:41:57 +01:00
|
|
|
if (type === "canvas") {
|
2021-02-12 14:10:40 +05:30
|
|
|
const viewModeOptions = [
|
2021-02-02 02:26:42 +05:30
|
|
|
...options,
|
2021-02-12 14:10:40 +05:30
|
|
|
typeof this.props.gridModeEnabled === "undefined" &&
|
|
|
|
actionToggleGridMode,
|
|
|
|
typeof this.props.zenModeEnabled === "undefined" && actionToggleZenMode,
|
|
|
|
typeof this.props.viewModeEnabled === "undefined" &&
|
|
|
|
actionToggleViewMode,
|
2021-02-02 02:26:42 +05:30
|
|
|
actionToggleStats,
|
|
|
|
];
|
|
|
|
|
|
|
|
if (this.state.viewModeEnabled) {
|
2021-10-04 15:43:17 +05:30
|
|
|
ContextMenu.push({
|
|
|
|
options: viewModeOptions,
|
|
|
|
top,
|
|
|
|
left,
|
|
|
|
actionManager: this.actionManager,
|
|
|
|
appState: this.state,
|
|
|
|
container: this.excalidrawContainerRef.current!,
|
2022-01-27 17:47:23 +05:30
|
|
|
elements,
|
2021-10-04 15:43:17 +05:30
|
|
|
});
|
|
|
|
} else {
|
|
|
|
ContextMenu.push({
|
|
|
|
options: [
|
2022-03-16 15:59:30 +01:00
|
|
|
this.deviceType.isMobile &&
|
2021-10-04 15:43:17 +05:30
|
|
|
navigator.clipboard && {
|
2022-03-28 14:46:40 +02:00
|
|
|
trackEvent: false,
|
2021-10-04 15:43:17 +05:30
|
|
|
name: "paste",
|
|
|
|
perform: (elements, appStates) => {
|
|
|
|
this.pasteFromClipboard(null);
|
|
|
|
return {
|
|
|
|
commitToHistory: false,
|
|
|
|
};
|
|
|
|
},
|
|
|
|
contextItemLabel: "labels.paste",
|
|
|
|
},
|
2022-03-16 15:59:30 +01:00
|
|
|
this.deviceType.isMobile && navigator.clipboard && separator,
|
2021-10-04 15:43:17 +05:30
|
|
|
probablySupportsClipboardBlob &&
|
|
|
|
elements.length > 0 &&
|
|
|
|
actionCopyAsPng,
|
|
|
|
probablySupportsClipboardWriteText &&
|
|
|
|
elements.length > 0 &&
|
|
|
|
actionCopyAsSvg,
|
2022-04-05 15:31:19 +02:00
|
|
|
probablySupportsClipboardWriteText &&
|
|
|
|
elements.length > 0 &&
|
|
|
|
isTextNodesOnly &&
|
|
|
|
copyAllTextNodesAsText,
|
2021-10-04 15:43:17 +05:30
|
|
|
((probablySupportsClipboardBlob && elements.length > 0) ||
|
|
|
|
(probablySupportsClipboardWriteText && elements.length > 0)) &&
|
|
|
|
separator,
|
|
|
|
actionSelectAll,
|
|
|
|
separator,
|
|
|
|
typeof this.props.gridModeEnabled === "undefined" &&
|
|
|
|
actionToggleGridMode,
|
|
|
|
typeof this.props.zenModeEnabled === "undefined" &&
|
|
|
|
actionToggleZenMode,
|
|
|
|
typeof this.props.viewModeEnabled === "undefined" &&
|
|
|
|
actionToggleViewMode,
|
|
|
|
actionToggleStats,
|
|
|
|
],
|
|
|
|
top,
|
|
|
|
left,
|
|
|
|
actionManager: this.actionManager,
|
|
|
|
appState: this.state,
|
|
|
|
container: this.excalidrawContainerRef.current!,
|
2022-01-27 17:47:23 +05:30
|
|
|
elements,
|
2021-10-04 15:43:17 +05:30
|
|
|
});
|
2021-02-02 02:26:42 +05:30
|
|
|
}
|
2021-10-04 15:43:17 +05:30
|
|
|
} else if (type === "element") {
|
|
|
|
if (this.state.viewModeEnabled) {
|
|
|
|
ContextMenu.push({
|
|
|
|
options: [navigator.clipboard && actionCopy, ...options],
|
|
|
|
top,
|
|
|
|
left,
|
|
|
|
actionManager: this.actionManager,
|
|
|
|
appState: this.state,
|
|
|
|
container: this.excalidrawContainerRef.current!,
|
2022-01-27 17:47:23 +05:30
|
|
|
elements,
|
2021-10-04 15:43:17 +05:30
|
|
|
});
|
|
|
|
} else {
|
|
|
|
ContextMenu.push({
|
|
|
|
options: [
|
2022-03-16 15:59:30 +01:00
|
|
|
this.deviceType.isMobile && actionCut,
|
|
|
|
this.deviceType.isMobile && navigator.clipboard && actionCopy,
|
|
|
|
this.deviceType.isMobile &&
|
2021-10-04 15:43:17 +05:30
|
|
|
navigator.clipboard && {
|
|
|
|
name: "paste",
|
2022-03-28 14:46:40 +02:00
|
|
|
trackEvent: false,
|
2021-10-04 15:43:17 +05:30
|
|
|
perform: (elements, appStates) => {
|
|
|
|
this.pasteFromClipboard(null);
|
|
|
|
return {
|
|
|
|
commitToHistory: false,
|
|
|
|
};
|
|
|
|
},
|
|
|
|
contextItemLabel: "labels.paste",
|
2021-01-29 02:32:00 +05:30
|
|
|
},
|
2022-03-16 15:59:30 +01:00
|
|
|
this.deviceType.isMobile && separator,
|
2021-10-04 15:43:17 +05:30
|
|
|
...options,
|
2021-01-28 00:41:17 +05:30
|
|
|
separator,
|
2021-10-04 15:43:17 +05:30
|
|
|
actionCopyStyles,
|
|
|
|
actionPasteStyles,
|
|
|
|
separator,
|
|
|
|
maybeGroupAction && actionGroup,
|
2022-03-21 17:54:54 +05:30
|
|
|
mayBeAllowUnbinding && actionUnbindText,
|
|
|
|
mayBeAllowBinding && actionBindText,
|
2021-10-04 15:43:17 +05:30
|
|
|
maybeUngroupAction && actionUngroup,
|
|
|
|
(maybeGroupAction || maybeUngroupAction) && separator,
|
|
|
|
actionAddToLibrary,
|
|
|
|
separator,
|
|
|
|
actionSendBackward,
|
|
|
|
actionBringForward,
|
|
|
|
actionSendToBack,
|
|
|
|
actionBringToFront,
|
|
|
|
separator,
|
|
|
|
maybeFlipHorizontal && actionFlipHorizontal,
|
|
|
|
maybeFlipVertical && actionFlipVertical,
|
|
|
|
(maybeFlipHorizontal || maybeFlipVertical) && separator,
|
2022-02-03 20:34:59 +05:30
|
|
|
actionLink.contextItemPredicate(elements, this.state) && actionLink,
|
2021-10-04 15:43:17 +05:30
|
|
|
actionDuplicateSelection,
|
|
|
|
actionDeleteSelected,
|
|
|
|
],
|
|
|
|
top,
|
|
|
|
left,
|
|
|
|
actionManager: this.actionManager,
|
|
|
|
appState: this.state,
|
|
|
|
container: this.excalidrawContainerRef.current!,
|
2022-01-27 17:47:23 +05:30
|
|
|
elements,
|
2021-10-04 15:43:17 +05:30
|
|
|
});
|
|
|
|
}
|
2021-02-02 02:26:42 +05:30
|
|
|
}
|
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();
|
2020-09-21 01:15:40 -07:00
|
|
|
|
|
|
|
if (isPanning) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2020-03-07 10:20:38 -05:00
|
|
|
const { deltaX, deltaY } = event;
|
2020-06-02 18:41:40 +02:00
|
|
|
const { selectedElementIds, previousSelectedElementIds } = this.state;
|
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-11-04 17:49:15 +00:00
|
|
|
|
2021-01-30 18:03:23 +01:00
|
|
|
let newZoom = this.state.zoom.value - delta / 100;
|
|
|
|
// increase zoom steps the more zoomed-in we are (applies to >100% only)
|
|
|
|
newZoom += Math.log10(Math.max(1, this.state.zoom.value)) * -sign;
|
|
|
|
// round to nearest step
|
|
|
|
newZoom = Math.round(newZoom * ZOOM_STEP * 100) / (ZOOM_STEP * 100);
|
|
|
|
|
2022-01-29 21:12:44 +01:00
|
|
|
this.setState((state) => ({
|
|
|
|
...getStateForZoom(
|
2020-12-14 14:14:56 +00:00
|
|
|
{
|
2022-01-29 21:12:44 +01:00
|
|
|
viewportX: cursorX,
|
|
|
|
viewportY: cursorY,
|
|
|
|
nextZoom: getNormalizedZoom(newZoom),
|
2020-12-14 14:14:56 +00:00
|
|
|
},
|
2022-01-29 21:12:44 +01:00
|
|
|
state,
|
2020-12-14 14:14:56 +00:00
|
|
|
),
|
2020-06-02 18:41:40 +02:00
|
|
|
selectedElementIds: {},
|
|
|
|
previousSelectedElementIds:
|
|
|
|
Object.keys(selectedElementIds).length !== 0
|
|
|
|
? selectedElementIds
|
|
|
|
: previousSelectedElementIds,
|
2020-09-21 12:16:34 -07:00
|
|
|
shouldCacheIgnoreZoom: true,
|
2020-03-07 10:20:38 -05:00
|
|
|
}));
|
2020-09-21 12:16:34 -07:00
|
|
|
this.resetShouldCacheIgnoreZoomDebounced();
|
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
|
2021-01-31 10:47:43 +01:00
|
|
|
scrollX: scrollX - (deltaY || deltaX) / zoom.value,
|
2020-04-30 22:34:38 +02:00
|
|
|
}));
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2020-03-07 10:20:38 -05:00
|
|
|
this.setState(({ zoom, scrollX, scrollY }) => ({
|
2021-01-31 10:47:43 +01:00
|
|
|
scrollX: scrollX - deltaX / zoom.value,
|
|
|
|
scrollY: scrollY - deltaY / zoom.value,
|
2020-03-07 10:20:38 -05:00
|
|
|
}));
|
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,
|
|
|
|
) {
|
2022-03-02 17:04:09 +01:00
|
|
|
const elementClickedInside = getTextBindableContainerAtPosition(
|
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
|
|
|
);
|
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,
|
|
|
|
);
|
|
|
|
|
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
|
|
|
|
}
|
2020-12-05 20:00:53 +05:30
|
|
|
|
|
|
|
this.props.onPointerUpdate?.({
|
|
|
|
pointer,
|
|
|
|
button,
|
|
|
|
pointersMap: gesture.pointers,
|
|
|
|
});
|
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(() => {
|
2021-01-13 17:42:42 +01:00
|
|
|
if (!this.unmounted) {
|
|
|
|
this.setState({ shouldCacheIgnoreZoom: false });
|
|
|
|
}
|
2020-04-02 23:56:14 -07:00
|
|
|
}, 300);
|
2020-03-28 16:59:36 -07:00
|
|
|
|
2021-04-04 15:05:16 +05:30
|
|
|
private updateDOMRect = (cb?: () => void) => {
|
|
|
|
if (this.excalidrawContainerRef?.current) {
|
|
|
|
const excalidrawContainer = this.excalidrawContainerRef.current;
|
|
|
|
const {
|
|
|
|
width,
|
|
|
|
height,
|
|
|
|
left: offsetLeft,
|
|
|
|
top: offsetTop,
|
|
|
|
} = excalidrawContainer.getBoundingClientRect();
|
|
|
|
const {
|
|
|
|
width: currentWidth,
|
|
|
|
height: currentHeight,
|
|
|
|
offsetTop: currentOffsetTop,
|
|
|
|
offsetLeft: currentOffsetLeft,
|
|
|
|
} = this.state;
|
|
|
|
|
|
|
|
if (
|
|
|
|
width === currentWidth &&
|
|
|
|
height === currentHeight &&
|
|
|
|
offsetLeft === currentOffsetLeft &&
|
|
|
|
offsetTop === currentOffsetTop
|
|
|
|
) {
|
|
|
|
if (cb) {
|
|
|
|
cb();
|
|
|
|
}
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.setState(
|
|
|
|
{
|
|
|
|
width,
|
|
|
|
height,
|
|
|
|
offsetLeft,
|
|
|
|
offsetTop,
|
|
|
|
},
|
|
|
|
() => {
|
|
|
|
cb && cb();
|
|
|
|
},
|
|
|
|
);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2021-04-04 22:05:02 +05:30
|
|
|
public refresh = () => {
|
2021-03-20 13:00:49 +05:30
|
|
|
this.setState({ ...this.getCanvasOffsets() });
|
|
|
|
};
|
|
|
|
|
|
|
|
private getCanvasOffsets(): Pick<AppState, "offsetTop" | "offsetLeft"> {
|
2021-04-04 15:05:16 +05:30
|
|
|
if (this.excalidrawContainerRef?.current) {
|
|
|
|
const excalidrawContainer = this.excalidrawContainerRef.current;
|
|
|
|
const { left, top } = excalidrawContainer.getBoundingClientRect();
|
2020-07-27 17:18:49 +05:30
|
|
|
return {
|
2021-03-20 13:00:49 +05:30
|
|
|
offsetLeft: left,
|
|
|
|
offsetTop: top,
|
2020-07-27 17:18:49 +05:30
|
|
|
};
|
|
|
|
}
|
|
|
|
return {
|
2021-03-20 13:00:49 +05:30
|
|
|
offsetLeft: 0,
|
|
|
|
offsetTop: 0,
|
2020-07-27 17:18:49 +05:30
|
|
|
};
|
|
|
|
}
|
2021-01-04 02:21:52 +05:30
|
|
|
|
|
|
|
private async updateLanguage() {
|
|
|
|
const currentLang =
|
|
|
|
languages.find((lang) => lang.code === this.props.langCode) ||
|
|
|
|
defaultLang;
|
|
|
|
await setLanguage(currentLang);
|
|
|
|
this.setAppState({});
|
|
|
|
}
|
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"];
|
|
|
|
app: InstanceType<typeof App>;
|
2021-04-24 18:21:02 +05:30
|
|
|
history: History;
|
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-12-05 20:00:53 +05:30
|
|
|
window.h = window.h || ({} as Window["h"]);
|
2020-03-18 20:44:05 +01:00
|
|
|
|
|
|
|
Object.defineProperties(window.h, {
|
|
|
|
elements: {
|
2020-12-05 20:00:53 +05:30
|
|
|
configurable: true,
|
2020-03-18 20:44:05 +01:00
|
|
|
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
|
|
|
},
|
|
|
|
},
|
|
|
|
});
|
|
|
|
}
|
2020-04-12 16:24:52 +05:30
|
|
|
export default App;
|