feat: add view mode in Excalidraw (#2840)

Co-authored-by: Lipis <lipiridis@gmail.com>
This commit is contained in:
Aakansha Doshi 2021-02-02 02:26:42 +05:30 committed by GitHub
parent 2b1b62d8f2
commit 675da16ca4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 546 additions and 232 deletions

View File

@ -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,
});

View File

@ -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 <App> 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(

View File

@ -22,7 +22,8 @@ export type ShortcutName =
| "gridMode"
| "zenMode"
| "stats"
| "addToLibrary";
| "addToLibrary"
| "viewMode";
const shortcutMap: Record<ShortcutName, string[]> = {
cut: [getShortcutKey("CtrlOrCmd+X")],
@ -56,6 +57,7 @@ const shortcutMap: Record<ShortcutName, string[]> = {
zenMode: [getShortcutKey("Alt+Z")],
stats: [],
addToLibrary: [],
viewMode: [getShortcutKey("Alt+R")],
};
export const getShortcutFromShortcutName = (name: ShortcutName) => {

View File

@ -84,7 +84,8 @@ export type ActionName =
| "alignVerticallyCentered"
| "alignHorizontallyCentered"
| "distributeHorizontally"
| "distributeVertically";
| "distributeVertically"
| "viewMode";
export interface Action {
name: ActionName;

View File

@ -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 = <ExportType extends "export" | "browser">(

View File

@ -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<ExcalidrawProps, AppState> {
offsetLeft,
offsetTop,
excalidrawRef,
viewModeEnabled = false,
} = props;
this.state = {
...defaultAppState,
@ -302,6 +306,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
width,
height,
...this.getCanvasOffsets({ offsetLeft, offsetTop }),
viewModeEnabled,
};
if (excalidrawRef) {
const readyPromise =
@ -342,6 +347,62 @@ class App extends React.Component<ExcalidrawProps, AppState> {
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 (
<canvas
id="canvas"
style={{
width: canvasDOMWidth,
height: canvasDOMHeight,
cursor: "grabbing",
}}
width={canvasWidth}
height={canvasHeight}
ref={this.handleCanvasRef}
onContextMenu={this.handleCanvasContextMenu}
onPointerMove={this.handleCanvasPointerMove}
onPointerUp={this.removePointer}
onPointerCancel={this.removePointer}
onTouchMove={this.handleTouchMove}
onPointerDown={this.handleCanvasPointerDown}
>
{t("labels.drawingCanvas")}
</canvas>
);
}
return (
<canvas
id="canvas"
style={{
width: canvasDOMWidth,
height: canvasDOMHeight,
}}
width={canvasWidth}
height={canvasHeight}
ref={this.handleCanvasRef}
onContextMenu={this.handleCanvasContextMenu}
onPointerDown={this.handleCanvasPointerDown}
onDoubleClick={this.handleCanvasDoubleClick}
onPointerMove={this.handleCanvasPointerMove}
onPointerUp={this.removePointer}
onPointerCancel={this.removePointer}
onTouchMove={this.handleTouchMove}
onDrop={this.handleCanvasOnDrop}
>
{t("labels.drawingCanvas")}
</canvas>
);
}
public render() {
const {
zenModeEnabled,
@ -349,20 +410,19 @@ class App extends React.Component<ExcalidrawProps, AppState> {
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 (
<div
className="excalidraw"
className={clsx("excalidraw", {
"excalidraw--view-mode": viewModeEnabled,
})}
ref={this.excalidrawContainerRef}
style={{
width: canvasDOMWidth,
@ -392,6 +452,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
isCollaborating={this.props.isCollaborating || false}
onExportToBackend={onExportToBackend}
renderCustomFooter={renderFooter}
viewModeEnabled={viewModeEnabled}
/>
{this.state.showStats && (
<Stats
@ -406,28 +467,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
clearToast={this.clearToast}
/>
)}
<main>
<canvas
id="canvas"
style={{
width: canvasDOMWidth,
height: canvasDOMHeight,
}}
width={canvasWidth}
height={canvasHeight}
ref={this.handleCanvasRef}
onContextMenu={this.handleCanvasContextMenu}
onPointerDown={this.handleCanvasPointerDown}
onDoubleClick={this.handleCanvasDoubleClick}
onPointerMove={this.handleCanvasPointerMove}
onPointerUp={this.removePointer}
onPointerCancel={this.removePointer}
onTouchMove={this.handleTouchMove}
onDrop={this.handleCanvasOnDrop}
>
{t("labels.drawingCanvas")}
</canvas>
</main>
<main>{this.renderCanvas()}</main>
</div>
);
}
@ -467,6 +507,13 @@ class App extends React.Component<ExcalidrawProps, AppState> {
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<ExcalidrawProps, AppState> {
height: state.height,
offsetTop: state.offsetTop,
offsetLeft: state.offsetLeft,
viewModeEnabled,
}),
() => {
if (actionResult.syncHistory) {
@ -658,7 +706,6 @@ class App extends React.Component<ExcalidrawProps, AppState> {
}
this.scene.addCallback(this.onSceneUpdated);
this.addEventListeners();
// optim to avoid extra render on init
@ -725,25 +772,16 @@ class App extends React.Component<ExcalidrawProps, AppState> {
}
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<ExcalidrawProps, AppState> {
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<ExcalidrawProps, AppState> {
});
}
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<ExcalidrawProps, AppState> {
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<ExcalidrawProps, AppState> {
});
}
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<ExcalidrawProps, AppState> {
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<ExcalidrawProps, AppState> {
!(
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<ExcalidrawProps, AppState> {
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<ExcalidrawProps, AppState> {
separator,
actionToggleGridMode,
actionToggleZenMode,
typeof this.props.viewModeEnabled === "undefined" &&
actionToggleViewMode,
actionToggleStats,
],
top: clientY,
@ -3632,6 +3727,17 @@ class App extends React.Component<ExcalidrawProps, AppState> {
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<ExcalidrawProps, AppState> {
contextItemLabel: "labels.paste",
},
_isMobile && separator,
probablySupportsClipboardBlob && actionCopyAsPng,
probablySupportsClipboardWriteText && actionCopyAsSvg,
...options,
separator,
actionCopyStyles,
actionPasteStyles,

View File

@ -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[];

View File

@ -227,6 +227,10 @@ export const HelpDialog = ({ onClose }: { onClose?: () => void }) => {
label={t("labels.gridMode")}
shortcuts={[getShortcutKey("CtrlOrCmd+'")]}
/>
<Shortcut
label={t("labels.viewMode")}
shortcuts={[getShortcutKey("Alt+R")]}
/>
</ShortcutIsland>
</Column>
<Column>

View File

@ -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 (
<Section
heading="canvasActions"
className={clsx("zen-mode-transition", {
"transition-left": zenModeEnabled,
})}
>
{/* the zIndex ensures this menu has higher stacking order,
see https://github.com/excalidraw/excalidraw/pull/1445 */}
<Island padding={2} style={{ zIndex: 1 }}>
<Stack.Col gap={4}>
<Stack.Row gap={1} justifyContent="space-between">
{actionManager.renderAction("saveScene")}
{actionManager.renderAction("saveAsScene")}
{renderExportDialog()}
</Stack.Row>
</Stack.Col>
</Island>
</Section>
);
};
const renderCanvasActions = () => (
<Section
heading="canvasActions"
@ -448,38 +472,42 @@ const LayerUI = ({
gap={4}
className={clsx({ "disable-pointerEvents": zenModeEnabled })}
>
{renderCanvasActions()}
{viewModeEnabled
? renderViewModeCanvasActions()
: renderCanvasActions()}
{shouldRenderSelectedShapeActions && renderSelectedShapeActions()}
</Stack.Col>
<Section heading="shapes">
{(heading) => (
<Stack.Col gap={4} align="start">
<Stack.Row gap={1}>
<Island
padding={1}
className={clsx({ "zen-mode": zenModeEnabled })}
>
<HintViewer appState={appState} elements={elements} />
{heading}
<Stack.Row gap={1}>
<ShapesSwitcher
elementType={appState.elementType}
setAppState={setAppState}
isLibraryOpen={appState.isLibraryOpen}
/>
</Stack.Row>
</Island>
<LockIcon
zenModeEnabled={zenModeEnabled}
checked={appState.elementLocked}
onChange={onLockToggle}
title={t("toolBar.lock")}
/>
</Stack.Row>
{libraryMenu}
</Stack.Col>
)}
</Section>
{!viewModeEnabled && (
<Section heading="shapes">
{(heading) => (
<Stack.Col gap={4} align="start">
<Stack.Row gap={1}>
<Island
padding={1}
className={clsx({ "zen-mode": zenModeEnabled })}
>
<HintViewer appState={appState} elements={elements} />
{heading}
<Stack.Row gap={1}>
<ShapesSwitcher
elementType={appState.elementType}
setAppState={setAppState}
isLibraryOpen={appState.isLibraryOpen}
/>
</Stack.Row>
</Island>
<LockIcon
zenModeEnabled={zenModeEnabled}
checked={appState.elementLocked}
onChange={onLockToggle}
title={t("toolBar.lock")}
/>
</Stack.Row>
{libraryMenu}
</Stack.Col>
)}
</Section>
)}
<UserList
className={clsx("zen-mode-transition", {
"transition-right": zenModeEnabled,
@ -524,6 +552,20 @@ const LayerUI = ({
);
};
const renderGitHubCorner = () => {
return (
<aside
className={clsx(
"layer-ui__wrapper__github-corner zen-mode-transition",
{
"transition-right": zenModeEnabled,
},
)}
>
<GitHubCorner appearance={appState.appearance} />
</aside>
);
};
const renderFooter = () => (
<footer role="contentinfo" className="layer-ui__wrapper__footer">
<div
@ -599,6 +641,7 @@ const LayerUI = ({
canvas={canvas}
isCollaborating={isCollaborating}
renderCustomFooter={renderCustomFooter}
viewModeEnabled={viewModeEnabled}
/>
</>
) : (
@ -610,18 +653,7 @@ const LayerUI = ({
{dialogs}
{renderFixedSideContainer()}
{renderBottomAppMenu()}
{
<aside
className={clsx(
"layer-ui__wrapper__github-corner zen-mode-transition",
{
"transition-right": zenModeEnabled,
},
)}
>
<GitHubCorner appearance={appState.appearance} />
</aside>
}
{renderGitHubCorner()}
{renderFooter()}
</div>
);

View File

@ -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) => (
<>
<FixedSideContainer side="top">
<Section heading="shapes">
{(heading) => (
<Stack.Col gap={4} align="center">
<Stack.Row gap={1}>
<Island padding={1}>
{heading}
<Stack.Row gap={1}>
<ShapesSwitcher
elementType={appState.elementType}
setAppState={setAppState}
isLibraryOpen={appState.isLibraryOpen}
/>
</Stack.Row>
</Island>
<LockIcon
checked={appState.elementLocked}
onChange={onLockToggle}
title={t("toolBar.lock")}
/>
</Stack.Row>
{libraryMenu}
</Stack.Col>
)}
</Section>
<HintViewer appState={appState} elements={elements} />
</FixedSideContainer>
<div
className="App-bottom-bar"
style={{
marginBottom: SCROLLBAR_WIDTH + SCROLLBAR_MARGIN * 2,
marginLeft: SCROLLBAR_WIDTH + SCROLLBAR_MARGIN * 2,
marginRight: SCROLLBAR_WIDTH + SCROLLBAR_MARGIN * 2,
}}
>
<Island padding={0}>
{appState.openMenu === "canvas" ? (
<Section className="App-mobile-menu" heading="canvasActions">
<div className="panelColumn">
<Stack.Col gap={4}>
{actionManager.renderAction("loadScene")}
{actionManager.renderAction("saveScene")}
{actionManager.renderAction("saveAsScene")}
{exportButton}
{actionManager.renderAction("clearCanvas")}
{onCollabButtonClick && (
<CollabButton
isCollaborating={isCollaborating}
collaboratorCount={appState.collaborators.size}
onClick={onCollabButtonClick}
/>
)}
<BackgroundPickerAndDarkModeToggle
actionManager={actionManager}
appState={appState}
setAppState={setAppState}
viewModeEnabled,
}: MobileMenuProps) => {
const renderFixedSideContainer = () => {
return (
<FixedSideContainer side="top">
<Section heading="shapes">
{(heading) => (
<Stack.Col gap={4} align="center">
<Stack.Row gap={1}>
<Island padding={1}>
{heading}
<Stack.Row gap={1}>
<ShapesSwitcher
elementType={appState.elementType}
setAppState={setAppState}
isLibraryOpen={appState.isLibraryOpen}
/>
</Stack.Row>
</Island>
<LockIcon
checked={appState.elementLocked}
onChange={onLockToggle}
title={t("toolBar.lock")}
/>
{renderCustomFooter?.(true)}
<fieldset>
<legend>{t("labels.collaborators")}</legend>
<UserList mobile>
{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]) => (
<React.Fragment key={clientId}>
{actionManager.renderAction(
"goToCollaborator",
clientId,
)}
</React.Fragment>
))}
</UserList>
</fieldset>
</Stack.Col>
</div>
</Section>
) : appState.openMenu === "shape" &&
showSelectedShapeActions(appState, elements) ? (
<Section className="App-mobile-menu" heading="selectedShapeActions">
<SelectedShapeActions
appState={appState}
elements={elements}
renderAction={actionManager.renderAction}
elementType={appState.elementType}
/>
</Section>
) : null}
<footer className="App-toolbar">
<div className="App-toolbar-content">
{actionManager.renderAction("toggleCanvasMenu")}
{actionManager.renderAction("toggleEditMenu")}
{actionManager.renderAction("undo")}
{actionManager.renderAction("redo")}
{actionManager.renderAction(
appState.multiElement ? "finalize" : "duplicateSelection",
)}
{actionManager.renderAction("deleteSelectedElements")}
</div>
{appState.scrolledOutside && !appState.openMenu && (
<button
className="scroll-back-to-content"
onClick={() => {
setAppState({
...calculateScrollCenter(elements, appState, canvas),
});
}}
>
{t("buttons.scrollBackToContent")}
</button>
</Stack.Row>
{libraryMenu}
</Stack.Col>
)}
</footer>
</Island>
</div>
</>
);
</Section>
<HintViewer appState={appState} elements={elements} />
</FixedSideContainer>
);
};
const renderAppToolbar = () => {
if (viewModeEnabled) {
return (
<div className="App-toolbar-content">
{actionManager.renderAction("toggleCanvasMenu")}
</div>
);
}
return (
<div className="App-toolbar-content">
{actionManager.renderAction("toggleCanvasMenu")}
{actionManager.renderAction("toggleEditMenu")}
{actionManager.renderAction("undo")}
{actionManager.renderAction("redo")}
{actionManager.renderAction(
appState.multiElement ? "finalize" : "duplicateSelection",
)}
{actionManager.renderAction("deleteSelectedElements")}
</div>
);
};
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 && (
<CollabButton
isCollaborating={isCollaborating}
collaboratorCount={appState.collaborators.size}
onClick={onCollabButtonClick}
/>
)}
{
<BackgroundPickerAndDarkModeToggle
actionManager={actionManager}
appState={appState}
setAppState={setAppState}
/>
}
</>
);
};
return (
<>
{!viewModeEnabled && renderFixedSideContainer()}
<div
className="App-bottom-bar"
style={{
marginBottom: SCROLLBAR_WIDTH + SCROLLBAR_MARGIN * 2,
marginLeft: SCROLLBAR_WIDTH + SCROLLBAR_MARGIN * 2,
marginRight: SCROLLBAR_WIDTH + SCROLLBAR_MARGIN * 2,
}}
>
<Island padding={0}>
{appState.openMenu === "canvas" ? (
<Section className="App-mobile-menu" heading="canvasActions">
<div className="panelColumn">
<Stack.Col gap={4}>
{renderCanvasActions()}
{renderCustomFooter?.(true)}
<fieldset>
<legend>{t("labels.collaborators")}</legend>
<UserList mobile>
{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]) => (
<React.Fragment key={clientId}>
{actionManager.renderAction(
"goToCollaborator",
clientId,
)}
</React.Fragment>
))}
</UserList>
</fieldset>
</Stack.Col>
</div>
</Section>
) : appState.openMenu === "shape" &&
!viewModeEnabled &&
showSelectedShapeActions(appState, elements) ? (
<Section className="App-mobile-menu" heading="selectedShapeActions">
<SelectedShapeActions
appState={appState}
elements={elements}
renderAction={actionManager.renderAction}
elementType={appState.elementType}
/>
</Section>
) : null}
<footer className="App-toolbar">
{renderAppToolbar()}
{appState.scrolledOutside && !appState.openMenu && (
<button
className="scroll-back-to-content"
onClick={() => {
setAppState({
...calculateScrollCenter(elements, appState, canvas),
});
}}
>
{t("buttons.scrollBackToContent")}
</button>
)}
</footer>
</Island>
</div>
</>
);
};

View File

@ -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,

View File

@ -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"),
);

View File

@ -21,6 +21,7 @@ export const CODES = {
V: "KeyV",
X: "KeyX",
Z: "KeyZ",
R: "KeyR",
} as const;
export const KEYS = {

View File

@ -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",

View File

@ -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).

View File

@ -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`

View File

@ -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}
/>
</IsMobileProvider>
</InitializeApp>
@ -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

View File

@ -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;

View File

@ -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 {

View File

@ -623,6 +623,7 @@ describe("regression tests", () => {
"selectAll",
"gridMode",
"zenMode",
"viewMode",
"stats",
];

View File

@ -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 = {