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, ActionResult,
} from "./types"; } from "./types";
import { ExcalidrawElement } from "../element/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 // This is the <App> component, but for now we don't care about anything but its
// `canvas` state. // `canvas` state.
type App = { canvas: HTMLCanvasElement | null }; type App = { canvas: HTMLCanvasElement | null; props: ExcalidrawProps };
export class ActionManager implements ActionsManagerInterface { export class ActionManager implements ActionsManagerInterface {
actions = {} as ActionsManagerInterface["actions"]; actions = {} as ActionsManagerInterface["actions"];
@ -66,6 +66,12 @@ export class ActionManager implements ActionsManagerInterface {
if (data.length === 0) { if (data.length === 0) {
return false; return false;
} }
const { viewModeEnabled } = this.getAppState();
if (viewModeEnabled) {
if (data[0].name !== "viewMode") {
return false;
}
}
event.preventDefault(); event.preventDefault();
this.updater( this.updater(

View File

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

View File

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

View File

@ -72,6 +72,7 @@ export const getDefaultAppState = (): Omit<
width: window.innerWidth, width: window.innerWidth,
zenModeEnabled: false, zenModeEnabled: false,
zoom: { value: 1 as NormalizedZoomValue, translation: { x: 0, y: 0 } }, 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 }, width: { browser: false, export: false },
zenModeEnabled: { browser: true, export: false }, zenModeEnabled: { browser: true, export: false },
zoom: { browser: true, export: false }, zoom: { browser: true, export: false },
viewModeEnabled: { browser: false, export: false },
}); });
const _clearAppStateForStorage = <ExportType extends "export" | "browser">( const _clearAppStateForStorage = <ExportType extends "export" | "browser">(

View File

@ -2,6 +2,8 @@ import { Point, simplify } from "points-on-curve";
import React from "react"; import React from "react";
import { RoughCanvas } from "roughjs/bin/canvas"; import { RoughCanvas } from "roughjs/bin/canvas";
import rough from "roughjs/bin/rough"; import rough from "roughjs/bin/rough";
import clsx from "clsx";
import "../actions"; import "../actions";
import { import {
actionAddToLibrary, actionAddToLibrary,
@ -175,10 +177,11 @@ import {
withBatchedUpdates, withBatchedUpdates,
} from "../utils"; } from "../utils";
import { isMobile } from "../is-mobile"; import { isMobile } from "../is-mobile";
import ContextMenu from "./ContextMenu"; import ContextMenu, { ContextMenuOption } from "./ContextMenu";
import LayerUI from "./LayerUI"; import LayerUI from "./LayerUI";
import { Stats } from "./Stats"; import { Stats } from "./Stats";
import { Toast } from "./Toast"; import { Toast } from "./Toast";
import { actionToggleViewMode } from "../actions/actionToggleViewMode";
const { history } = createHistory(); const { history } = createHistory();
@ -295,6 +298,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
offsetLeft, offsetLeft,
offsetTop, offsetTop,
excalidrawRef, excalidrawRef,
viewModeEnabled = false,
} = props; } = props;
this.state = { this.state = {
...defaultAppState, ...defaultAppState,
@ -302,6 +306,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
width, width,
height, height,
...this.getCanvasOffsets({ offsetLeft, offsetTop }), ...this.getCanvasOffsets({ offsetLeft, offsetTop }),
viewModeEnabled,
}; };
if (excalidrawRef) { if (excalidrawRef) {
const readyPromise = const readyPromise =
@ -342,6 +347,62 @@ class App extends React.Component<ExcalidrawProps, AppState> {
this.actionManager.registerAction(createRedoAction(history)); 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() { public render() {
const { const {
zenModeEnabled, zenModeEnabled,
@ -349,20 +410,19 @@ class App extends React.Component<ExcalidrawProps, AppState> {
height: canvasDOMHeight, height: canvasDOMHeight,
offsetTop, offsetTop,
offsetLeft, offsetLeft,
viewModeEnabled,
} = this.state; } = this.state;
const { onCollabButtonClick, onExportToBackend, renderFooter } = this.props; 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_X = canvasDOMWidth / 2;
const DEFAULT_PASTE_Y = canvasDOMHeight / 2; const DEFAULT_PASTE_Y = canvasDOMHeight / 2;
return ( return (
<div <div
className="excalidraw" className={clsx("excalidraw", {
"excalidraw--view-mode": viewModeEnabled,
})}
ref={this.excalidrawContainerRef} ref={this.excalidrawContainerRef}
style={{ style={{
width: canvasDOMWidth, width: canvasDOMWidth,
@ -392,6 +452,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
isCollaborating={this.props.isCollaborating || false} isCollaborating={this.props.isCollaborating || false}
onExportToBackend={onExportToBackend} onExportToBackend={onExportToBackend}
renderCustomFooter={renderFooter} renderCustomFooter={renderFooter}
viewModeEnabled={viewModeEnabled}
/> />
{this.state.showStats && ( {this.state.showStats && (
<Stats <Stats
@ -406,28 +467,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
clearToast={this.clearToast} clearToast={this.clearToast}
/> />
)} )}
<main> <main>{this.renderCanvas()}</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>
</div> </div>
); );
} }
@ -467,6 +507,13 @@ class App extends React.Component<ExcalidrawProps, AppState> {
if (actionResult.commitToHistory) { if (actionResult.commitToHistory) {
history.resumeRecording(); history.resumeRecording();
} }
let viewModeEnabled = actionResult?.appState?.viewModeEnabled || false;
if (typeof this.props.viewModeEnabled !== "undefined") {
viewModeEnabled = this.props.viewModeEnabled;
}
this.setState( this.setState(
(state) => ({ (state) => ({
...actionResult.appState, ...actionResult.appState,
@ -476,6 +523,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
height: state.height, height: state.height,
offsetTop: state.offsetTop, offsetTop: state.offsetTop,
offsetLeft: state.offsetLeft, offsetLeft: state.offsetLeft,
viewModeEnabled,
}), }),
() => { () => {
if (actionResult.syncHistory) { if (actionResult.syncHistory) {
@ -658,7 +706,6 @@ class App extends React.Component<ExcalidrawProps, AppState> {
} }
this.scene.addCallback(this.onSceneUpdated); this.scene.addCallback(this.onSceneUpdated);
this.addEventListeners(); this.addEventListeners();
// optim to avoid extra render on init // optim to avoid extra render on init
@ -725,25 +772,16 @@ class App extends React.Component<ExcalidrawProps, AppState> {
} }
private addEventListeners() { private addEventListeners() {
this.removeEventListeners();
document.addEventListener(EVENT.COPY, this.onCopy); 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.KEYDOWN, this.onKeyDown, false);
document.addEventListener(EVENT.KEYUP, this.onKeyUp, { passive: true }); document.addEventListener(EVENT.KEYUP, this.onKeyUp, { passive: true });
document.addEventListener( document.addEventListener(
EVENT.MOUSE_MOVE, EVENT.MOUSE_MOVE,
this.updateCurrentCursorPosition, 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 // rerender text elements on font load to fix #637 && #1553
document.fonts?.addEventListener?.("loadingdone", this.onFontLoaded); document.fonts?.addEventListener?.("loadingdone", this.onFontLoaded);
// Safari-only desktop pinch zoom // Safari-only desktop pinch zoom
document.addEventListener( document.addEventListener(
EVENT.GESTURE_START, EVENT.GESTURE_START,
@ -760,6 +798,18 @@ class App extends React.Component<ExcalidrawProps, AppState> {
this.onGestureEnd as any, this.onGestureEnd as any,
false, 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) { 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 document
.querySelector(".excalidraw") .querySelector(".excalidraw")
?.classList.toggle("Appearance_dark", this.state.appearance === "dark"); ?.classList.toggle("Appearance_dark", this.state.appearance === "dark");
@ -1134,10 +1195,6 @@ class App extends React.Component<ExcalidrawProps, AppState> {
this.actionManager.executeAction(actionToggleZenMode); this.actionManager.executeAction(actionToggleZenMode);
}; };
toggleGridMode = () => {
this.actionManager.executeAction(actionToggleGridMode);
};
toggleStats = () => { toggleStats = () => {
if (!this.state.showStats) { if (!this.state.showStats) {
trackEvent("dialog", "stats"); 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)) { if (this.actionManager.handleKeyDown(event)) {
return; return;
} }
if (this.state.viewModeEnabled) {
return;
}
if (event[KEYS.CTRL_OR_CMD]) {
this.setState({ isBindingEnabled: false });
}
if (event.code === CODES.NINE) { if (event.code === CODES.NINE) {
this.setState({ isLibraryOpen: !this.state.isLibraryOpen }); this.setState({ isLibraryOpen: !this.state.isLibraryOpen });
} }
@ -2046,6 +2107,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
lastPointerUp = onPointerUp; lastPointerUp = onPointerUp;
if (!this.state.viewModeEnabled) {
window.addEventListener(EVENT.POINTER_MOVE, onPointerMove); window.addEventListener(EVENT.POINTER_MOVE, onPointerMove);
window.addEventListener(EVENT.POINTER_UP, onPointerUp); window.addEventListener(EVENT.POINTER_UP, onPointerUp);
window.addEventListener(EVENT.KEYDOWN, onKeyDown); window.addEventListener(EVENT.KEYDOWN, onKeyDown);
@ -2054,6 +2116,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
pointerDownState.eventListeners.onUp = onPointerUp; pointerDownState.eventListeners.onUp = onPointerUp;
pointerDownState.eventListeners.onKeyUp = onKeyUp; pointerDownState.eventListeners.onKeyUp = onKeyUp;
pointerDownState.eventListeners.onKeyDown = onKeyDown; pointerDownState.eventListeners.onKeyDown = onKeyDown;
}
}; };
private maybeOpenContextMenuAfterPointerDownOnTouchDevices = ( private maybeOpenContextMenuAfterPointerDownOnTouchDevices = (
@ -2103,7 +2166,8 @@ class App extends React.Component<ExcalidrawProps, AppState> {
!( !(
gesture.pointers.size === 0 && gesture.pointers.size === 0 &&
(event.button === POINTER_BUTTON.WHEEL || (event.button === POINTER_BUTTON.WHEEL ||
(event.button === POINTER_BUTTON.MAIN && isHoldingSpace)) (event.button === POINTER_BUTTON.MAIN && isHoldingSpace) ||
this.state.viewModeEnabled)
) )
) { ) {
return false; return false;
@ -3590,7 +3654,36 @@ class App extends React.Component<ExcalidrawProps, AppState> {
const elements = this.scene.getElements(); const elements = this.scene.getElements();
const element = this.getElementAtPosition(x, y); 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) { 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({ ContextMenu.push({
options: [ options: [
_isMobile && _isMobile &&
@ -3618,6 +3711,8 @@ class App extends React.Component<ExcalidrawProps, AppState> {
separator, separator,
actionToggleGridMode, actionToggleGridMode,
actionToggleZenMode, actionToggleZenMode,
typeof this.props.viewModeEnabled === "undefined" &&
actionToggleViewMode,
actionToggleStats, actionToggleStats,
], ],
top: clientY, top: clientY,
@ -3632,6 +3727,17 @@ class App extends React.Component<ExcalidrawProps, AppState> {
this.setState({ selectedElementIds: { [element.id]: true } }); 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({ ContextMenu.push({
options: [ options: [
_isMobile && actionCut, _isMobile && actionCut,
@ -3648,8 +3754,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
contextItemLabel: "labels.paste", contextItemLabel: "labels.paste",
}, },
_isMobile && separator, _isMobile && separator,
probablySupportsClipboardBlob && actionCopyAsPng, ...options,
probablySupportsClipboardWriteText && actionCopyAsSvg,
separator, separator,
actionCopyStyles, actionCopyStyles,
actionPasteStyles, actionPasteStyles,

View File

@ -13,7 +13,7 @@ import { Action } from "../actions/types";
import { ActionManager } from "../actions/manager"; import { ActionManager } from "../actions/manager";
import { AppState } from "../types"; import { AppState } from "../types";
type ContextMenuOption = "separator" | Action; export type ContextMenuOption = "separator" | Action;
type ContextMenuProps = { type ContextMenuProps = {
options: ContextMenuOption[]; options: ContextMenuOption[];

View File

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

View File

@ -61,6 +61,7 @@ interface LayerUIProps {
canvas: HTMLCanvasElement | null, canvas: HTMLCanvasElement | null,
) => void; ) => void;
renderCustomFooter?: (isMobile: boolean) => JSX.Element; renderCustomFooter?: (isMobile: boolean) => JSX.Element;
viewModeEnabled: boolean;
} }
const useOnClickOutside = ( const useOnClickOutside = (
@ -299,6 +300,7 @@ const LayerUI = ({
isCollaborating, isCollaborating,
onExportToBackend, onExportToBackend,
renderCustomFooter, renderCustomFooter,
viewModeEnabled,
}: LayerUIProps) => { }: LayerUIProps) => {
const isMobile = useIsMobile(); 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 = () => ( const renderCanvasActions = () => (
<Section <Section
heading="canvasActions" heading="canvasActions"
@ -448,9 +472,12 @@ const LayerUI = ({
gap={4} gap={4}
className={clsx({ "disable-pointerEvents": zenModeEnabled })} className={clsx({ "disable-pointerEvents": zenModeEnabled })}
> >
{renderCanvasActions()} {viewModeEnabled
? renderViewModeCanvasActions()
: renderCanvasActions()}
{shouldRenderSelectedShapeActions && renderSelectedShapeActions()} {shouldRenderSelectedShapeActions && renderSelectedShapeActions()}
</Stack.Col> </Stack.Col>
{!viewModeEnabled && (
<Section heading="shapes"> <Section heading="shapes">
{(heading) => ( {(heading) => (
<Stack.Col gap={4} align="start"> <Stack.Col gap={4} align="start">
@ -480,6 +507,7 @@ const LayerUI = ({
</Stack.Col> </Stack.Col>
)} )}
</Section> </Section>
)}
<UserList <UserList
className={clsx("zen-mode-transition", { className={clsx("zen-mode-transition", {
"transition-right": zenModeEnabled, "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 = () => ( const renderFooter = () => (
<footer role="contentinfo" className="layer-ui__wrapper__footer"> <footer role="contentinfo" className="layer-ui__wrapper__footer">
<div <div
@ -599,6 +641,7 @@ const LayerUI = ({
canvas={canvas} canvas={canvas}
isCollaborating={isCollaborating} isCollaborating={isCollaborating}
renderCustomFooter={renderCustomFooter} renderCustomFooter={renderCustomFooter}
viewModeEnabled={viewModeEnabled}
/> />
</> </>
) : ( ) : (
@ -610,18 +653,7 @@ const LayerUI = ({
{dialogs} {dialogs}
{renderFixedSideContainer()} {renderFixedSideContainer()}
{renderBottomAppMenu()} {renderBottomAppMenu()}
{ {renderGitHubCorner()}
<aside
className={clsx(
"layer-ui__wrapper__github-corner zen-mode-transition",
{
"transition-right": zenModeEnabled,
},
)}
>
<GitHubCorner appearance={appState.appearance} />
</aside>
}
{renderFooter()} {renderFooter()}
</div> </div>
); );

View File

@ -29,6 +29,7 @@ type MobileMenuProps = {
canvas: HTMLCanvasElement | null; canvas: HTMLCanvasElement | null;
isCollaborating: boolean; isCollaborating: boolean;
renderCustomFooter?: (isMobile: boolean) => JSX.Element; renderCustomFooter?: (isMobile: boolean) => JSX.Element;
viewModeEnabled: boolean;
}; };
export const MobileMenu = ({ export const MobileMenu = ({
@ -43,8 +44,10 @@ export const MobileMenu = ({
canvas, canvas,
isCollaborating, isCollaborating,
renderCustomFooter, renderCustomFooter,
}: MobileMenuProps) => ( viewModeEnabled,
<> }: MobileMenuProps) => {
const renderFixedSideContainer = () => {
return (
<FixedSideContainer side="top"> <FixedSideContainer side="top">
<Section heading="shapes"> <Section heading="shapes">
{(heading) => ( {(heading) => (
@ -72,6 +75,68 @@ export const MobileMenu = ({
</Section> </Section>
<HintViewer appState={appState} elements={elements} /> <HintViewer appState={appState} elements={elements} />
</FixedSideContainer> </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 <div
className="App-bottom-bar" className="App-bottom-bar"
style={{ style={{
@ -85,30 +150,16 @@ export const MobileMenu = ({
<Section className="App-mobile-menu" heading="canvasActions"> <Section className="App-mobile-menu" heading="canvasActions">
<div className="panelColumn"> <div className="panelColumn">
<Stack.Col gap={4}> <Stack.Col gap={4}>
{actionManager.renderAction("loadScene")} {renderCanvasActions()}
{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}
/>
{renderCustomFooter?.(true)} {renderCustomFooter?.(true)}
<fieldset> <fieldset>
<legend>{t("labels.collaborators")}</legend> <legend>{t("labels.collaborators")}</legend>
<UserList mobile> <UserList mobile>
{Array.from(appState.collaborators) {Array.from(appState.collaborators)
// Collaborator is either not initialized or is actually the current user. // Collaborator is either not initialized or is actually the current user.
.filter(([_, client]) => Object.keys(client).length !== 0) .filter(
([_, client]) => Object.keys(client).length !== 0,
)
.map(([clientId, client]) => ( .map(([clientId, client]) => (
<React.Fragment key={clientId}> <React.Fragment key={clientId}>
{actionManager.renderAction( {actionManager.renderAction(
@ -123,6 +174,7 @@ export const MobileMenu = ({
</div> </div>
</Section> </Section>
) : appState.openMenu === "shape" && ) : appState.openMenu === "shape" &&
!viewModeEnabled &&
showSelectedShapeActions(appState, elements) ? ( showSelectedShapeActions(appState, elements) ? (
<Section className="App-mobile-menu" heading="selectedShapeActions"> <Section className="App-mobile-menu" heading="selectedShapeActions">
<SelectedShapeActions <SelectedShapeActions
@ -134,16 +186,7 @@ export const MobileMenu = ({
</Section> </Section>
) : null} ) : null}
<footer className="App-toolbar"> <footer className="App-toolbar">
<div className="App-toolbar-content"> {renderAppToolbar()}
{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 && ( {appState.scrolledOutside && !appState.openMenu && (
<button <button
className="scroll-back-to-content" className="scroll-back-to-content"
@ -161,3 +204,4 @@ export const MobileMenu = ({
</div> </div>
</> </>
); );
};

View File

@ -492,6 +492,13 @@
pointer-events: none !important; pointer-events: none !important;
} }
&.excalidraw--view-mode {
.App-menu {
display: flex;
justify-content: space-between;
}
}
@media print { @media print {
.App-bottom-bar, .App-bottom-bar,
.FixedSideContainer, .FixedSideContainer,

View File

@ -7,7 +7,8 @@ export const showSelectedShapeActions = (
elements: readonly NonDeletedExcalidrawElement[], elements: readonly NonDeletedExcalidrawElement[],
) => ) =>
Boolean( Boolean(
appState.editingElement || !appState.viewModeEnabled &&
(appState.editingElement ||
getSelectedElements(elements, appState).length || getSelectedElements(elements, appState).length ||
appState.elementType !== "selection", appState.elementType !== "selection"),
); );

View File

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

View File

@ -91,7 +91,8 @@
"centerVertically": "Center vertically", "centerVertically": "Center vertically",
"centerHorizontally": "Center horizontally", "centerHorizontally": "Center horizontally",
"distributeHorizontally": "Distribute horizontally", "distributeHorizontally": "Distribute horizontally",
"distributeVertically": "Distribute vertically" "distributeVertically": "Distribute vertically",
"viewMode": "View mode"
}, },
"buttons": { "buttons": {
"clearReset": "Reset the canvas", "clearReset": "Reset the canvas",

View File

@ -16,12 +16,16 @@ Please add the latest change on the top under the correct section.
## Excalidraw API ## 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). - Expose `getAppState` on `excalidrawRef` [#2834](https://github.com/excalidraw/excalidraw/pull/2834).
## Excalidraw Library ## Excalidraw Library
### Features ### 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) - 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). - 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 | | [`onExportToBackend`](#onExportToBackend) | Function | | Callback triggered when link button is clicked on export dialog |
| [`langCode`](#langCode) | string | `en` | Language code string | | [`langCode`](#langCode) | string | `en` | Language code string |
| [`renderFooter `](#renderFooter) | Function | | Function that renders custom UI footer | | [`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` ### `Extra API's`
@ -330,3 +331,7 @@ import { defaultLang, languages } from "@excalidraw/excalidraw";
#### `renderFooter` #### `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). 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, onExportToBackend,
renderFooter, renderFooter,
langCode = defaultLang.code, langCode = defaultLang.code,
viewModeEnabled,
} = props; } = props;
useEffect(() => { useEffect(() => {
@ -64,6 +65,7 @@ const Excalidraw = (props: ExcalidrawProps) => {
onExportToBackend={onExportToBackend} onExportToBackend={onExportToBackend}
renderFooter={renderFooter} renderFooter={renderFooter}
langCode={langCode} langCode={langCode}
viewModeEnabled={viewModeEnabled}
/> />
</IsMobileProvider> </IsMobileProvider>
</InitializeApp> </InitializeApp>
@ -81,7 +83,6 @@ const areEqual = (
const prevKeys = Object.keys(prevProps) as (keyof typeof prev)[]; const prevKeys = Object.keys(prevProps) as (keyof typeof prev)[];
const nextKeys = Object.keys(nextProps) as (keyof typeof next)[]; const nextKeys = Object.keys(nextProps) as (keyof typeof next)[];
return ( return (
prevUser?.name === nextUser?.name && prevUser?.name === nextUser?.name &&
prevKeys.length === nextKeys.length && prevKeys.length === nextKeys.length &&
@ -89,6 +90,10 @@ const areEqual = (
); );
}; };
Excalidraw.defaultProps = {
lanCode: defaultLang.code,
};
const forwardedRefComp = forwardRef< const forwardedRefComp = forwardRef<
ExcalidrawAPIRefValue, ExcalidrawAPIRefValue,
PublicExcalidrawProps PublicExcalidrawProps

View File

@ -373,12 +373,14 @@ export const renderScene = (
sceneState.zoom, sceneState.zoom,
"mouse", // when we render we don't know which pointer type so use mouse "mouse", // when we render we don't know which pointer type so use mouse
); );
if (!appState.viewModeEnabled) {
renderTransformHandles( renderTransformHandles(
context, context,
sceneState, sceneState,
transformHandles, transformHandles,
locallySelectedElements[0].angle, locallySelectedElements[0].angle,
); );
}
} else if (locallySelectedElements.length > 1 && !appState.isRotating) { } else if (locallySelectedElements.length > 1 && !appState.isRotating) {
const dashedLinePadding = 4 / sceneState.zoom.value; const dashedLinePadding = 4 / sceneState.zoom.value;
context.fillStyle = oc.white; context.fillStyle = oc.white;

View File

@ -76,6 +76,7 @@ Object {
"suggestedBindings": Array [], "suggestedBindings": Array [],
"toastMessage": null, "toastMessage": null,
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"viewModeEnabled": false,
"width": 1024, "width": 1024,
"zenModeEnabled": false, "zenModeEnabled": false,
"zoom": Object { "zoom": Object {
@ -542,6 +543,7 @@ Object {
"suggestedBindings": Array [], "suggestedBindings": Array [],
"toastMessage": null, "toastMessage": null,
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"viewModeEnabled": false,
"width": 1024, "width": 1024,
"zenModeEnabled": false, "zenModeEnabled": false,
"zoom": Object { "zoom": Object {
@ -990,6 +992,7 @@ Object {
"suggestedBindings": Array [], "suggestedBindings": Array [],
"toastMessage": null, "toastMessage": null,
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"viewModeEnabled": false,
"width": 1024, "width": 1024,
"zenModeEnabled": false, "zenModeEnabled": false,
"zoom": Object { "zoom": Object {
@ -1766,6 +1769,7 @@ Object {
"suggestedBindings": Array [], "suggestedBindings": Array [],
"toastMessage": null, "toastMessage": null,
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"viewModeEnabled": false,
"width": 1024, "width": 1024,
"zenModeEnabled": false, "zenModeEnabled": false,
"zoom": Object { "zoom": Object {
@ -1973,6 +1977,7 @@ Object {
"suggestedBindings": Array [], "suggestedBindings": Array [],
"toastMessage": null, "toastMessage": null,
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"viewModeEnabled": false,
"width": 1024, "width": 1024,
"zenModeEnabled": false, "zenModeEnabled": false,
"zoom": Object { "zoom": Object {
@ -2424,6 +2429,7 @@ Object {
"suggestedBindings": Array [], "suggestedBindings": Array [],
"toastMessage": null, "toastMessage": null,
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"viewModeEnabled": false,
"width": 1024, "width": 1024,
"zenModeEnabled": false, "zenModeEnabled": false,
"zoom": Object { "zoom": Object {
@ -2672,6 +2678,7 @@ Object {
"suggestedBindings": Array [], "suggestedBindings": Array [],
"toastMessage": null, "toastMessage": null,
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"viewModeEnabled": false,
"width": 1024, "width": 1024,
"zenModeEnabled": false, "zenModeEnabled": false,
"zoom": Object { "zoom": Object {
@ -2837,6 +2844,7 @@ Object {
"suggestedBindings": Array [], "suggestedBindings": Array [],
"toastMessage": null, "toastMessage": null,
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"viewModeEnabled": false,
"width": 1024, "width": 1024,
"zenModeEnabled": false, "zenModeEnabled": false,
"zoom": Object { "zoom": Object {
@ -3309,6 +3317,7 @@ Object {
"suggestedBindings": Array [], "suggestedBindings": Array [],
"toastMessage": null, "toastMessage": null,
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"viewModeEnabled": false,
"width": 1024, "width": 1024,
"zenModeEnabled": false, "zenModeEnabled": false,
"zoom": Object { "zoom": Object {
@ -3618,6 +3627,7 @@ Object {
"suggestedBindings": Array [], "suggestedBindings": Array [],
"toastMessage": null, "toastMessage": null,
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"viewModeEnabled": false,
"width": 1024, "width": 1024,
"zenModeEnabled": false, "zenModeEnabled": false,
"zoom": Object { "zoom": Object {
@ -3822,6 +3832,7 @@ Object {
"suggestedBindings": Array [], "suggestedBindings": Array [],
"toastMessage": null, "toastMessage": null,
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"viewModeEnabled": false,
"width": 1024, "width": 1024,
"zenModeEnabled": false, "zenModeEnabled": false,
"zoom": Object { "zoom": Object {
@ -4062,6 +4073,7 @@ Object {
"suggestedBindings": Array [], "suggestedBindings": Array [],
"toastMessage": null, "toastMessage": null,
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"viewModeEnabled": false,
"width": 1024, "width": 1024,
"zenModeEnabled": false, "zenModeEnabled": false,
"zoom": Object { "zoom": Object {
@ -4313,6 +4325,7 @@ Object {
"suggestedBindings": Array [], "suggestedBindings": Array [],
"toastMessage": null, "toastMessage": null,
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"viewModeEnabled": false,
"width": 1024, "width": 1024,
"zenModeEnabled": false, "zenModeEnabled": false,
"zoom": Object { "zoom": Object {
@ -4714,6 +4727,7 @@ Object {
"suggestedBindings": Array [], "suggestedBindings": Array [],
"toastMessage": null, "toastMessage": null,
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"viewModeEnabled": false,
"width": 1024, "width": 1024,
"zenModeEnabled": false, "zenModeEnabled": false,
"zoom": Object { "zoom": Object {
@ -4985,6 +4999,7 @@ Object {
"suggestedBindings": Array [], "suggestedBindings": Array [],
"toastMessage": null, "toastMessage": null,
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"viewModeEnabled": false,
"width": 1024, "width": 1024,
"zenModeEnabled": false, "zenModeEnabled": false,
"zoom": Object { "zoom": Object {
@ -5310,6 +5325,7 @@ Object {
"suggestedBindings": Array [], "suggestedBindings": Array [],
"toastMessage": null, "toastMessage": null,
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"viewModeEnabled": false,
"width": 1024, "width": 1024,
"zenModeEnabled": false, "zenModeEnabled": false,
"zoom": Object { "zoom": Object {
@ -5494,6 +5510,7 @@ Object {
"suggestedBindings": Array [], "suggestedBindings": Array [],
"toastMessage": null, "toastMessage": null,
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"viewModeEnabled": false,
"width": 1024, "width": 1024,
"zenModeEnabled": false, "zenModeEnabled": false,
"zoom": Object { "zoom": Object {
@ -5656,6 +5673,7 @@ Object {
"suggestedBindings": Array [], "suggestedBindings": Array [],
"toastMessage": null, "toastMessage": null,
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"viewModeEnabled": false,
"width": 1024, "width": 1024,
"zenModeEnabled": false, "zenModeEnabled": false,
"zoom": Object { "zoom": Object {
@ -6114,6 +6132,7 @@ Object {
"suggestedBindings": Array [], "suggestedBindings": Array [],
"toastMessage": null, "toastMessage": null,
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"viewModeEnabled": false,
"width": 1024, "width": 1024,
"zenModeEnabled": false, "zenModeEnabled": false,
"zoom": Object { "zoom": Object {
@ -6423,6 +6442,7 @@ Object {
"suggestedBindings": Array [], "suggestedBindings": Array [],
"toastMessage": null, "toastMessage": null,
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"viewModeEnabled": false,
"width": 1024, "width": 1024,
"zenModeEnabled": false, "zenModeEnabled": false,
"zoom": Object { "zoom": Object {
@ -8460,6 +8480,7 @@ Object {
"suggestedBindings": Array [], "suggestedBindings": Array [],
"toastMessage": null, "toastMessage": null,
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"viewModeEnabled": false,
"width": 1024, "width": 1024,
"zenModeEnabled": false, "zenModeEnabled": false,
"zoom": Object { "zoom": Object {
@ -8821,6 +8842,7 @@ Object {
"suggestedBindings": Array [], "suggestedBindings": Array [],
"toastMessage": null, "toastMessage": null,
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"viewModeEnabled": false,
"width": 1024, "width": 1024,
"zenModeEnabled": false, "zenModeEnabled": false,
"zoom": Object { "zoom": Object {
@ -9072,6 +9094,7 @@ Object {
"suggestedBindings": Array [], "suggestedBindings": Array [],
"toastMessage": null, "toastMessage": null,
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"viewModeEnabled": false,
"width": 1024, "width": 1024,
"zenModeEnabled": false, "zenModeEnabled": false,
"zoom": Object { "zoom": Object {
@ -9324,6 +9347,7 @@ Object {
"suggestedBindings": Array [], "suggestedBindings": Array [],
"toastMessage": null, "toastMessage": null,
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"viewModeEnabled": false,
"width": 1024, "width": 1024,
"zenModeEnabled": false, "zenModeEnabled": false,
"zoom": Object { "zoom": Object {
@ -9632,6 +9656,7 @@ Object {
"suggestedBindings": Array [], "suggestedBindings": Array [],
"toastMessage": null, "toastMessage": null,
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"viewModeEnabled": false,
"width": 1024, "width": 1024,
"zenModeEnabled": false, "zenModeEnabled": false,
"zoom": Object { "zoom": Object {
@ -9794,6 +9819,7 @@ Object {
"suggestedBindings": Array [], "suggestedBindings": Array [],
"toastMessage": null, "toastMessage": null,
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"viewModeEnabled": false,
"width": 1024, "width": 1024,
"zenModeEnabled": false, "zenModeEnabled": false,
"zoom": Object { "zoom": Object {
@ -9956,6 +9982,7 @@ Object {
"suggestedBindings": Array [], "suggestedBindings": Array [],
"toastMessage": null, "toastMessage": null,
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"viewModeEnabled": false,
"width": 1024, "width": 1024,
"zenModeEnabled": false, "zenModeEnabled": false,
"zoom": Object { "zoom": Object {
@ -10118,6 +10145,7 @@ Object {
"suggestedBindings": Array [], "suggestedBindings": Array [],
"toastMessage": null, "toastMessage": null,
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"viewModeEnabled": false,
"width": 1024, "width": 1024,
"zenModeEnabled": false, "zenModeEnabled": false,
"zoom": Object { "zoom": Object {
@ -10310,6 +10338,7 @@ Object {
"suggestedBindings": Array [], "suggestedBindings": Array [],
"toastMessage": null, "toastMessage": null,
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"viewModeEnabled": false,
"width": 1024, "width": 1024,
"zenModeEnabled": false, "zenModeEnabled": false,
"zoom": Object { "zoom": Object {
@ -10502,6 +10531,7 @@ Object {
"suggestedBindings": Array [], "suggestedBindings": Array [],
"toastMessage": null, "toastMessage": null,
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"viewModeEnabled": false,
"width": 1024, "width": 1024,
"zenModeEnabled": false, "zenModeEnabled": false,
"zoom": Object { "zoom": Object {
@ -10694,6 +10724,7 @@ Object {
"suggestedBindings": Array [], "suggestedBindings": Array [],
"toastMessage": null, "toastMessage": null,
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"viewModeEnabled": false,
"width": 1024, "width": 1024,
"zenModeEnabled": false, "zenModeEnabled": false,
"zoom": Object { "zoom": Object {
@ -10886,6 +10917,7 @@ Object {
"suggestedBindings": Array [], "suggestedBindings": Array [],
"toastMessage": null, "toastMessage": null,
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"viewModeEnabled": false,
"width": 1024, "width": 1024,
"zenModeEnabled": false, "zenModeEnabled": false,
"zoom": Object { "zoom": Object {
@ -11048,6 +11080,7 @@ Object {
"suggestedBindings": Array [], "suggestedBindings": Array [],
"toastMessage": null, "toastMessage": null,
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"viewModeEnabled": false,
"width": 1024, "width": 1024,
"zenModeEnabled": false, "zenModeEnabled": false,
"zoom": Object { "zoom": Object {
@ -11210,6 +11243,7 @@ Object {
"suggestedBindings": Array [], "suggestedBindings": Array [],
"toastMessage": null, "toastMessage": null,
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"viewModeEnabled": false,
"width": 1024, "width": 1024,
"zenModeEnabled": false, "zenModeEnabled": false,
"zoom": Object { "zoom": Object {
@ -11402,6 +11436,7 @@ Object {
"suggestedBindings": Array [], "suggestedBindings": Array [],
"toastMessage": null, "toastMessage": null,
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"viewModeEnabled": false,
"width": 1024, "width": 1024,
"zenModeEnabled": false, "zenModeEnabled": false,
"zoom": Object { "zoom": Object {
@ -11564,6 +11599,7 @@ Object {
"suggestedBindings": Array [], "suggestedBindings": Array [],
"toastMessage": null, "toastMessage": null,
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"viewModeEnabled": false,
"width": 1024, "width": 1024,
"zenModeEnabled": false, "zenModeEnabled": false,
"zoom": Object { "zoom": Object {
@ -11767,6 +11803,7 @@ Object {
"suggestedBindings": Array [], "suggestedBindings": Array [],
"toastMessage": null, "toastMessage": null,
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"viewModeEnabled": false,
"width": 1024, "width": 1024,
"zenModeEnabled": false, "zenModeEnabled": false,
"zoom": Object { "zoom": Object {
@ -12474,6 +12511,7 @@ Object {
"suggestedBindings": Array [], "suggestedBindings": Array [],
"toastMessage": null, "toastMessage": null,
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"viewModeEnabled": false,
"width": 1024, "width": 1024,
"zenModeEnabled": false, "zenModeEnabled": false,
"zoom": Object { "zoom": Object {
@ -12721,6 +12759,7 @@ Object {
"suggestedBindings": Array [], "suggestedBindings": Array [],
"toastMessage": null, "toastMessage": null,
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"viewModeEnabled": false,
"width": 1024, "width": 1024,
"zenModeEnabled": false, "zenModeEnabled": false,
"zoom": Object { "zoom": Object {
@ -12819,6 +12858,7 @@ Object {
"suggestedBindings": Array [], "suggestedBindings": Array [],
"toastMessage": null, "toastMessage": null,
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"viewModeEnabled": false,
"width": 1024, "width": 1024,
"zenModeEnabled": false, "zenModeEnabled": false,
"zoom": Object { "zoom": Object {
@ -12919,6 +12959,7 @@ Object {
"suggestedBindings": Array [], "suggestedBindings": Array [],
"toastMessage": null, "toastMessage": null,
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"viewModeEnabled": false,
"width": 1024, "width": 1024,
"zenModeEnabled": false, "zenModeEnabled": false,
"zoom": Object { "zoom": Object {
@ -13081,6 +13122,7 @@ Object {
"suggestedBindings": Array [], "suggestedBindings": Array [],
"toastMessage": null, "toastMessage": null,
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"viewModeEnabled": false,
"width": 1024, "width": 1024,
"zenModeEnabled": false, "zenModeEnabled": false,
"zoom": Object { "zoom": Object {
@ -13387,6 +13429,7 @@ Object {
"suggestedBindings": Array [], "suggestedBindings": Array [],
"toastMessage": null, "toastMessage": null,
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"viewModeEnabled": false,
"width": 1024, "width": 1024,
"zenModeEnabled": false, "zenModeEnabled": false,
"zoom": Object { "zoom": Object {
@ -13693,6 +13736,7 @@ Object {
"suggestedBindings": Array [], "suggestedBindings": Array [],
"toastMessage": "Copied styles.", "toastMessage": "Copied styles.",
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"viewModeEnabled": false,
"width": 1024, "width": 1024,
"zenModeEnabled": false, "zenModeEnabled": false,
"zoom": Object { "zoom": Object {
@ -13853,6 +13897,7 @@ Object {
"suggestedBindings": Array [], "suggestedBindings": Array [],
"toastMessage": null, "toastMessage": null,
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"viewModeEnabled": false,
"width": 1024, "width": 1024,
"zenModeEnabled": false, "zenModeEnabled": false,
"zoom": Object { "zoom": Object {
@ -14049,6 +14094,7 @@ Object {
"suggestedBindings": Array [], "suggestedBindings": Array [],
"toastMessage": null, "toastMessage": null,
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"viewModeEnabled": false,
"width": 1024, "width": 1024,
"zenModeEnabled": false, "zenModeEnabled": false,
"zoom": Object { "zoom": Object {
@ -14302,6 +14348,7 @@ Object {
"suggestedBindings": Array [], "suggestedBindings": Array [],
"toastMessage": null, "toastMessage": null,
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"viewModeEnabled": false,
"width": 1024, "width": 1024,
"zenModeEnabled": false, "zenModeEnabled": false,
"zoom": Object { "zoom": Object {
@ -14618,6 +14665,7 @@ Object {
"suggestedBindings": Array [], "suggestedBindings": Array [],
"toastMessage": "Copied styles.", "toastMessage": "Copied styles.",
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"viewModeEnabled": false,
"width": 1024, "width": 1024,
"zenModeEnabled": false, "zenModeEnabled": false,
"zoom": Object { "zoom": Object {
@ -15455,6 +15503,7 @@ Object {
"suggestedBindings": Array [], "suggestedBindings": Array [],
"toastMessage": null, "toastMessage": null,
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"viewModeEnabled": false,
"width": 1024, "width": 1024,
"zenModeEnabled": false, "zenModeEnabled": false,
"zoom": Object { "zoom": Object {
@ -15761,6 +15810,7 @@ Object {
"suggestedBindings": Array [], "suggestedBindings": Array [],
"toastMessage": null, "toastMessage": null,
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"viewModeEnabled": false,
"width": 1024, "width": 1024,
"zenModeEnabled": false, "zenModeEnabled": false,
"zoom": Object { "zoom": Object {
@ -16071,6 +16121,7 @@ Object {
"suggestedBindings": Array [], "suggestedBindings": Array [],
"toastMessage": null, "toastMessage": null,
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"viewModeEnabled": false,
"width": 1024, "width": 1024,
"zenModeEnabled": false, "zenModeEnabled": false,
"zoom": Object { "zoom": Object {
@ -16447,6 +16498,7 @@ Object {
"suggestedBindings": Array [], "suggestedBindings": Array [],
"toastMessage": null, "toastMessage": null,
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"viewModeEnabled": false,
"width": 1024, "width": 1024,
"zenModeEnabled": false, "zenModeEnabled": false,
"zoom": Object { "zoom": Object {
@ -16618,6 +16670,7 @@ Object {
"suggestedBindings": Array [], "suggestedBindings": Array [],
"toastMessage": null, "toastMessage": null,
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"viewModeEnabled": false,
"width": 1024, "width": 1024,
"zenModeEnabled": false, "zenModeEnabled": false,
"zoom": Object { "zoom": Object {
@ -16931,6 +16984,7 @@ Object {
"suggestedBindings": Array [], "suggestedBindings": Array [],
"toastMessage": null, "toastMessage": null,
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"viewModeEnabled": false,
"width": 1024, "width": 1024,
"zenModeEnabled": false, "zenModeEnabled": false,
"zoom": Object { "zoom": Object {
@ -17171,6 +17225,7 @@ Object {
"suggestedBindings": Array [], "suggestedBindings": Array [],
"toastMessage": null, "toastMessage": null,
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"viewModeEnabled": false,
"width": 1024, "width": 1024,
"zenModeEnabled": false, "zenModeEnabled": false,
"zoom": Object { "zoom": Object {
@ -17426,6 +17481,7 @@ Object {
"suggestedBindings": Array [], "suggestedBindings": Array [],
"toastMessage": null, "toastMessage": null,
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"viewModeEnabled": false,
"width": 1024, "width": 1024,
"zenModeEnabled": false, "zenModeEnabled": false,
"zoom": Object { "zoom": Object {
@ -17741,6 +17797,7 @@ Object {
"suggestedBindings": Array [], "suggestedBindings": Array [],
"toastMessage": null, "toastMessage": null,
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"viewModeEnabled": false,
"width": 1024, "width": 1024,
"zenModeEnabled": false, "zenModeEnabled": false,
"zoom": Object { "zoom": Object {
@ -17841,6 +17898,7 @@ Object {
"suggestedBindings": Array [], "suggestedBindings": Array [],
"toastMessage": null, "toastMessage": null,
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"viewModeEnabled": false,
"width": 1024, "width": 1024,
"zenModeEnabled": false, "zenModeEnabled": false,
"zoom": Object { "zoom": Object {
@ -18014,6 +18072,7 @@ Object {
"suggestedBindings": Array [], "suggestedBindings": Array [],
"toastMessage": null, "toastMessage": null,
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"viewModeEnabled": false,
"width": 1024, "width": 1024,
"zenModeEnabled": false, "zenModeEnabled": false,
"zoom": Object { "zoom": Object {
@ -18820,6 +18879,7 @@ Object {
"suggestedBindings": Array [], "suggestedBindings": Array [],
"toastMessage": null, "toastMessage": null,
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"viewModeEnabled": false,
"width": 1024, "width": 1024,
"zenModeEnabled": false, "zenModeEnabled": false,
"zoom": Object { "zoom": Object {
@ -18922,6 +18982,7 @@ Object {
"suggestedBindings": Array [], "suggestedBindings": Array [],
"toastMessage": null, "toastMessage": null,
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"viewModeEnabled": false,
"width": 1024, "width": 1024,
"zenModeEnabled": false, "zenModeEnabled": false,
"zoom": Object { "zoom": Object {
@ -19698,6 +19759,7 @@ Object {
"suggestedBindings": Array [], "suggestedBindings": Array [],
"toastMessage": null, "toastMessage": null,
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"viewModeEnabled": false,
"width": 1024, "width": 1024,
"zenModeEnabled": false, "zenModeEnabled": false,
"zoom": Object { "zoom": Object {
@ -20099,6 +20161,7 @@ Object {
"suggestedBindings": Array [], "suggestedBindings": Array [],
"toastMessage": null, "toastMessage": null,
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"viewModeEnabled": false,
"width": 1024, "width": 1024,
"zenModeEnabled": false, "zenModeEnabled": false,
"zoom": Object { "zoom": Object {
@ -20346,6 +20409,7 @@ Object {
"suggestedBindings": Array [], "suggestedBindings": Array [],
"toastMessage": null, "toastMessage": null,
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"viewModeEnabled": false,
"width": 1024, "width": 1024,
"zenModeEnabled": false, "zenModeEnabled": false,
"zoom": Object { "zoom": Object {
@ -20446,6 +20510,7 @@ Object {
"suggestedBindings": Array [], "suggestedBindings": Array [],
"toastMessage": null, "toastMessage": null,
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"viewModeEnabled": false,
"width": 1024, "width": 1024,
"zenModeEnabled": false, "zenModeEnabled": false,
"zoom": Object { "zoom": Object {
@ -20940,6 +21005,7 @@ Object {
"suggestedBindings": Array [], "suggestedBindings": Array [],
"toastMessage": null, "toastMessage": null,
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"viewModeEnabled": false,
"width": 1024, "width": 1024,
"zenModeEnabled": false, "zenModeEnabled": false,
"zoom": Object { "zoom": Object {
@ -21038,6 +21104,7 @@ Object {
"suggestedBindings": Array [], "suggestedBindings": Array [],
"toastMessage": null, "toastMessage": null,
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"viewModeEnabled": false,
"width": 1024, "width": 1024,
"zenModeEnabled": false, "zenModeEnabled": false,
"zoom": Object { "zoom": Object {

View File

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

View File

@ -85,6 +85,7 @@ export type AppState = {
zenModeEnabled: boolean; zenModeEnabled: boolean;
appearance: "light" | "dark"; appearance: "light" | "dark";
gridSize: number | null; gridSize: number | null;
viewModeEnabled: boolean;
/** top-most selected groups (i.e. does not include nested groups) */ /** top-most selected groups (i.e. does not include nested groups) */
selectedGroupIds: { [groupId: string]: boolean }; selectedGroupIds: { [groupId: string]: boolean };
@ -181,6 +182,7 @@ export interface ExcalidrawProps {
) => void; ) => void;
renderFooter?: (isMobile: boolean) => JSX.Element; renderFooter?: (isMobile: boolean) => JSX.Element;
langCode?: Language["code"]; langCode?: Language["code"];
viewModeEnabled?: boolean;
} }
export type SceneData = { export type SceneData = {