diff --git a/src/actions/actionToggleViewMode.tsx b/src/actions/actionToggleViewMode.tsx new file mode 100644 index 00000000..0808a5d5 --- /dev/null +++ b/src/actions/actionToggleViewMode.tsx @@ -0,0 +1,22 @@ +import { CODES, KEYS } from "../keys"; +import { register } from "./register"; +import { trackEvent } from "../analytics"; + +export const actionToggleViewMode = register({ + name: "viewMode", + perform(elements, appState) { + trackEvent("view", "mode", "view"); + return { + appState: { + ...appState, + viewModeEnabled: !this.checked!(appState), + selectedElementIds: {}, + }, + commitToHistory: false, + }; + }, + checked: (appState) => appState.viewModeEnabled, + contextItemLabel: "labels.viewMode", + keyTest: (event) => + !event[KEYS.CTRL_OR_CMD] && event.altKey && event.code === CODES.R, +}); diff --git a/src/actions/manager.tsx b/src/actions/manager.tsx index d48e3b0e..71eccf14 100644 --- a/src/actions/manager.tsx +++ b/src/actions/manager.tsx @@ -7,11 +7,11 @@ import { ActionResult, } from "./types"; import { ExcalidrawElement } from "../element/types"; -import { AppState } from "../types"; +import { AppState, ExcalidrawProps } from "../types"; // This is the component, but for now we don't care about anything but its // `canvas` state. -type App = { canvas: HTMLCanvasElement | null }; +type App = { canvas: HTMLCanvasElement | null; props: ExcalidrawProps }; export class ActionManager implements ActionsManagerInterface { actions = {} as ActionsManagerInterface["actions"]; @@ -66,6 +66,12 @@ export class ActionManager implements ActionsManagerInterface { if (data.length === 0) { return false; } + const { viewModeEnabled } = this.getAppState(); + if (viewModeEnabled) { + if (data[0].name !== "viewMode") { + return false; + } + } event.preventDefault(); this.updater( diff --git a/src/actions/shortcuts.ts b/src/actions/shortcuts.ts index a05c598d..4c9bc60c 100644 --- a/src/actions/shortcuts.ts +++ b/src/actions/shortcuts.ts @@ -22,7 +22,8 @@ export type ShortcutName = | "gridMode" | "zenMode" | "stats" - | "addToLibrary"; + | "addToLibrary" + | "viewMode"; const shortcutMap: Record = { cut: [getShortcutKey("CtrlOrCmd+X")], @@ -56,6 +57,7 @@ const shortcutMap: Record = { zenMode: [getShortcutKey("Alt+Z")], stats: [], addToLibrary: [], + viewMode: [getShortcutKey("Alt+R")], }; export const getShortcutFromShortcutName = (name: ShortcutName) => { diff --git a/src/actions/types.ts b/src/actions/types.ts index 66f8dece..9d90efd8 100644 --- a/src/actions/types.ts +++ b/src/actions/types.ts @@ -84,7 +84,8 @@ export type ActionName = | "alignVerticallyCentered" | "alignHorizontallyCentered" | "distributeHorizontally" - | "distributeVertically"; + | "distributeVertically" + | "viewMode"; export interface Action { name: ActionName; diff --git a/src/appState.ts b/src/appState.ts index fb9dac66..f77e6cff 100644 --- a/src/appState.ts +++ b/src/appState.ts @@ -72,6 +72,7 @@ export const getDefaultAppState = (): Omit< width: window.innerWidth, zenModeEnabled: false, zoom: { value: 1 as NormalizedZoomValue, translation: { x: 0, y: 0 } }, + viewModeEnabled: false, }; }; @@ -151,6 +152,7 @@ const APP_STATE_STORAGE_CONF = (< width: { browser: false, export: false }, zenModeEnabled: { browser: true, export: false }, zoom: { browser: true, export: false }, + viewModeEnabled: { browser: false, export: false }, }); const _clearAppStateForStorage = ( diff --git a/src/components/App.tsx b/src/components/App.tsx index 74484b7f..cf68c6fe 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -2,6 +2,8 @@ import { Point, simplify } from "points-on-curve"; import React from "react"; import { RoughCanvas } from "roughjs/bin/canvas"; import rough from "roughjs/bin/rough"; +import clsx from "clsx"; + import "../actions"; import { actionAddToLibrary, @@ -175,10 +177,11 @@ import { withBatchedUpdates, } from "../utils"; import { isMobile } from "../is-mobile"; -import ContextMenu from "./ContextMenu"; +import ContextMenu, { ContextMenuOption } from "./ContextMenu"; import LayerUI from "./LayerUI"; import { Stats } from "./Stats"; import { Toast } from "./Toast"; +import { actionToggleViewMode } from "../actions/actionToggleViewMode"; const { history } = createHistory(); @@ -295,6 +298,7 @@ class App extends React.Component { offsetLeft, offsetTop, excalidrawRef, + viewModeEnabled = false, } = props; this.state = { ...defaultAppState, @@ -302,6 +306,7 @@ class App extends React.Component { width, height, ...this.getCanvasOffsets({ offsetLeft, offsetTop }), + viewModeEnabled, }; if (excalidrawRef) { const readyPromise = @@ -342,6 +347,62 @@ class App extends React.Component { this.actionManager.registerAction(createRedoAction(history)); } + 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 ( + + {t("labels.drawingCanvas")} + + ); + } + return ( + + {t("labels.drawingCanvas")} + + ); + } + public render() { const { zenModeEnabled, @@ -349,20 +410,19 @@ class App extends React.Component { height: canvasDOMHeight, offsetTop, offsetLeft, + viewModeEnabled, } = this.state; const { onCollabButtonClick, onExportToBackend, renderFooter } = this.props; - const canvasScale = window.devicePixelRatio; - - const canvasWidth = canvasDOMWidth * canvasScale; - const canvasHeight = canvasDOMHeight * canvasScale; const DEFAULT_PASTE_X = canvasDOMWidth / 2; const DEFAULT_PASTE_Y = canvasDOMHeight / 2; return (
{ isCollaborating={this.props.isCollaborating || false} onExportToBackend={onExportToBackend} renderCustomFooter={renderFooter} + viewModeEnabled={viewModeEnabled} /> {this.state.showStats && ( { clearToast={this.clearToast} /> )} -
- - {t("labels.drawingCanvas")} - -
+
{this.renderCanvas()}
); } @@ -467,6 +507,13 @@ class App extends React.Component { if (actionResult.commitToHistory) { history.resumeRecording(); } + + let viewModeEnabled = actionResult?.appState?.viewModeEnabled || false; + + if (typeof this.props.viewModeEnabled !== "undefined") { + viewModeEnabled = this.props.viewModeEnabled; + } + this.setState( (state) => ({ ...actionResult.appState, @@ -476,6 +523,7 @@ class App extends React.Component { height: state.height, offsetTop: state.offsetTop, offsetLeft: state.offsetLeft, + viewModeEnabled, }), () => { if (actionResult.syncHistory) { @@ -658,7 +706,6 @@ class App extends React.Component { } this.scene.addCallback(this.onSceneUpdated); - this.addEventListeners(); // optim to avoid extra render on init @@ -725,25 +772,16 @@ class App extends React.Component { } private addEventListeners() { + this.removeEventListeners(); document.addEventListener(EVENT.COPY, this.onCopy); - document.addEventListener(EVENT.PASTE, this.pasteFromClipboard); - document.addEventListener(EVENT.CUT, this.onCut); - document.addEventListener(EVENT.KEYDOWN, this.onKeyDown, false); document.addEventListener(EVENT.KEYUP, this.onKeyUp, { passive: true }); document.addEventListener( EVENT.MOUSE_MOVE, this.updateCurrentCursorPosition, ); - window.addEventListener(EVENT.RESIZE, this.onResize, false); - window.addEventListener(EVENT.UNLOAD, this.onUnload, false); - window.addEventListener(EVENT.BLUR, this.onBlur, false); - window.addEventListener(EVENT.DRAG_OVER, this.disableEvent, false); - window.addEventListener(EVENT.DROP, this.disableEvent, false); - // 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, @@ -760,6 +798,18 @@ class App extends React.Component { this.onGestureEnd as any, false, ); + if (this.state.viewModeEnabled) { + return; + } + + document.addEventListener(EVENT.PASTE, this.pasteFromClipboard); + document.addEventListener(EVENT.CUT, this.onCut); + + window.addEventListener(EVENT.RESIZE, this.onResize, false); + window.addEventListener(EVENT.UNLOAD, this.onUnload, false); + window.addEventListener(EVENT.BLUR, this.onBlur, false); + window.addEventListener(EVENT.DRAG_OVER, this.disableEvent, false); + window.addEventListener(EVENT.DROP, this.disableEvent, false); } componentDidUpdate(prevProps: ExcalidrawProps, prevState: AppState) { @@ -782,6 +832,17 @@ class App extends React.Component { }); } + if (prevProps.viewModeEnabled !== this.props.viewModeEnabled) { + this.setState( + { viewModeEnabled: !!this.props.viewModeEnabled }, + this.addEventListeners, + ); + } + + if (prevState.viewModeEnabled !== this.state.viewModeEnabled) { + this.addEventListeners(); + } + document .querySelector(".excalidraw") ?.classList.toggle("Appearance_dark", this.state.appearance === "dark"); @@ -1134,10 +1195,6 @@ class App extends React.Component { this.actionManager.executeAction(actionToggleZenMode); }; - toggleGridMode = () => { - this.actionManager.executeAction(actionToggleGridMode); - }; - toggleStats = () => { if (!this.state.showStats) { trackEvent("dialog", "stats"); @@ -1232,14 +1289,18 @@ class App extends React.Component { }); } - if (event[KEYS.CTRL_OR_CMD]) { - this.setState({ isBindingEnabled: false }); - } - if (this.actionManager.handleKeyDown(event)) { return; } + if (this.state.viewModeEnabled) { + return; + } + + if (event[KEYS.CTRL_OR_CMD]) { + this.setState({ isBindingEnabled: false }); + } + if (event.code === CODES.NINE) { this.setState({ isLibraryOpen: !this.state.isLibraryOpen }); } @@ -2046,14 +2107,16 @@ class App extends React.Component { lastPointerUp = onPointerUp; - 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; + 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; + } }; private maybeOpenContextMenuAfterPointerDownOnTouchDevices = ( @@ -2103,7 +2166,8 @@ class App extends React.Component { !( gesture.pointers.size === 0 && (event.button === POINTER_BUTTON.WHEEL || - (event.button === POINTER_BUTTON.MAIN && isHoldingSpace)) + (event.button === POINTER_BUTTON.MAIN && isHoldingSpace) || + this.state.viewModeEnabled) ) ) { return false; @@ -3590,7 +3654,36 @@ class App extends React.Component { const elements = this.scene.getElements(); const element = this.getElementAtPosition(x, y); + const options: ContextMenuOption[] = []; + if (probablySupportsClipboardBlob && elements.length > 0) { + options.push(actionCopyAsPng); + } + + if (probablySupportsClipboardWriteText && elements.length > 0) { + options.push(actionCopyAsSvg); + } if (!element) { + const viewModeOptions: ContextMenuOption[] = [ + ...options, + actionToggleStats, + ]; + + if (typeof this.props.viewModeEnabled === "undefined") { + viewModeOptions.push(actionToggleViewMode); + } + + ContextMenu.push({ + options: viewModeOptions, + top: clientY, + left: clientX, + actionManager: this.actionManager, + appState: this.state, + }); + + if (this.state.viewModeEnabled) { + return; + } + ContextMenu.push({ options: [ _isMobile && @@ -3618,6 +3711,8 @@ class App extends React.Component { separator, actionToggleGridMode, actionToggleZenMode, + typeof this.props.viewModeEnabled === "undefined" && + actionToggleViewMode, actionToggleStats, ], top: clientY, @@ -3632,6 +3727,17 @@ class App extends React.Component { this.setState({ selectedElementIds: { [element.id]: true } }); } + if (this.state.viewModeEnabled) { + ContextMenu.push({ + options: [navigator.clipboard && actionCopy, ...options], + top: clientY, + left: clientX, + actionManager: this.actionManager, + appState: this.state, + }); + return; + } + ContextMenu.push({ options: [ _isMobile && actionCut, @@ -3648,8 +3754,7 @@ class App extends React.Component { contextItemLabel: "labels.paste", }, _isMobile && separator, - probablySupportsClipboardBlob && actionCopyAsPng, - probablySupportsClipboardWriteText && actionCopyAsSvg, + ...options, separator, actionCopyStyles, actionPasteStyles, diff --git a/src/components/ContextMenu.tsx b/src/components/ContextMenu.tsx index 17556665..2a2c9b96 100644 --- a/src/components/ContextMenu.tsx +++ b/src/components/ContextMenu.tsx @@ -13,7 +13,7 @@ import { Action } from "../actions/types"; import { ActionManager } from "../actions/manager"; import { AppState } from "../types"; -type ContextMenuOption = "separator" | Action; +export type ContextMenuOption = "separator" | Action; type ContextMenuProps = { options: ContextMenuOption[]; diff --git a/src/components/HelpDialog.tsx b/src/components/HelpDialog.tsx index 2a806476..a6670897 100644 --- a/src/components/HelpDialog.tsx +++ b/src/components/HelpDialog.tsx @@ -227,6 +227,10 @@ export const HelpDialog = ({ onClose }: { onClose?: () => void }) => { label={t("labels.gridMode")} shortcuts={[getShortcutKey("CtrlOrCmd+'")]} /> + diff --git a/src/components/LayerUI.tsx b/src/components/LayerUI.tsx index 69871364..ef245e8c 100644 --- a/src/components/LayerUI.tsx +++ b/src/components/LayerUI.tsx @@ -61,6 +61,7 @@ interface LayerUIProps { canvas: HTMLCanvasElement | null, ) => void; renderCustomFooter?: (isMobile: boolean) => JSX.Element; + viewModeEnabled: boolean; } const useOnClickOutside = ( @@ -299,6 +300,7 @@ const LayerUI = ({ isCollaborating, onExportToBackend, renderCustomFooter, + viewModeEnabled, }: LayerUIProps) => { const isMobile = useIsMobile(); @@ -358,6 +360,28 @@ const LayerUI = ({ ); }; + const renderViewModeCanvasActions = () => { + return ( +
+ {/* the zIndex ensures this menu has higher stacking order, + see https://github.com/excalidraw/excalidraw/pull/1445 */} + + + + {actionManager.renderAction("saveScene")} + {actionManager.renderAction("saveAsScene")} + {renderExportDialog()} + + + +
+ ); + }; const renderCanvasActions = () => (
- {renderCanvasActions()} + {viewModeEnabled + ? renderViewModeCanvasActions() + : renderCanvasActions()} {shouldRenderSelectedShapeActions && renderSelectedShapeActions()} -
- {(heading) => ( - - - - - {heading} - - - - - - - {libraryMenu} - - )} -
+ {!viewModeEnabled && ( +
+ {(heading) => ( + + + + + {heading} + + + + + + + {libraryMenu} + + )} +
+ )} { + return ( + + ); + }; const renderFooter = () => (
) : ( @@ -610,18 +653,7 @@ const LayerUI = ({ {dialogs} {renderFixedSideContainer()} {renderBottomAppMenu()} - { - - } + {renderGitHubCorner()} {renderFooter()}
); diff --git a/src/components/MobileMenu.tsx b/src/components/MobileMenu.tsx index 1500de55..6c94dbd5 100644 --- a/src/components/MobileMenu.tsx +++ b/src/components/MobileMenu.tsx @@ -29,6 +29,7 @@ type MobileMenuProps = { canvas: HTMLCanvasElement | null; isCollaborating: boolean; renderCustomFooter?: (isMobile: boolean) => JSX.Element; + viewModeEnabled: boolean; }; export const MobileMenu = ({ @@ -43,121 +44,164 @@ export const MobileMenu = ({ canvas, isCollaborating, renderCustomFooter, -}: MobileMenuProps) => ( - <> - -
- {(heading) => ( - - - - {heading} - - - - - - - {libraryMenu} - - )} -
- -
-
- - {appState.openMenu === "canvas" ? ( -
-
- - {actionManager.renderAction("loadScene")} - {actionManager.renderAction("saveScene")} - {actionManager.renderAction("saveAsScene")} - {exportButton} - {actionManager.renderAction("clearCanvas")} - {onCollabButtonClick && ( - - )} - { + const renderFixedSideContainer = () => { + return ( + +
+ {(heading) => ( + + + + {heading} + + + + + - {renderCustomFooter?.(true)} -
- {t("labels.collaborators")} - - {Array.from(appState.collaborators) - // Collaborator is either not initialized or is actually the current user. - .filter(([_, client]) => Object.keys(client).length !== 0) - .map(([clientId, client]) => ( - - {actionManager.renderAction( - "goToCollaborator", - clientId, - )} - - ))} - -
-
-
-
- ) : appState.openMenu === "shape" && - showSelectedShapeActions(appState, elements) ? ( -
- -
- ) : null} -
-
- {actionManager.renderAction("toggleCanvasMenu")} - {actionManager.renderAction("toggleEditMenu")} - {actionManager.renderAction("undo")} - {actionManager.renderAction("redo")} - {actionManager.renderAction( - appState.multiElement ? "finalize" : "duplicateSelection", - )} - {actionManager.renderAction("deleteSelectedElements")} -
- {appState.scrolledOutside && !appState.openMenu && ( - + + {libraryMenu} + )} -
-
-
- -); +
+ + + ); + }; + + const renderAppToolbar = () => { + if (viewModeEnabled) { + return ( +
+ {actionManager.renderAction("toggleCanvasMenu")} +
+ ); + } + return ( +
+ {actionManager.renderAction("toggleCanvasMenu")} + {actionManager.renderAction("toggleEditMenu")} + {actionManager.renderAction("undo")} + {actionManager.renderAction("redo")} + {actionManager.renderAction( + appState.multiElement ? "finalize" : "duplicateSelection", + )} + {actionManager.renderAction("deleteSelectedElements")} +
+ ); + }; + + const renderCanvasActions = () => { + if (viewModeEnabled) { + return ( + <> + {actionManager.renderAction("saveScene")} + {actionManager.renderAction("saveAsScene")} + {exportButton} + + ); + } + return ( + <> + {actionManager.renderAction("loadScene")} + {actionManager.renderAction("saveScene")} + {actionManager.renderAction("saveAsScene")} + {exportButton} + {actionManager.renderAction("clearCanvas")} + {onCollabButtonClick && ( + + )} + { + + } + + ); + }; + return ( + <> + {!viewModeEnabled && renderFixedSideContainer()} +
+ + {appState.openMenu === "canvas" ? ( +
+
+ + {renderCanvasActions()} + {renderCustomFooter?.(true)} +
+ {t("labels.collaborators")} + + {Array.from(appState.collaborators) + // Collaborator is either not initialized or is actually the current user. + .filter( + ([_, client]) => Object.keys(client).length !== 0, + ) + .map(([clientId, client]) => ( + + {actionManager.renderAction( + "goToCollaborator", + clientId, + )} + + ))} + +
+
+
+
+ ) : appState.openMenu === "shape" && + !viewModeEnabled && + showSelectedShapeActions(appState, elements) ? ( +
+ +
+ ) : null} + +
+
+ + ); +}; diff --git a/src/css/styles.scss b/src/css/styles.scss index b86cd562..573dba8f 100644 --- a/src/css/styles.scss +++ b/src/css/styles.scss @@ -492,6 +492,13 @@ pointer-events: none !important; } + &.excalidraw--view-mode { + .App-menu { + display: flex; + justify-content: space-between; + } + } + @media print { .App-bottom-bar, .FixedSideContainer, diff --git a/src/element/showSelectedShapeActions.ts b/src/element/showSelectedShapeActions.ts index d0a7439c..54528981 100644 --- a/src/element/showSelectedShapeActions.ts +++ b/src/element/showSelectedShapeActions.ts @@ -7,7 +7,8 @@ export const showSelectedShapeActions = ( elements: readonly NonDeletedExcalidrawElement[], ) => Boolean( - appState.editingElement || - getSelectedElements(elements, appState).length || - appState.elementType !== "selection", + !appState.viewModeEnabled && + (appState.editingElement || + getSelectedElements(elements, appState).length || + appState.elementType !== "selection"), ); diff --git a/src/keys.ts b/src/keys.ts index 4acc6ab9..31baff55 100644 --- a/src/keys.ts +++ b/src/keys.ts @@ -21,6 +21,7 @@ export const CODES = { V: "KeyV", X: "KeyX", Z: "KeyZ", + R: "KeyR", } as const; export const KEYS = { diff --git a/src/locales/en.json b/src/locales/en.json index c4d06be6..a7c40380 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -91,7 +91,8 @@ "centerVertically": "Center vertically", "centerHorizontally": "Center horizontally", "distributeHorizontally": "Distribute horizontally", - "distributeVertically": "Distribute vertically" + "distributeVertically": "Distribute vertically", + "viewMode": "View mode" }, "buttons": { "clearReset": "Reset the canvas", diff --git a/src/packages/excalidraw/CHANGELOG.md b/src/packages/excalidraw/CHANGELOG.md index 8da8b6a9..c9c866e5 100644 --- a/src/packages/excalidraw/CHANGELOG.md +++ b/src/packages/excalidraw/CHANGELOG.md @@ -16,12 +16,16 @@ Please add the latest change on the top under the correct section. ## Excalidraw API +### Features + +- Add `viewModeEnabled` prop which enabled the view mode [#2840](https://github.com/excalidraw/excalidraw/pull/2840). When this prop is used, the view mode will not show up in context menu is so it is fully controlled by host. - Expose `getAppState` on `excalidrawRef` [#2834](https://github.com/excalidraw/excalidraw/pull/2834). ## Excalidraw Library ### Features +- Add view mode [#2840](https://github.com/excalidraw/excalidraw/pull/2840). - Remove `copy`, `cut`, and `paste` actions from contextmenu [#2872](https://github.com/excalidraw/excalidraw/pull/2872) - Support `Ctrl-Y` shortcut to redo on Windows [#2831](https://github.com/excalidraw/excalidraw/pull/2831). diff --git a/src/packages/excalidraw/README.md b/src/packages/excalidraw/README.md index 409c3fba..214eb412 100644 --- a/src/packages/excalidraw/README.md +++ b/src/packages/excalidraw/README.md @@ -138,6 +138,7 @@ export default function App() { | [`onExportToBackend`](#onExportToBackend) | Function | | Callback triggered when link button is clicked on export dialog | | [`langCode`](#langCode) | string | `en` | Language code string | | [`renderFooter `](#renderFooter) | Function | | Function that renders custom UI footer | +| [`viewModeEnabled`](#viewModeEnabled) | boolean | false | This implies if the app is in view mode. | ### `Extra API's` @@ -330,3 +331,7 @@ import { defaultLang, languages } from "@excalidraw/excalidraw"; #### `renderFooter` A function that renders (returns JSX) custom UI footer. For example, you can use this to render a language picker that was previously being rendered by Excalidraw itself (for now, you'll need to implement your own language picker). + +#### `viewModeEnabled` + +This prop indicates if the app is in `view mode`. When this prop is used, the `view mode` will not show up in context menu is so it is fully controlled by host. Also the value of this prop if passed will be used over the value of `intialData.appState.viewModeEnabled` diff --git a/src/packages/excalidraw/index.tsx b/src/packages/excalidraw/index.tsx index c298fc94..0f744131 100644 --- a/src/packages/excalidraw/index.tsx +++ b/src/packages/excalidraw/index.tsx @@ -26,6 +26,7 @@ const Excalidraw = (props: ExcalidrawProps) => { onExportToBackend, renderFooter, langCode = defaultLang.code, + viewModeEnabled, } = props; useEffect(() => { @@ -64,6 +65,7 @@ const Excalidraw = (props: ExcalidrawProps) => { onExportToBackend={onExportToBackend} renderFooter={renderFooter} langCode={langCode} + viewModeEnabled={viewModeEnabled} /> @@ -81,7 +83,6 @@ const areEqual = ( const prevKeys = Object.keys(prevProps) as (keyof typeof prev)[]; const nextKeys = Object.keys(nextProps) as (keyof typeof next)[]; - return ( prevUser?.name === nextUser?.name && prevKeys.length === nextKeys.length && @@ -89,6 +90,10 @@ const areEqual = ( ); }; +Excalidraw.defaultProps = { + lanCode: defaultLang.code, +}; + const forwardedRefComp = forwardRef< ExcalidrawAPIRefValue, PublicExcalidrawProps diff --git a/src/renderer/renderScene.ts b/src/renderer/renderScene.ts index 5fed9749..c802fde4 100644 --- a/src/renderer/renderScene.ts +++ b/src/renderer/renderScene.ts @@ -373,12 +373,14 @@ export const renderScene = ( sceneState.zoom, "mouse", // when we render we don't know which pointer type so use mouse ); - renderTransformHandles( - context, - sceneState, - transformHandles, - locallySelectedElements[0].angle, - ); + if (!appState.viewModeEnabled) { + renderTransformHandles( + context, + sceneState, + transformHandles, + locallySelectedElements[0].angle, + ); + } } else if (locallySelectedElements.length > 1 && !appState.isRotating) { const dashedLinePadding = 4 / sceneState.zoom.value; context.fillStyle = oc.white; diff --git a/src/tests/__snapshots__/regressionTests.test.tsx.snap b/src/tests/__snapshots__/regressionTests.test.tsx.snap index 83b6ad55..574d47ce 100644 --- a/src/tests/__snapshots__/regressionTests.test.tsx.snap +++ b/src/tests/__snapshots__/regressionTests.test.tsx.snap @@ -76,6 +76,7 @@ Object { "suggestedBindings": Array [], "toastMessage": null, "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, "width": 1024, "zenModeEnabled": false, "zoom": Object { @@ -542,6 +543,7 @@ Object { "suggestedBindings": Array [], "toastMessage": null, "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, "width": 1024, "zenModeEnabled": false, "zoom": Object { @@ -990,6 +992,7 @@ Object { "suggestedBindings": Array [], "toastMessage": null, "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, "width": 1024, "zenModeEnabled": false, "zoom": Object { @@ -1766,6 +1769,7 @@ Object { "suggestedBindings": Array [], "toastMessage": null, "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, "width": 1024, "zenModeEnabled": false, "zoom": Object { @@ -1973,6 +1977,7 @@ Object { "suggestedBindings": Array [], "toastMessage": null, "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, "width": 1024, "zenModeEnabled": false, "zoom": Object { @@ -2424,6 +2429,7 @@ Object { "suggestedBindings": Array [], "toastMessage": null, "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, "width": 1024, "zenModeEnabled": false, "zoom": Object { @@ -2672,6 +2678,7 @@ Object { "suggestedBindings": Array [], "toastMessage": null, "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, "width": 1024, "zenModeEnabled": false, "zoom": Object { @@ -2837,6 +2844,7 @@ Object { "suggestedBindings": Array [], "toastMessage": null, "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, "width": 1024, "zenModeEnabled": false, "zoom": Object { @@ -3309,6 +3317,7 @@ Object { "suggestedBindings": Array [], "toastMessage": null, "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, "width": 1024, "zenModeEnabled": false, "zoom": Object { @@ -3618,6 +3627,7 @@ Object { "suggestedBindings": Array [], "toastMessage": null, "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, "width": 1024, "zenModeEnabled": false, "zoom": Object { @@ -3822,6 +3832,7 @@ Object { "suggestedBindings": Array [], "toastMessage": null, "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, "width": 1024, "zenModeEnabled": false, "zoom": Object { @@ -4062,6 +4073,7 @@ Object { "suggestedBindings": Array [], "toastMessage": null, "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, "width": 1024, "zenModeEnabled": false, "zoom": Object { @@ -4313,6 +4325,7 @@ Object { "suggestedBindings": Array [], "toastMessage": null, "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, "width": 1024, "zenModeEnabled": false, "zoom": Object { @@ -4714,6 +4727,7 @@ Object { "suggestedBindings": Array [], "toastMessage": null, "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, "width": 1024, "zenModeEnabled": false, "zoom": Object { @@ -4985,6 +4999,7 @@ Object { "suggestedBindings": Array [], "toastMessage": null, "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, "width": 1024, "zenModeEnabled": false, "zoom": Object { @@ -5310,6 +5325,7 @@ Object { "suggestedBindings": Array [], "toastMessage": null, "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, "width": 1024, "zenModeEnabled": false, "zoom": Object { @@ -5494,6 +5510,7 @@ Object { "suggestedBindings": Array [], "toastMessage": null, "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, "width": 1024, "zenModeEnabled": false, "zoom": Object { @@ -5656,6 +5673,7 @@ Object { "suggestedBindings": Array [], "toastMessage": null, "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, "width": 1024, "zenModeEnabled": false, "zoom": Object { @@ -6114,6 +6132,7 @@ Object { "suggestedBindings": Array [], "toastMessage": null, "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, "width": 1024, "zenModeEnabled": false, "zoom": Object { @@ -6423,6 +6442,7 @@ Object { "suggestedBindings": Array [], "toastMessage": null, "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, "width": 1024, "zenModeEnabled": false, "zoom": Object { @@ -8460,6 +8480,7 @@ Object { "suggestedBindings": Array [], "toastMessage": null, "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, "width": 1024, "zenModeEnabled": false, "zoom": Object { @@ -8821,6 +8842,7 @@ Object { "suggestedBindings": Array [], "toastMessage": null, "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, "width": 1024, "zenModeEnabled": false, "zoom": Object { @@ -9072,6 +9094,7 @@ Object { "suggestedBindings": Array [], "toastMessage": null, "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, "width": 1024, "zenModeEnabled": false, "zoom": Object { @@ -9324,6 +9347,7 @@ Object { "suggestedBindings": Array [], "toastMessage": null, "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, "width": 1024, "zenModeEnabled": false, "zoom": Object { @@ -9632,6 +9656,7 @@ Object { "suggestedBindings": Array [], "toastMessage": null, "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, "width": 1024, "zenModeEnabled": false, "zoom": Object { @@ -9794,6 +9819,7 @@ Object { "suggestedBindings": Array [], "toastMessage": null, "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, "width": 1024, "zenModeEnabled": false, "zoom": Object { @@ -9956,6 +9982,7 @@ Object { "suggestedBindings": Array [], "toastMessage": null, "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, "width": 1024, "zenModeEnabled": false, "zoom": Object { @@ -10118,6 +10145,7 @@ Object { "suggestedBindings": Array [], "toastMessage": null, "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, "width": 1024, "zenModeEnabled": false, "zoom": Object { @@ -10310,6 +10338,7 @@ Object { "suggestedBindings": Array [], "toastMessage": null, "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, "width": 1024, "zenModeEnabled": false, "zoom": Object { @@ -10502,6 +10531,7 @@ Object { "suggestedBindings": Array [], "toastMessage": null, "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, "width": 1024, "zenModeEnabled": false, "zoom": Object { @@ -10694,6 +10724,7 @@ Object { "suggestedBindings": Array [], "toastMessage": null, "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, "width": 1024, "zenModeEnabled": false, "zoom": Object { @@ -10886,6 +10917,7 @@ Object { "suggestedBindings": Array [], "toastMessage": null, "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, "width": 1024, "zenModeEnabled": false, "zoom": Object { @@ -11048,6 +11080,7 @@ Object { "suggestedBindings": Array [], "toastMessage": null, "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, "width": 1024, "zenModeEnabled": false, "zoom": Object { @@ -11210,6 +11243,7 @@ Object { "suggestedBindings": Array [], "toastMessage": null, "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, "width": 1024, "zenModeEnabled": false, "zoom": Object { @@ -11402,6 +11436,7 @@ Object { "suggestedBindings": Array [], "toastMessage": null, "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, "width": 1024, "zenModeEnabled": false, "zoom": Object { @@ -11564,6 +11599,7 @@ Object { "suggestedBindings": Array [], "toastMessage": null, "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, "width": 1024, "zenModeEnabled": false, "zoom": Object { @@ -11767,6 +11803,7 @@ Object { "suggestedBindings": Array [], "toastMessage": null, "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, "width": 1024, "zenModeEnabled": false, "zoom": Object { @@ -12474,6 +12511,7 @@ Object { "suggestedBindings": Array [], "toastMessage": null, "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, "width": 1024, "zenModeEnabled": false, "zoom": Object { @@ -12721,6 +12759,7 @@ Object { "suggestedBindings": Array [], "toastMessage": null, "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, "width": 1024, "zenModeEnabled": false, "zoom": Object { @@ -12819,6 +12858,7 @@ Object { "suggestedBindings": Array [], "toastMessage": null, "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, "width": 1024, "zenModeEnabled": false, "zoom": Object { @@ -12919,6 +12959,7 @@ Object { "suggestedBindings": Array [], "toastMessage": null, "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, "width": 1024, "zenModeEnabled": false, "zoom": Object { @@ -13081,6 +13122,7 @@ Object { "suggestedBindings": Array [], "toastMessage": null, "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, "width": 1024, "zenModeEnabled": false, "zoom": Object { @@ -13387,6 +13429,7 @@ Object { "suggestedBindings": Array [], "toastMessage": null, "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, "width": 1024, "zenModeEnabled": false, "zoom": Object { @@ -13693,6 +13736,7 @@ Object { "suggestedBindings": Array [], "toastMessage": "Copied styles.", "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, "width": 1024, "zenModeEnabled": false, "zoom": Object { @@ -13853,6 +13897,7 @@ Object { "suggestedBindings": Array [], "toastMessage": null, "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, "width": 1024, "zenModeEnabled": false, "zoom": Object { @@ -14049,6 +14094,7 @@ Object { "suggestedBindings": Array [], "toastMessage": null, "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, "width": 1024, "zenModeEnabled": false, "zoom": Object { @@ -14302,6 +14348,7 @@ Object { "suggestedBindings": Array [], "toastMessage": null, "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, "width": 1024, "zenModeEnabled": false, "zoom": Object { @@ -14618,6 +14665,7 @@ Object { "suggestedBindings": Array [], "toastMessage": "Copied styles.", "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, "width": 1024, "zenModeEnabled": false, "zoom": Object { @@ -15455,6 +15503,7 @@ Object { "suggestedBindings": Array [], "toastMessage": null, "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, "width": 1024, "zenModeEnabled": false, "zoom": Object { @@ -15761,6 +15810,7 @@ Object { "suggestedBindings": Array [], "toastMessage": null, "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, "width": 1024, "zenModeEnabled": false, "zoom": Object { @@ -16071,6 +16121,7 @@ Object { "suggestedBindings": Array [], "toastMessage": null, "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, "width": 1024, "zenModeEnabled": false, "zoom": Object { @@ -16447,6 +16498,7 @@ Object { "suggestedBindings": Array [], "toastMessage": null, "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, "width": 1024, "zenModeEnabled": false, "zoom": Object { @@ -16618,6 +16670,7 @@ Object { "suggestedBindings": Array [], "toastMessage": null, "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, "width": 1024, "zenModeEnabled": false, "zoom": Object { @@ -16931,6 +16984,7 @@ Object { "suggestedBindings": Array [], "toastMessage": null, "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, "width": 1024, "zenModeEnabled": false, "zoom": Object { @@ -17171,6 +17225,7 @@ Object { "suggestedBindings": Array [], "toastMessage": null, "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, "width": 1024, "zenModeEnabled": false, "zoom": Object { @@ -17426,6 +17481,7 @@ Object { "suggestedBindings": Array [], "toastMessage": null, "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, "width": 1024, "zenModeEnabled": false, "zoom": Object { @@ -17741,6 +17797,7 @@ Object { "suggestedBindings": Array [], "toastMessage": null, "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, "width": 1024, "zenModeEnabled": false, "zoom": Object { @@ -17841,6 +17898,7 @@ Object { "suggestedBindings": Array [], "toastMessage": null, "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, "width": 1024, "zenModeEnabled": false, "zoom": Object { @@ -18014,6 +18072,7 @@ Object { "suggestedBindings": Array [], "toastMessage": null, "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, "width": 1024, "zenModeEnabled": false, "zoom": Object { @@ -18820,6 +18879,7 @@ Object { "suggestedBindings": Array [], "toastMessage": null, "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, "width": 1024, "zenModeEnabled": false, "zoom": Object { @@ -18922,6 +18982,7 @@ Object { "suggestedBindings": Array [], "toastMessage": null, "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, "width": 1024, "zenModeEnabled": false, "zoom": Object { @@ -19698,6 +19759,7 @@ Object { "suggestedBindings": Array [], "toastMessage": null, "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, "width": 1024, "zenModeEnabled": false, "zoom": Object { @@ -20099,6 +20161,7 @@ Object { "suggestedBindings": Array [], "toastMessage": null, "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, "width": 1024, "zenModeEnabled": false, "zoom": Object { @@ -20346,6 +20409,7 @@ Object { "suggestedBindings": Array [], "toastMessage": null, "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, "width": 1024, "zenModeEnabled": false, "zoom": Object { @@ -20446,6 +20510,7 @@ Object { "suggestedBindings": Array [], "toastMessage": null, "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, "width": 1024, "zenModeEnabled": false, "zoom": Object { @@ -20940,6 +21005,7 @@ Object { "suggestedBindings": Array [], "toastMessage": null, "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, "width": 1024, "zenModeEnabled": false, "zoom": Object { @@ -21038,6 +21104,7 @@ Object { "suggestedBindings": Array [], "toastMessage": null, "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, "width": 1024, "zenModeEnabled": false, "zoom": Object { diff --git a/src/tests/regressionTests.test.tsx b/src/tests/regressionTests.test.tsx index 9cb8b383..a0bba4f4 100644 --- a/src/tests/regressionTests.test.tsx +++ b/src/tests/regressionTests.test.tsx @@ -623,6 +623,7 @@ describe("regression tests", () => { "selectAll", "gridMode", "zenMode", + "viewMode", "stats", ]; diff --git a/src/types.ts b/src/types.ts index 503e8172..dd31b9f8 100644 --- a/src/types.ts +++ b/src/types.ts @@ -85,6 +85,7 @@ export type AppState = { zenModeEnabled: boolean; appearance: "light" | "dark"; gridSize: number | null; + viewModeEnabled: boolean; /** top-most selected groups (i.e. does not include nested groups) */ selectedGroupIds: { [groupId: string]: boolean }; @@ -181,6 +182,7 @@ export interface ExcalidrawProps { ) => void; renderFooter?: (isMobile: boolean) => JSX.Element; langCode?: Language["code"]; + viewModeEnabled?: boolean; } export type SceneData = {