|  | @@ -42,11 +42,7 @@ import { actions } from "../actions/register";
 | 
	
		
			
				|  |  |  import { ActionResult } from "../actions/types";
 | 
	
		
			
				|  |  |  import { trackEvent } from "../analytics";
 | 
	
		
			
				|  |  |  import { getDefaultAppState, isEraserActive } from "../appState";
 | 
	
		
			
				|  |  | -import {
 | 
	
		
			
				|  |  | -  parseClipboard,
 | 
	
		
			
				|  |  | -  probablySupportsClipboardBlob,
 | 
	
		
			
				|  |  | -  probablySupportsClipboardWriteText,
 | 
	
		
			
				|  |  | -} from "../clipboard";
 | 
	
		
			
				|  |  | +import { parseClipboard } from "../clipboard";
 | 
	
		
			
				|  |  |  import {
 | 
	
		
			
				|  |  |    APP_NAME,
 | 
	
		
			
				|  |  |    CURSOR_TYPE,
 | 
	
	
		
			
				|  | @@ -227,7 +223,11 @@ import {
 | 
	
		
			
				|  |  |    updateActiveTool,
 | 
	
		
			
				|  |  |    getShortcutKey,
 | 
	
		
			
				|  |  |  } from "../utils";
 | 
	
		
			
				|  |  | -import ContextMenu, { ContextMenuOption } from "./ContextMenu";
 | 
	
		
			
				|  |  | +import {
 | 
	
		
			
				|  |  | +  ContextMenu,
 | 
	
		
			
				|  |  | +  ContextMenuItems,
 | 
	
		
			
				|  |  | +  CONTEXT_MENU_SEPARATOR,
 | 
	
		
			
				|  |  | +} from "./ContextMenu";
 | 
	
		
			
				|  |  |  import LayerUI from "./LayerUI";
 | 
	
		
			
				|  |  |  import { Toast } from "./Toast";
 | 
	
		
			
				|  |  |  import { actionToggleViewMode } from "../actions/actionToggleViewMode";
 | 
	
	
		
			
				|  | @@ -274,6 +274,7 @@ import {
 | 
	
		
			
				|  |  |  import { shouldShowBoundingBox } from "../element/transformHandles";
 | 
	
		
			
				|  |  |  import { atom } from "jotai";
 | 
	
		
			
				|  |  |  import { Fonts } from "../scene/Fonts";
 | 
	
		
			
				|  |  | +import { actionPaste } from "../actions/actionClipboard";
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  export const isMenuOpenAtom = atom(false);
 | 
	
		
			
				|  |  |  export const isDropdownOpenAtom = atom(false);
 | 
	
	
		
			
				|  | @@ -383,7 +384,6 @@ class App extends React.Component<AppProps, AppState> {
 | 
	
		
			
				|  |  |    hitLinkElement?: NonDeletedExcalidrawElement;
 | 
	
		
			
				|  |  |    lastPointerDown: React.PointerEvent<HTMLCanvasElement> | null = null;
 | 
	
		
			
				|  |  |    lastPointerUp: React.PointerEvent<HTMLElement> | PointerEvent | null = null;
 | 
	
		
			
				|  |  | -  contextMenuOpen: boolean = false;
 | 
	
		
			
				|  |  |    lastScenePointer: { x: number; y: number } | null = null;
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |    constructor(props: AppProps) {
 | 
	
	
		
			
				|  | @@ -602,6 +602,7 @@ class App extends React.Component<AppProps, AppState> {
 | 
	
		
			
				|  |  |                    <div className="excalidraw-textEditorContainer" />
 | 
	
		
			
				|  |  |                    <div className="excalidraw-contextMenuContainer" />
 | 
	
		
			
				|  |  |                    {selectedElement.length === 1 &&
 | 
	
		
			
				|  |  | +                    !this.state.contextMenu &&
 | 
	
		
			
				|  |  |                      this.state.showHyperlinkPopup && (
 | 
	
		
			
				|  |  |                        <Hyperlink
 | 
	
		
			
				|  |  |                          key={selectedElement[0].id}
 | 
	
	
		
			
				|  | @@ -618,6 +619,14 @@ class App extends React.Component<AppProps, AppState> {
 | 
	
		
			
				|  |  |                        closable={this.state.toast.closable}
 | 
	
		
			
				|  |  |                      />
 | 
	
		
			
				|  |  |                    )}
 | 
	
		
			
				|  |  | +                  {this.state.contextMenu && (
 | 
	
		
			
				|  |  | +                    <ContextMenu
 | 
	
		
			
				|  |  | +                      items={this.state.contextMenu.items}
 | 
	
		
			
				|  |  | +                      top={this.state.contextMenu.top}
 | 
	
		
			
				|  |  | +                      left={this.state.contextMenu.left}
 | 
	
		
			
				|  |  | +                      actionManager={this.actionManager}
 | 
	
		
			
				|  |  | +                    />
 | 
	
		
			
				|  |  | +                  )}
 | 
	
		
			
				|  |  |                    <main>{this.renderCanvas()}</main>
 | 
	
		
			
				|  |  |                  </ExcalidrawElementsContext.Provider>{" "}
 | 
	
		
			
				|  |  |                </ExcalidrawAppStateContext.Provider>
 | 
	
	
		
			
				|  | @@ -644,8 +653,6 @@ class App extends React.Component<AppProps, AppState> {
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |    private syncActionResult = withBatchedUpdates(
 | 
	
		
			
				|  |  |      (actionResult: ActionResult) => {
 | 
	
		
			
				|  |  | -      // Since context menu closes when action triggered so setting to false
 | 
	
		
			
				|  |  | -      this.contextMenuOpen = false;
 | 
	
		
			
				|  |  |        if (this.unmounted || actionResult === false) {
 | 
	
		
			
				|  |  |          return;
 | 
	
		
			
				|  |  |        }
 | 
	
	
		
			
				|  | @@ -674,7 +681,7 @@ class App extends React.Component<AppProps, AppState> {
 | 
	
		
			
				|  |  |          this.addNewImagesToImageCache();
 | 
	
		
			
				|  |  |        }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -      if (actionResult.appState || editingElement) {
 | 
	
		
			
				|  |  | +      if (actionResult.appState || editingElement || this.state.contextMenu) {
 | 
	
		
			
				|  |  |          if (actionResult.commitToHistory) {
 | 
	
		
			
				|  |  |            this.history.resumeRecording();
 | 
	
		
			
				|  |  |          }
 | 
	
	
		
			
				|  | @@ -700,12 +707,17 @@ class App extends React.Component<AppProps, AppState> {
 | 
	
		
			
				|  |  |          if (typeof this.props.name !== "undefined") {
 | 
	
		
			
				|  |  |            name = this.props.name;
 | 
	
		
			
				|  |  |          }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  |          this.setState(
 | 
	
		
			
				|  |  |            (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 || {}, {
 | 
	
		
			
				|  |  | +              // NOTE this will prevent opening context menu using an action
 | 
	
		
			
				|  |  | +              // or programmatically from the host, so it will need to be
 | 
	
		
			
				|  |  | +              // rewritten later
 | 
	
		
			
				|  |  | +              contextMenu: null,
 | 
	
		
			
				|  |  |                editingElement:
 | 
	
		
			
				|  |  |                  editingElement || actionResult.appState?.editingElement || null,
 | 
	
		
			
				|  |  |                viewModeEnabled,
 | 
	
	
		
			
				|  | @@ -1462,7 +1474,7 @@ class App extends React.Component<AppProps, AppState> {
 | 
	
		
			
				|  |  |      }
 | 
	
		
			
				|  |  |    };
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -  private pasteFromClipboard = withBatchedUpdates(
 | 
	
		
			
				|  |  | +  public pasteFromClipboard = withBatchedUpdates(
 | 
	
		
			
				|  |  |      async (event: ClipboardEvent | null) => {
 | 
	
		
			
				|  |  |        const isPlainPaste = !!(IS_PLAIN_PASTE && event);
 | 
	
		
			
				|  |  |  
 | 
	
	
		
			
				|  | @@ -1470,7 +1482,7 @@ class App extends React.Component<AppProps, AppState> {
 | 
	
		
			
				|  |  |        const target = document.activeElement;
 | 
	
		
			
				|  |  |        const isExcalidrawActive =
 | 
	
		
			
				|  |  |          this.excalidrawContainerRef.current?.contains(target);
 | 
	
		
			
				|  |  | -      if (!isExcalidrawActive) {
 | 
	
		
			
				|  |  | +      if (event && !isExcalidrawActive) {
 | 
	
		
			
				|  |  |          return;
 | 
	
		
			
				|  |  |        }
 | 
	
		
			
				|  |  |  
 | 
	
	
		
			
				|  | @@ -1744,10 +1756,11 @@ class App extends React.Component<AppProps, AppState> {
 | 
	
		
			
				|  |  |      this.history.resumeRecording();
 | 
	
		
			
				|  |  |    }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -  // Collaboration
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -  setAppState: React.Component<any, AppState>["setState"] = (state) => {
 | 
	
		
			
				|  |  | -    this.setState(state);
 | 
	
		
			
				|  |  | +  setAppState: React.Component<any, AppState>["setState"] = (
 | 
	
		
			
				|  |  | +    state,
 | 
	
		
			
				|  |  | +    callback,
 | 
	
		
			
				|  |  | +  ) => {
 | 
	
		
			
				|  |  | +    this.setState(state, callback);
 | 
	
		
			
				|  |  |    };
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |    removePointer = (event: React.PointerEvent<HTMLElement> | PointerEvent) => {
 | 
	
	
		
			
				|  | @@ -3101,7 +3114,7 @@ class App extends React.Component<AppProps, AppState> {
 | 
	
		
			
				|  |  |          hitElement &&
 | 
	
		
			
				|  |  |          hitElement.link &&
 | 
	
		
			
				|  |  |          this.state.selectedElementIds[hitElement.id] &&
 | 
	
		
			
				|  |  | -        !this.contextMenuOpen &&
 | 
	
		
			
				|  |  | +        !this.state.contextMenu &&
 | 
	
		
			
				|  |  |          !this.state.showHyperlinkPopup
 | 
	
		
			
				|  |  |        ) {
 | 
	
		
			
				|  |  |          this.setState({ showHyperlinkPopup: "info" });
 | 
	
	
		
			
				|  | @@ -3323,6 +3336,14 @@ class App extends React.Component<AppProps, AppState> {
 | 
	
		
			
				|  |  |    private handleCanvasPointerDown = (
 | 
	
		
			
				|  |  |      event: React.PointerEvent<HTMLCanvasElement>,
 | 
	
		
			
				|  |  |    ) => {
 | 
	
		
			
				|  |  | +    // since contextMenu options are potentially evaluated on each render,
 | 
	
		
			
				|  |  | +    // and an contextMenu action may depend on selection state, we must
 | 
	
		
			
				|  |  | +    // close the contextMenu before we update the selection on pointerDown
 | 
	
		
			
				|  |  | +    // (e.g. resetting selection)
 | 
	
		
			
				|  |  | +    if (this.state.contextMenu) {
 | 
	
		
			
				|  |  | +      this.setState({ contextMenu: null });
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  |      // 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)
 | 
	
	
		
			
				|  | @@ -3389,8 +3410,6 @@ class App extends React.Component<AppProps, AppState> {
 | 
	
		
			
				|  |  |        return;
 | 
	
		
			
				|  |  |      }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -    // Since context menu closes on pointer down so setting to false
 | 
	
		
			
				|  |  | -    this.contextMenuOpen = false;
 | 
	
		
			
				|  |  |      this.clearSelectionIfNotUsingSelection();
 | 
	
		
			
				|  |  |      this.updateBindingEnabledOnPointerMove(event);
 | 
	
		
			
				|  |  |  
 | 
	
	
		
			
				|  | @@ -5949,7 +5968,17 @@ class App extends React.Component<AppProps, AppState> {
 | 
	
		
			
				|  |  |        includeLockedElements: true,
 | 
	
		
			
				|  |  |      });
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -    const type = element ? "element" : "canvas";
 | 
	
		
			
				|  |  | +    const selectedElements = getSelectedElements(
 | 
	
		
			
				|  |  | +      this.scene.getNonDeletedElements(),
 | 
	
		
			
				|  |  | +      this.state,
 | 
	
		
			
				|  |  | +    );
 | 
	
		
			
				|  |  | +    const isHittignCommonBoundBox =
 | 
	
		
			
				|  |  | +      this.isHittingCommonBoundingBoxOfSelectedElements(
 | 
	
		
			
				|  |  | +        { x, y },
 | 
	
		
			
				|  |  | +        selectedElements,
 | 
	
		
			
				|  |  | +      );
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    const type = element || isHittignCommonBoundBox ? "element" : "canvas";
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |      const container = this.excalidrawContainerRef.current!;
 | 
	
		
			
				|  |  |      const { top: offsetTop, left: offsetLeft } =
 | 
	
	
		
			
				|  | @@ -5957,25 +5986,30 @@ class App extends React.Component<AppProps, AppState> {
 | 
	
		
			
				|  |  |      const left = event.clientX - offsetLeft;
 | 
	
		
			
				|  |  |      const top = event.clientY - offsetTop;
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -    if (element && !this.state.selectedElementIds[element.id]) {
 | 
	
		
			
				|  |  | -      this.setState(
 | 
	
		
			
				|  |  | -        selectGroupsForSelectedElements(
 | 
	
		
			
				|  |  | -          {
 | 
	
		
			
				|  |  | -            ...this.state,
 | 
	
		
			
				|  |  | -            selectedElementIds: { [element.id]: true },
 | 
	
		
			
				|  |  | -            selectedLinearElement: isLinearElement(element)
 | 
	
		
			
				|  |  | -              ? new LinearElementEditor(element, this.scene)
 | 
	
		
			
				|  |  | -              : null,
 | 
	
		
			
				|  |  | -          },
 | 
	
		
			
				|  |  | -          this.scene.getNonDeletedElements(),
 | 
	
		
			
				|  |  | -        ),
 | 
	
		
			
				|  |  | -        () => {
 | 
	
		
			
				|  |  | -          this._openContextMenu({ top, left }, type);
 | 
	
		
			
				|  |  | -        },
 | 
	
		
			
				|  |  | -      );
 | 
	
		
			
				|  |  | -    } else {
 | 
	
		
			
				|  |  | -      this._openContextMenu({ top, left }, type);
 | 
	
		
			
				|  |  | -    }
 | 
	
		
			
				|  |  | +    trackEvent("contextMenu", "openContextMenu", type);
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    this.setState(
 | 
	
		
			
				|  |  | +      {
 | 
	
		
			
				|  |  | +        ...(element && !this.state.selectedElementIds[element.id]
 | 
	
		
			
				|  |  | +          ? selectGroupsForSelectedElements(
 | 
	
		
			
				|  |  | +              {
 | 
	
		
			
				|  |  | +                ...this.state,
 | 
	
		
			
				|  |  | +                selectedElementIds: { [element.id]: true },
 | 
	
		
			
				|  |  | +                selectedLinearElement: isLinearElement(element)
 | 
	
		
			
				|  |  | +                  ? new LinearElementEditor(element, this.scene)
 | 
	
		
			
				|  |  | +                  : null,
 | 
	
		
			
				|  |  | +              },
 | 
	
		
			
				|  |  | +              this.scene.getNonDeletedElements(),
 | 
	
		
			
				|  |  | +            )
 | 
	
		
			
				|  |  | +          : this.state),
 | 
	
		
			
				|  |  | +        showHyperlinkPopup: false,
 | 
	
		
			
				|  |  | +      },
 | 
	
		
			
				|  |  | +      () => {
 | 
	
		
			
				|  |  | +        this.setState({
 | 
	
		
			
				|  |  | +          contextMenu: { top, left, items: this.getContextMenuItems(type) },
 | 
	
		
			
				|  |  | +        });
 | 
	
		
			
				|  |  | +      },
 | 
	
		
			
				|  |  | +    );
 | 
	
		
			
				|  |  |    };
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |    private maybeDragNewGenericElement = (
 | 
	
	
		
			
				|  | @@ -6083,215 +6117,84 @@ class App extends React.Component<AppProps, AppState> {
 | 
	
		
			
				|  |  |      return false;
 | 
	
		
			
				|  |  |    };
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -  /** @private use this.handleCanvasContextMenu */
 | 
	
		
			
				|  |  | -  private _openContextMenu = (
 | 
	
		
			
				|  |  | -    {
 | 
	
		
			
				|  |  | -      left,
 | 
	
		
			
				|  |  | -      top,
 | 
	
		
			
				|  |  | -    }: {
 | 
	
		
			
				|  |  | -      left: number;
 | 
	
		
			
				|  |  | -      top: number;
 | 
	
		
			
				|  |  | -    },
 | 
	
		
			
				|  |  | +  private getContextMenuItems = (
 | 
	
		
			
				|  |  |      type: "canvas" | "element",
 | 
	
		
			
				|  |  | -  ) => {
 | 
	
		
			
				|  |  | -    trackEvent("contextMenu", "openContextMenu", type);
 | 
	
		
			
				|  |  | -    if (this.state.showHyperlinkPopup) {
 | 
	
		
			
				|  |  | -      this.setState({ showHyperlinkPopup: false });
 | 
	
		
			
				|  |  | -    }
 | 
	
		
			
				|  |  | -    this.contextMenuOpen = true;
 | 
	
		
			
				|  |  | -    const maybeGroupAction = actionGroup.contextItemPredicate!(
 | 
	
		
			
				|  |  | -      this.actionManager.getElementsIncludingDeleted(),
 | 
	
		
			
				|  |  | -      this.actionManager.getAppState(),
 | 
	
		
			
				|  |  | -    );
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -    const maybeUngroupAction = actionUngroup.contextItemPredicate!(
 | 
	
		
			
				|  |  | -      this.actionManager.getElementsIncludingDeleted(),
 | 
	
		
			
				|  |  | -      this.actionManager.getAppState(),
 | 
	
		
			
				|  |  | -    );
 | 
	
		
			
				|  |  | +  ): ContextMenuItems => {
 | 
	
		
			
				|  |  | +    const options: ContextMenuItems = [];
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -    const maybeFlipHorizontal = actionFlipHorizontal.contextItemPredicate!(
 | 
	
		
			
				|  |  | -      this.actionManager.getElementsIncludingDeleted(),
 | 
	
		
			
				|  |  | -      this.actionManager.getAppState(),
 | 
	
		
			
				|  |  | -    );
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -    const maybeFlipVertical = actionFlipVertical.contextItemPredicate!(
 | 
	
		
			
				|  |  | -      this.actionManager.getElementsIncludingDeleted(),
 | 
	
		
			
				|  |  | -      this.actionManager.getAppState(),
 | 
	
		
			
				|  |  | -    );
 | 
	
		
			
				|  |  | +    options.push(actionCopyAsPng, actionCopyAsSvg);
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -    const mayBeAllowUnbinding = actionUnbindText.contextItemPredicate(
 | 
	
		
			
				|  |  | -      this.actionManager.getElementsIncludingDeleted(),
 | 
	
		
			
				|  |  | -      this.actionManager.getAppState(),
 | 
	
		
			
				|  |  | -    );
 | 
	
		
			
				|  |  | +    // canvas contextMenu
 | 
	
		
			
				|  |  | +    // -------------------------------------------------------------------------
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -    const mayBeAllowBinding = actionBindText.contextItemPredicate(
 | 
	
		
			
				|  |  | -      this.actionManager.getElementsIncludingDeleted(),
 | 
	
		
			
				|  |  | -      this.actionManager.getAppState(),
 | 
	
		
			
				|  |  | -    );
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -    const mayBeAllowToggleLineEditing =
 | 
	
		
			
				|  |  | -      actionToggleLinearEditor.contextItemPredicate(
 | 
	
		
			
				|  |  | -        this.actionManager.getElementsIncludingDeleted(),
 | 
	
		
			
				|  |  | -        this.actionManager.getAppState(),
 | 
	
		
			
				|  |  | -      );
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -    const separator = "separator";
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -    const elements = this.scene.getNonDeletedElements();
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -    const selectedElements = getSelectedElements(
 | 
	
		
			
				|  |  | -      this.scene.getNonDeletedElements(),
 | 
	
		
			
				|  |  | -      this.state,
 | 
	
		
			
				|  |  | -    );
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -    const options: ContextMenuOption[] = [];
 | 
	
		
			
				|  |  | -    if (probablySupportsClipboardBlob && elements.length > 0) {
 | 
	
		
			
				|  |  | -      options.push(actionCopyAsPng);
 | 
	
		
			
				|  |  | -    }
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -    if (probablySupportsClipboardWriteText && elements.length > 0) {
 | 
	
		
			
				|  |  | -      options.push(actionCopyAsSvg);
 | 
	
		
			
				|  |  | -    }
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -    if (
 | 
	
		
			
				|  |  | -      type === "element" &&
 | 
	
		
			
				|  |  | -      copyText.contextItemPredicate(elements, this.state) &&
 | 
	
		
			
				|  |  | -      probablySupportsClipboardWriteText
 | 
	
		
			
				|  |  | -    ) {
 | 
	
		
			
				|  |  | -      options.push(copyText);
 | 
	
		
			
				|  |  | -    }
 | 
	
		
			
				|  |  |      if (type === "canvas") {
 | 
	
		
			
				|  |  | -      const viewModeOptions = [
 | 
	
		
			
				|  |  | -        ...options,
 | 
	
		
			
				|  |  | -        typeof this.props.gridModeEnabled === "undefined" &&
 | 
	
		
			
				|  |  | +      if (this.state.viewModeEnabled) {
 | 
	
		
			
				|  |  | +        return [
 | 
	
		
			
				|  |  | +          ...options,
 | 
	
		
			
				|  |  |            actionToggleGridMode,
 | 
	
		
			
				|  |  | -        typeof this.props.zenModeEnabled === "undefined" && actionToggleZenMode,
 | 
	
		
			
				|  |  | -        typeof this.props.viewModeEnabled === "undefined" &&
 | 
	
		
			
				|  |  | +          actionToggleZenMode,
 | 
	
		
			
				|  |  |            actionToggleViewMode,
 | 
	
		
			
				|  |  | +          actionToggleStats,
 | 
	
		
			
				|  |  | +        ];
 | 
	
		
			
				|  |  | +      }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +      return [
 | 
	
		
			
				|  |  | +        actionPaste,
 | 
	
		
			
				|  |  | +        CONTEXT_MENU_SEPARATOR,
 | 
	
		
			
				|  |  | +        actionCopyAsPng,
 | 
	
		
			
				|  |  | +        actionCopyAsSvg,
 | 
	
		
			
				|  |  | +        copyText,
 | 
	
		
			
				|  |  | +        CONTEXT_MENU_SEPARATOR,
 | 
	
		
			
				|  |  | +        actionSelectAll,
 | 
	
		
			
				|  |  | +        CONTEXT_MENU_SEPARATOR,
 | 
	
		
			
				|  |  | +        actionToggleGridMode,
 | 
	
		
			
				|  |  | +        actionToggleZenMode,
 | 
	
		
			
				|  |  | +        actionToggleViewMode,
 | 
	
		
			
				|  |  |          actionToggleStats,
 | 
	
		
			
				|  |  |        ];
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -      if (this.state.viewModeEnabled) {
 | 
	
		
			
				|  |  | -        ContextMenu.push({
 | 
	
		
			
				|  |  | -          options: viewModeOptions,
 | 
	
		
			
				|  |  | -          top,
 | 
	
		
			
				|  |  | -          left,
 | 
	
		
			
				|  |  | -          actionManager: this.actionManager,
 | 
	
		
			
				|  |  | -          appState: this.state,
 | 
	
		
			
				|  |  | -          container: this.excalidrawContainerRef.current!,
 | 
	
		
			
				|  |  | -          elements,
 | 
	
		
			
				|  |  | -        });
 | 
	
		
			
				|  |  | -      } else {
 | 
	
		
			
				|  |  | -        ContextMenu.push({
 | 
	
		
			
				|  |  | -          options: [
 | 
	
		
			
				|  |  | -            this.device.isMobile &&
 | 
	
		
			
				|  |  | -              navigator.clipboard && {
 | 
	
		
			
				|  |  | -                trackEvent: false,
 | 
	
		
			
				|  |  | -                name: "paste",
 | 
	
		
			
				|  |  | -                perform: (elements, appStates) => {
 | 
	
		
			
				|  |  | -                  this.pasteFromClipboard(null);
 | 
	
		
			
				|  |  | -                  return {
 | 
	
		
			
				|  |  | -                    commitToHistory: false,
 | 
	
		
			
				|  |  | -                  };
 | 
	
		
			
				|  |  | -                },
 | 
	
		
			
				|  |  | -                contextItemLabel: "labels.paste",
 | 
	
		
			
				|  |  | -              },
 | 
	
		
			
				|  |  | -            this.device.isMobile && navigator.clipboard && separator,
 | 
	
		
			
				|  |  | -            probablySupportsClipboardBlob &&
 | 
	
		
			
				|  |  | -              elements.length > 0 &&
 | 
	
		
			
				|  |  | -              actionCopyAsPng,
 | 
	
		
			
				|  |  | -            probablySupportsClipboardWriteText &&
 | 
	
		
			
				|  |  | -              elements.length > 0 &&
 | 
	
		
			
				|  |  | -              actionCopyAsSvg,
 | 
	
		
			
				|  |  | -            probablySupportsClipboardWriteText &&
 | 
	
		
			
				|  |  | -              selectedElements.length > 0 &&
 | 
	
		
			
				|  |  | -              copyText,
 | 
	
		
			
				|  |  | -            ((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!,
 | 
	
		
			
				|  |  | -          elements,
 | 
	
		
			
				|  |  | -        });
 | 
	
		
			
				|  |  | -      }
 | 
	
		
			
				|  |  | -    } 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!,
 | 
	
		
			
				|  |  | -          elements,
 | 
	
		
			
				|  |  | -        });
 | 
	
		
			
				|  |  | -      } else {
 | 
	
		
			
				|  |  | -        ContextMenu.push({
 | 
	
		
			
				|  |  | -          options: [
 | 
	
		
			
				|  |  | -            this.device.isMobile && actionCut,
 | 
	
		
			
				|  |  | -            this.device.isMobile && navigator.clipboard && actionCopy,
 | 
	
		
			
				|  |  | -            this.device.isMobile &&
 | 
	
		
			
				|  |  | -              navigator.clipboard && {
 | 
	
		
			
				|  |  | -                name: "paste",
 | 
	
		
			
				|  |  | -                trackEvent: false,
 | 
	
		
			
				|  |  | -                perform: (elements, appStates) => {
 | 
	
		
			
				|  |  | -                  this.pasteFromClipboard(null);
 | 
	
		
			
				|  |  | -                  return {
 | 
	
		
			
				|  |  | -                    commitToHistory: false,
 | 
	
		
			
				|  |  | -                  };
 | 
	
		
			
				|  |  | -                },
 | 
	
		
			
				|  |  | -                contextItemLabel: "labels.paste",
 | 
	
		
			
				|  |  | -              },
 | 
	
		
			
				|  |  | -            this.device.isMobile && separator,
 | 
	
		
			
				|  |  | -            ...options,
 | 
	
		
			
				|  |  | -            separator,
 | 
	
		
			
				|  |  | -            actionCopyStyles,
 | 
	
		
			
				|  |  | -            actionPasteStyles,
 | 
	
		
			
				|  |  | -            separator,
 | 
	
		
			
				|  |  | -            maybeGroupAction && actionGroup,
 | 
	
		
			
				|  |  | -            mayBeAllowUnbinding && actionUnbindText,
 | 
	
		
			
				|  |  | -            mayBeAllowBinding && actionBindText,
 | 
	
		
			
				|  |  | -            maybeUngroupAction && actionUngroup,
 | 
	
		
			
				|  |  | -            (maybeGroupAction || maybeUngroupAction) && separator,
 | 
	
		
			
				|  |  | -            actionAddToLibrary,
 | 
	
		
			
				|  |  | -            separator,
 | 
	
		
			
				|  |  | -            actionSendBackward,
 | 
	
		
			
				|  |  | -            actionBringForward,
 | 
	
		
			
				|  |  | -            actionSendToBack,
 | 
	
		
			
				|  |  | -            actionBringToFront,
 | 
	
		
			
				|  |  | -            separator,
 | 
	
		
			
				|  |  | -            maybeFlipHorizontal && actionFlipHorizontal,
 | 
	
		
			
				|  |  | -            maybeFlipVertical && actionFlipVertical,
 | 
	
		
			
				|  |  | -            (maybeFlipHorizontal || maybeFlipVertical) && separator,
 | 
	
		
			
				|  |  | -            mayBeAllowToggleLineEditing && actionToggleLinearEditor,
 | 
	
		
			
				|  |  | -            actionLink.contextItemPredicate(elements, this.state) && actionLink,
 | 
	
		
			
				|  |  | -            actionDuplicateSelection,
 | 
	
		
			
				|  |  | -            actionToggleLock,
 | 
	
		
			
				|  |  | -            separator,
 | 
	
		
			
				|  |  | -            actionDeleteSelected,
 | 
	
		
			
				|  |  | -          ],
 | 
	
		
			
				|  |  | -          top,
 | 
	
		
			
				|  |  | -          left,
 | 
	
		
			
				|  |  | -          actionManager: this.actionManager,
 | 
	
		
			
				|  |  | -          appState: this.state,
 | 
	
		
			
				|  |  | -          container: this.excalidrawContainerRef.current!,
 | 
	
		
			
				|  |  | -          elements,
 | 
	
		
			
				|  |  | -        });
 | 
	
		
			
				|  |  | -      }
 | 
	
		
			
				|  |  |      }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    // element contextMenu
 | 
	
		
			
				|  |  | +    // -------------------------------------------------------------------------
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    options.push(copyText);
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    if (this.state.viewModeEnabled) {
 | 
	
		
			
				|  |  | +      return [actionCopy, ...options];
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    return [
 | 
	
		
			
				|  |  | +      actionCut,
 | 
	
		
			
				|  |  | +      actionCopy,
 | 
	
		
			
				|  |  | +      actionPaste,
 | 
	
		
			
				|  |  | +      CONTEXT_MENU_SEPARATOR,
 | 
	
		
			
				|  |  | +      ...options,
 | 
	
		
			
				|  |  | +      CONTEXT_MENU_SEPARATOR,
 | 
	
		
			
				|  |  | +      actionCopyStyles,
 | 
	
		
			
				|  |  | +      actionPasteStyles,
 | 
	
		
			
				|  |  | +      CONTEXT_MENU_SEPARATOR,
 | 
	
		
			
				|  |  | +      actionGroup,
 | 
	
		
			
				|  |  | +      actionUnbindText,
 | 
	
		
			
				|  |  | +      actionBindText,
 | 
	
		
			
				|  |  | +      actionUngroup,
 | 
	
		
			
				|  |  | +      CONTEXT_MENU_SEPARATOR,
 | 
	
		
			
				|  |  | +      actionAddToLibrary,
 | 
	
		
			
				|  |  | +      CONTEXT_MENU_SEPARATOR,
 | 
	
		
			
				|  |  | +      actionSendBackward,
 | 
	
		
			
				|  |  | +      actionBringForward,
 | 
	
		
			
				|  |  | +      actionSendToBack,
 | 
	
		
			
				|  |  | +      actionBringToFront,
 | 
	
		
			
				|  |  | +      CONTEXT_MENU_SEPARATOR,
 | 
	
		
			
				|  |  | +      actionFlipHorizontal,
 | 
	
		
			
				|  |  | +      actionFlipVertical,
 | 
	
		
			
				|  |  | +      CONTEXT_MENU_SEPARATOR,
 | 
	
		
			
				|  |  | +      actionToggleLinearEditor,
 | 
	
		
			
				|  |  | +      actionLink,
 | 
	
		
			
				|  |  | +      actionDuplicateSelection,
 | 
	
		
			
				|  |  | +      actionToggleLock,
 | 
	
		
			
				|  |  | +      CONTEXT_MENU_SEPARATOR,
 | 
	
		
			
				|  |  | +      actionDeleteSelected,
 | 
	
		
			
				|  |  | +    ];
 | 
	
		
			
				|  |  |    };
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |    private handleWheel = withBatchedUpdates((event: WheelEvent) => {
 |