mirror of
				https://github.com/excalidraw/excalidraw.git
				synced 2025-11-04 12:54:23 +01:00 
			
		
		
		
	Compare commits
	
		
			3 Commits
		
	
	
		
			dwelle/exp
			...
			zsviczian-
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					4c39abf07f | ||
| 
						 | 
					02c1236962 | ||
| 
						 | 
					25ea97d0f9 | 
@@ -89,28 +89,6 @@ export const actionChangeExportScale = register({
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const actionChangeExportPadding = register({
 | 
			
		||||
  name: "changeExportPadding",
 | 
			
		||||
  trackEvent: { category: "export", action: "togglePadding" },
 | 
			
		||||
  perform: (_elements, appState, value) => {
 | 
			
		||||
    return {
 | 
			
		||||
      appState: {
 | 
			
		||||
        ...appState,
 | 
			
		||||
        exportPadding: value ? DEFAULT_EXPORT_PADDING : 0,
 | 
			
		||||
      },
 | 
			
		||||
      commitToHistory: false,
 | 
			
		||||
    };
 | 
			
		||||
  },
 | 
			
		||||
  PanelComponent: ({ appState, updateData }) => (
 | 
			
		||||
    <CheckboxItem
 | 
			
		||||
      checked={!!appState.exportPadding}
 | 
			
		||||
      onChange={(checked) => updateData(checked)}
 | 
			
		||||
    >
 | 
			
		||||
      {"Padding"}
 | 
			
		||||
    </CheckboxItem>
 | 
			
		||||
  ),
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const actionChangeExportBackground = register({
 | 
			
		||||
  name: "changeExportBackground",
 | 
			
		||||
  trackEvent: { category: "export", action: "toggleBackground" },
 | 
			
		||||
 
 | 
			
		||||
@@ -68,7 +68,6 @@ export type ActionName =
 | 
			
		||||
  | "finalize"
 | 
			
		||||
  | "changeProjectName"
 | 
			
		||||
  | "changeExportBackground"
 | 
			
		||||
  | "changeExportPadding"
 | 
			
		||||
  | "changeExportEmbedScene"
 | 
			
		||||
  | "changeExportScale"
 | 
			
		||||
  | "saveToActiveFile"
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,5 @@
 | 
			
		||||
import oc from "open-color";
 | 
			
		||||
import {
 | 
			
		||||
  DEFAULT_EXPORT_PADDING,
 | 
			
		||||
  DEFAULT_FONT_FAMILY,
 | 
			
		||||
  DEFAULT_FONT_SIZE,
 | 
			
		||||
  DEFAULT_TEXT_ALIGN,
 | 
			
		||||
@@ -56,7 +55,6 @@ export const getDefaultAppState = (): Omit<
 | 
			
		||||
    exportScale: defaultExportScale,
 | 
			
		||||
    exportEmbedScene: false,
 | 
			
		||||
    exportWithDarkMode: false,
 | 
			
		||||
    exportPadding: DEFAULT_EXPORT_PADDING,
 | 
			
		||||
    fileHandle: null,
 | 
			
		||||
    gridSize: null,
 | 
			
		||||
    isBindingEnabled: true,
 | 
			
		||||
@@ -147,7 +145,6 @@ const APP_STATE_STORAGE_CONF = (<
 | 
			
		||||
  exportBackground: { browser: true, export: false, server: false },
 | 
			
		||||
  exportEmbedScene: { browser: true, export: false, server: false },
 | 
			
		||||
  exportScale: { browser: true, export: false, server: false },
 | 
			
		||||
  exportPadding: { browser: true, export: false, server: false },
 | 
			
		||||
  exportWithDarkMode: { browser: true, export: false, server: false },
 | 
			
		||||
  fileHandle: { browser: false, export: false, server: false },
 | 
			
		||||
  gridSize: { browser: true, export: true, server: true },
 | 
			
		||||
 
 | 
			
		||||
@@ -516,6 +516,7 @@ class App extends React.Component<AppProps, AppState> {
 | 
			
		||||
    const {
 | 
			
		||||
      onCollabButtonClick,
 | 
			
		||||
      renderTopRightUI,
 | 
			
		||||
      renderMenuLinks,
 | 
			
		||||
      renderFooter,
 | 
			
		||||
      renderCustomStats,
 | 
			
		||||
    } = this.props;
 | 
			
		||||
@@ -562,6 +563,7 @@ class App extends React.Component<AppProps, AppState> {
 | 
			
		||||
                    langCode={getLanguage().code}
 | 
			
		||||
                    isCollaborating={this.props.isCollaborating}
 | 
			
		||||
                    renderTopRightUI={renderTopRightUI}
 | 
			
		||||
                    renderMenuLinks={renderMenuLinks}
 | 
			
		||||
                    renderCustomFooter={renderFooter}
 | 
			
		||||
                    renderCustomStats={renderCustomStats}
 | 
			
		||||
                    renderCustomSidebar={this.props.renderSidebar}
 | 
			
		||||
@@ -576,6 +578,7 @@ class App extends React.Component<AppProps, AppState> {
 | 
			
		||||
                    id={this.id}
 | 
			
		||||
                    onImageAction={this.onImageAction}
 | 
			
		||||
                    renderWelcomeScreen={
 | 
			
		||||
                      this.props.hideWelcomeScreen !== true &&
 | 
			
		||||
                      this.state.showWelcomeScreen &&
 | 
			
		||||
                      this.state.activeTool.type === "selection" &&
 | 
			
		||||
                      !this.scene.getElementsIncludingDeleted().length
 | 
			
		||||
 
 | 
			
		||||
@@ -79,6 +79,7 @@ const ImageExportModal = ({
 | 
			
		||||
  elements,
 | 
			
		||||
  appState,
 | 
			
		||||
  files,
 | 
			
		||||
  exportPadding = DEFAULT_EXPORT_PADDING,
 | 
			
		||||
  actionManager,
 | 
			
		||||
  onExportToPng,
 | 
			
		||||
  onExportToSvg,
 | 
			
		||||
@@ -87,6 +88,7 @@ const ImageExportModal = ({
 | 
			
		||||
  appState: AppState;
 | 
			
		||||
  elements: readonly NonDeletedExcalidrawElement[];
 | 
			
		||||
  files: BinaryFiles;
 | 
			
		||||
  exportPadding?: number;
 | 
			
		||||
  actionManager: ActionManager;
 | 
			
		||||
  onExportToPng: ExportCB;
 | 
			
		||||
  onExportToSvg: ExportCB;
 | 
			
		||||
@@ -114,7 +116,7 @@ const ImageExportModal = ({
 | 
			
		||||
    exportToCanvas(exportedElements, appState, files, {
 | 
			
		||||
      exportBackground,
 | 
			
		||||
      viewBackgroundColor,
 | 
			
		||||
      exportPadding: appState.exportPadding,
 | 
			
		||||
      exportPadding,
 | 
			
		||||
    })
 | 
			
		||||
      .then((canvas) => {
 | 
			
		||||
        // if converting to blob fails, there's some problem that will
 | 
			
		||||
@@ -132,6 +134,7 @@ const ImageExportModal = ({
 | 
			
		||||
    files,
 | 
			
		||||
    exportedElements,
 | 
			
		||||
    exportBackground,
 | 
			
		||||
    exportPadding,
 | 
			
		||||
    viewBackgroundColor,
 | 
			
		||||
  ]);
 | 
			
		||||
 | 
			
		||||
@@ -148,10 +151,8 @@ const ImageExportModal = ({
 | 
			
		||||
            // dunno why this is needed, but when the items wrap it creates
 | 
			
		||||
            // an overflow
 | 
			
		||||
            overflow: "hidden",
 | 
			
		||||
            gap: ".6rem",
 | 
			
		||||
          }}
 | 
			
		||||
        >
 | 
			
		||||
          {actionManager.renderAction("changeExportPadding")}
 | 
			
		||||
          {actionManager.renderAction("changeExportBackground")}
 | 
			
		||||
          {someElementIsSelected && (
 | 
			
		||||
            <CheckboxItem
 | 
			
		||||
@@ -220,6 +221,7 @@ export const ImageExportDialog = ({
 | 
			
		||||
  appState,
 | 
			
		||||
  setAppState,
 | 
			
		||||
  files,
 | 
			
		||||
  exportPadding = DEFAULT_EXPORT_PADDING,
 | 
			
		||||
  actionManager,
 | 
			
		||||
  onExportToPng,
 | 
			
		||||
  onExportToSvg,
 | 
			
		||||
@@ -229,6 +231,7 @@ export const ImageExportDialog = ({
 | 
			
		||||
  setAppState: React.Component<any, AppState>["setState"];
 | 
			
		||||
  elements: readonly NonDeletedExcalidrawElement[];
 | 
			
		||||
  files: BinaryFiles;
 | 
			
		||||
  exportPadding?: number;
 | 
			
		||||
  actionManager: ActionManager;
 | 
			
		||||
  onExportToPng: ExportCB;
 | 
			
		||||
  onExportToSvg: ExportCB;
 | 
			
		||||
@@ -246,6 +249,7 @@ export const ImageExportDialog = ({
 | 
			
		||||
            elements={elements}
 | 
			
		||||
            appState={appState}
 | 
			
		||||
            files={files}
 | 
			
		||||
            exportPadding={exportPadding}
 | 
			
		||||
            actionManager={actionManager}
 | 
			
		||||
            onExportToPng={onExportToPng}
 | 
			
		||||
            onExportToSvg={onExportToSvg}
 | 
			
		||||
 
 | 
			
		||||
@@ -71,6 +71,7 @@ interface LayerUIProps {
 | 
			
		||||
  langCode: Language["code"];
 | 
			
		||||
  isCollaborating: boolean;
 | 
			
		||||
  renderTopRightUI?: ExcalidrawProps["renderTopRightUI"];
 | 
			
		||||
  renderMenuLinks?: ExcalidrawProps["renderMenuLinks"];
 | 
			
		||||
  renderCustomFooter?: ExcalidrawProps["renderFooter"];
 | 
			
		||||
  renderCustomStats?: ExcalidrawProps["renderCustomStats"];
 | 
			
		||||
  renderCustomSidebar?: ExcalidrawProps["renderSidebar"];
 | 
			
		||||
@@ -96,6 +97,7 @@ const LayerUI = ({
 | 
			
		||||
  showExitZenModeBtn,
 | 
			
		||||
  isCollaborating,
 | 
			
		||||
  renderTopRightUI,
 | 
			
		||||
  renderMenuLinks,
 | 
			
		||||
  renderCustomFooter,
 | 
			
		||||
  renderCustomStats,
 | 
			
		||||
  renderCustomSidebar,
 | 
			
		||||
@@ -144,7 +146,6 @@ const LayerUI = ({
 | 
			
		||||
            exportBackground: appState.exportBackground,
 | 
			
		||||
            name: appState.name,
 | 
			
		||||
            viewBackgroundColor: appState.viewBackgroundColor,
 | 
			
		||||
            exportPadding: appState.exportPadding,
 | 
			
		||||
          },
 | 
			
		||||
        )
 | 
			
		||||
          .catch(muteFSAbortError)
 | 
			
		||||
@@ -197,6 +198,7 @@ const LayerUI = ({
 | 
			
		||||
        })}
 | 
			
		||||
        onClick={() => setIsMenuOpen(!isMenuOpen)}
 | 
			
		||||
        type="button"
 | 
			
		||||
        data-testid="menu-button"
 | 
			
		||||
      >
 | 
			
		||||
        {HamburgerMenuIcon}
 | 
			
		||||
      </button>
 | 
			
		||||
@@ -218,16 +220,19 @@ const LayerUI = ({
 | 
			
		||||
                actionManager.renderAction("loadScene")}
 | 
			
		||||
              {/* // TODO barnabasmolnar/editor-redesign  */}
 | 
			
		||||
              {/* is this fine here? */}
 | 
			
		||||
              {appState.fileHandle &&
 | 
			
		||||
              {UIOptions.canvasActions.saveToActiveFile &&
 | 
			
		||||
                appState.fileHandle &&
 | 
			
		||||
                actionManager.renderAction("saveToActiveFile")}
 | 
			
		||||
              {renderJSONExportDialog()}
 | 
			
		||||
              <MenuItem
 | 
			
		||||
                label={t("buttons.exportImage")}
 | 
			
		||||
                icon={ExportImageIcon}
 | 
			
		||||
                dataTestId="image-export-button"
 | 
			
		||||
                onClick={() => setAppState({ openDialog: "imageExport" })}
 | 
			
		||||
                shortcut={getShortcutFromShortcutName("imageExport")}
 | 
			
		||||
              />
 | 
			
		||||
              {UIOptions.canvasActions.saveAsImage && (
 | 
			
		||||
                <MenuItem
 | 
			
		||||
                  label={t("buttons.exportImage")}
 | 
			
		||||
                  icon={ExportImageIcon}
 | 
			
		||||
                  dataTestId="image-export-button"
 | 
			
		||||
                  onClick={() => setAppState({ openDialog: "imageExport" })}
 | 
			
		||||
                  shortcut={getShortcutFromShortcutName("imageExport")}
 | 
			
		||||
                />
 | 
			
		||||
              )}
 | 
			
		||||
              {onCollabButtonClick && (
 | 
			
		||||
                <CollabButton
 | 
			
		||||
                  isCollaborating={isCollaborating}
 | 
			
		||||
@@ -238,8 +243,16 @@ const LayerUI = ({
 | 
			
		||||
              {actionManager.renderAction("toggleShortcuts", undefined, true)}
 | 
			
		||||
              {!appState.viewModeEnabled &&
 | 
			
		||||
                actionManager.renderAction("clearCanvas")}
 | 
			
		||||
              <Separator />
 | 
			
		||||
              <MenuLinks />
 | 
			
		||||
              {typeof renderMenuLinks === "undefined" ? ( //zsviczian
 | 
			
		||||
                <Separator />
 | 
			
		||||
              ) : (
 | 
			
		||||
                renderMenuLinks && <Separator />
 | 
			
		||||
              )}
 | 
			
		||||
              {typeof renderMenuLinks === "undefined" ? ( //zsviczian
 | 
			
		||||
                <MenuLinks />
 | 
			
		||||
              ) : (
 | 
			
		||||
                renderMenuLinks && renderMenuLinks(device.isMobile, appState)
 | 
			
		||||
              )}
 | 
			
		||||
              <Separator />
 | 
			
		||||
              <div
 | 
			
		||||
                style={{
 | 
			
		||||
@@ -249,9 +262,11 @@ const LayerUI = ({
 | 
			
		||||
                }}
 | 
			
		||||
              >
 | 
			
		||||
                <div>{actionManager.renderAction("toggleTheme")}</div>
 | 
			
		||||
                <div style={{ padding: "0 0.625rem" }}>
 | 
			
		||||
                  <LanguageList style={{ width: "100%" }} />
 | 
			
		||||
                </div>
 | 
			
		||||
                {UIOptions.showLanguageList !== false && (
 | 
			
		||||
                  <div style={{ padding: "0 0.625rem" }}>
 | 
			
		||||
                    <LanguageList style={{ width: "100%" }} />
 | 
			
		||||
                  </div>
 | 
			
		||||
                )}
 | 
			
		||||
                {!appState.viewModeEnabled && (
 | 
			
		||||
                  <div>
 | 
			
		||||
                    <div style={{ fontSize: ".75rem", marginBottom: ".5rem" }}>
 | 
			
		||||
@@ -482,9 +497,11 @@ const LayerUI = ({
 | 
			
		||||
          renderCustomFooter={renderCustomFooter}
 | 
			
		||||
          onImageAction={onImageAction}
 | 
			
		||||
          renderTopRightUI={renderTopRightUI}
 | 
			
		||||
          renderMenuLinks={renderMenuLinks}
 | 
			
		||||
          renderCustomStats={renderCustomStats}
 | 
			
		||||
          renderSidebars={renderSidebars}
 | 
			
		||||
          device={device}
 | 
			
		||||
          UIOptions={UIOptions}
 | 
			
		||||
        />
 | 
			
		||||
      )}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
import React from "react";
 | 
			
		||||
import { AppState, Device, ExcalidrawProps } from "../types";
 | 
			
		||||
import { AppProps, AppState, Device, ExcalidrawProps } from "../types";
 | 
			
		||||
import { ActionManager } from "../actions/manager";
 | 
			
		||||
import { t } from "../i18n";
 | 
			
		||||
import Stack from "./Stack";
 | 
			
		||||
@@ -45,10 +45,12 @@ type MobileMenuProps = {
 | 
			
		||||
    isMobile: boolean,
 | 
			
		||||
    appState: AppState,
 | 
			
		||||
  ) => JSX.Element | null;
 | 
			
		||||
  renderMenuLinks?: ExcalidrawProps["renderMenuLinks"];
 | 
			
		||||
  renderCustomStats?: ExcalidrawProps["renderCustomStats"];
 | 
			
		||||
  renderSidebars: () => JSX.Element | null;
 | 
			
		||||
  device: Device;
 | 
			
		||||
  renderWelcomeScreen?: boolean;
 | 
			
		||||
  UIOptions: AppProps["UIOptions"];
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const MobileMenu = ({
 | 
			
		||||
@@ -66,10 +68,12 @@ export const MobileMenu = ({
 | 
			
		||||
  renderCustomFooter,
 | 
			
		||||
  onImageAction,
 | 
			
		||||
  renderTopRightUI,
 | 
			
		||||
  renderMenuLinks,
 | 
			
		||||
  renderCustomStats,
 | 
			
		||||
  renderSidebars,
 | 
			
		||||
  device,
 | 
			
		||||
  renderWelcomeScreen,
 | 
			
		||||
  UIOptions,
 | 
			
		||||
}: MobileMenuProps) => {
 | 
			
		||||
  const renderToolbar = () => {
 | 
			
		||||
    return (
 | 
			
		||||
@@ -111,8 +115,8 @@ export const MobileMenu = ({
 | 
			
		||||
                    />
 | 
			
		||||
                  </Stack.Row>
 | 
			
		||||
                </Island>
 | 
			
		||||
                {renderTopRightUI && renderTopRightUI(true, appState)}
 | 
			
		||||
                <div className="mobile-misc-tools-container">
 | 
			
		||||
                  {renderTopRightUI && renderTopRightUI(true, appState)}
 | 
			
		||||
                  <PenModeButton
 | 
			
		||||
                    checked={appState.penMode}
 | 
			
		||||
                    onChange={onPenModeToggle}
 | 
			
		||||
@@ -192,12 +196,14 @@ export const MobileMenu = ({
 | 
			
		||||
        {!appState.viewModeEnabled && actionManager.renderAction("loadScene")}
 | 
			
		||||
        {renderJSONExportDialog()}
 | 
			
		||||
        {renderImageExportDialog()}
 | 
			
		||||
        <MenuItem
 | 
			
		||||
          label={t("buttons.exportImage")}
 | 
			
		||||
          icon={ExportImageIcon}
 | 
			
		||||
          dataTestId="image-export-button"
 | 
			
		||||
          onClick={() => setAppState({ openDialog: "imageExport" })}
 | 
			
		||||
        />
 | 
			
		||||
        {UIOptions.canvasActions.saveAsImage && (
 | 
			
		||||
          <MenuItem
 | 
			
		||||
            label={t("buttons.exportImage")}
 | 
			
		||||
            icon={ExportImageIcon}
 | 
			
		||||
            dataTestId="image-export-button"
 | 
			
		||||
            onClick={() => setAppState({ openDialog: "imageExport" })}
 | 
			
		||||
          />
 | 
			
		||||
        )}
 | 
			
		||||
        {onCollabButtonClick && (
 | 
			
		||||
          <CollabButton
 | 
			
		||||
            isCollaborating={isCollaborating}
 | 
			
		||||
@@ -207,8 +213,16 @@ export const MobileMenu = ({
 | 
			
		||||
        )}
 | 
			
		||||
        {actionManager.renderAction("toggleShortcuts", undefined, true)}
 | 
			
		||||
        {!appState.viewModeEnabled && actionManager.renderAction("clearCanvas")}
 | 
			
		||||
        <Separator />
 | 
			
		||||
        <MenuLinks />
 | 
			
		||||
        {typeof renderMenuLinks === "undefined" ? ( //zsviczian
 | 
			
		||||
          <Separator />
 | 
			
		||||
        ) : (
 | 
			
		||||
          renderMenuLinks && <Separator />
 | 
			
		||||
        )}
 | 
			
		||||
        {typeof renderMenuLinks === "undefined" ? ( //zsviczian
 | 
			
		||||
          <MenuLinks />
 | 
			
		||||
        ) : (
 | 
			
		||||
          renderMenuLinks && renderMenuLinks(device.isMobile, appState)
 | 
			
		||||
        )}
 | 
			
		||||
        <Separator />
 | 
			
		||||
        {!appState.viewModeEnabled && (
 | 
			
		||||
          <div style={{ marginBottom: ".5rem" }}>
 | 
			
		||||
 
 | 
			
		||||
@@ -90,10 +90,10 @@ describe("Sidebar", () => {
 | 
			
		||||
 | 
			
		||||
    const sidebar = container.querySelector<HTMLElement>(".test-sidebar");
 | 
			
		||||
    expect(sidebar).not.toBe(null);
 | 
			
		||||
    const closeButton = queryByTestId(sidebar!, "sidebar-close");
 | 
			
		||||
    const closeButton = queryByTestId(sidebar!, "sidebar-close")!;
 | 
			
		||||
    expect(closeButton).not.toBe(null);
 | 
			
		||||
 | 
			
		||||
    fireEvent.click(closeButton!.querySelector("button")!);
 | 
			
		||||
    fireEvent.click(closeButton);
 | 
			
		||||
    await waitFor(() => {
 | 
			
		||||
      expect(container.querySelector<HTMLElement>(".test-sidebar")).toBe(null);
 | 
			
		||||
      expect(onClose).toHaveBeenCalled();
 | 
			
		||||
 
 | 
			
		||||
@@ -492,7 +492,7 @@ export const getElementBounds = (
 | 
			
		||||
 | 
			
		||||
export const getCommonBounds = (
 | 
			
		||||
  elements: readonly ExcalidrawElement[],
 | 
			
		||||
): [minX: number, minY: number, maxX: number, maxY: number] => {
 | 
			
		||||
): [number, number, number, number] => {
 | 
			
		||||
  if (!elements.length) {
 | 
			
		||||
    return [0, 0, 0, 0];
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -392,6 +392,8 @@ No, Excalidraw package doesn't come with collaboration built in, since the imple
 | 
			
		||||
| [`onPointerUpdate`](#onPointerUpdate) | Function |  | Callback triggered when mouse pointer is updated. |
 | 
			
		||||
| [`langCode`](#langCode) | string | `en` | Language code string |
 | 
			
		||||
| [`renderTopRightUI`](#renderTopRightUI) | Function |  | Function that renders custom UI in top right corner |
 | 
			
		||||
| [`hideWelcomeScreen`](#hideWelcomeScreen) | boolean |  | This implies if the app should always hide the welcome sreen |
 | 
			
		||||
| [`renderMenuLinks`](#renderMenuLinks) | Function |  | Function that renders custom list of links (or other custom UI) in the app menu |
 | 
			
		||||
| [`renderFooter `](#renderFooter) | Function |  | Function that renders custom UI footer |
 | 
			
		||||
| [`renderCustomStats`](#renderCustomStats) | Function |  | Function that can be used to render custom stats on the stats dialog. |
 | 
			
		||||
| [`renderSIdebar`](#renderSIdebar) | Function |  | Render function that renders custom sidebar. |
 | 
			
		||||
@@ -613,6 +615,22 @@ import { defaultLang, languages } from "@excalidraw/excalidraw";
 | 
			
		||||
 | 
			
		||||
A function returning JSX to render custom UI in the top right corner of the app.
 | 
			
		||||
 | 
			
		||||
#### `hideWelcomeScreen`
 | 
			
		||||
 | 
			
		||||
<pre>
 | 
			
		||||
boolean
 | 
			
		||||
</pre>
 | 
			
		||||
 | 
			
		||||
Boolean value to override the displaying of the welcome screen elements. If set to true, the welcome screen will never be shown.
 | 
			
		||||
 | 
			
		||||
#### `renderMenuLinks`
 | 
			
		||||
 | 
			
		||||
<pre>
 | 
			
		||||
((isMobile: boolean, appState: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L79">AppState</a>) => JSX | null)|null
 | 
			
		||||
</pre>
 | 
			
		||||
 | 
			
		||||
A function returning JSX to render custom UI (intended to be a list of custom links) replacing the default list of links in the app menu. If set to null, the list of links will not be displayed. If unset, the default list of links will be displayed.
 | 
			
		||||
 | 
			
		||||
#### `renderFooter`
 | 
			
		||||
 | 
			
		||||
<pre>
 | 
			
		||||
@@ -685,7 +703,7 @@ This prop sets the name of the drawing which will be used when exporting the dra
 | 
			
		||||
 | 
			
		||||
#### `UIOptions`
 | 
			
		||||
 | 
			
		||||
This prop can be used to customise UI of Excalidraw. Currently we support customising [`canvasActions`](#canvasActions) and [`dockedSidebarBreakpoint`](dockedSidebarBreakpoint). It accepts the below parameters
 | 
			
		||||
This prop can be used to customise UI of Excalidraw. Currently we support customising [`canvasActions`](#canvasActions), [`dockedSidebarBreakpoint`](dockedSidebarBreakpoint), and ['showLanguageList`](showLanguageList). It accepts the below parameters
 | 
			
		||||
 | 
			
		||||
<pre>
 | 
			
		||||
{ canvasActions: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L208"> CanvasActions<a/> }
 | 
			
		||||
@@ -709,6 +727,10 @@ This prop indicates at what point should we break to a docked, permanent sidebar
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
### `showLanguageList`
 | 
			
		||||
 | 
			
		||||
Boolean prop. If set to `true` the `language selector dropdown list` will be hidden in the app menu. If `false` or `undefined` the language dropdown will be rendered.
 | 
			
		||||
 | 
			
		||||
#### `exportOpts`
 | 
			
		||||
 | 
			
		||||
The below attributes can be set in `UIOptions.canvasActions.export` to customize the export dialog. If `UIOptions.canvasActions.export` is `false` the export button will not be rendered.
 | 
			
		||||
 
 | 
			
		||||
@@ -20,6 +20,8 @@ const ExcalidrawBase = (props: ExcalidrawProps) => {
 | 
			
		||||
    isCollaborating = false,
 | 
			
		||||
    onPointerUpdate,
 | 
			
		||||
    renderTopRightUI,
 | 
			
		||||
    hideWelcomeScreen,
 | 
			
		||||
    renderMenuLinks,
 | 
			
		||||
    renderFooter,
 | 
			
		||||
    renderSidebar,
 | 
			
		||||
    langCode = defaultLang.code,
 | 
			
		||||
@@ -93,6 +95,8 @@ const ExcalidrawBase = (props: ExcalidrawProps) => {
 | 
			
		||||
          isCollaborating={isCollaborating}
 | 
			
		||||
          onPointerUpdate={onPointerUpdate}
 | 
			
		||||
          renderTopRightUI={renderTopRightUI}
 | 
			
		||||
          hideWelcomeScreen={hideWelcomeScreen}
 | 
			
		||||
          renderMenuLinks={renderMenuLinks}
 | 
			
		||||
          renderFooter={renderFooter}
 | 
			
		||||
          langCode={langCode}
 | 
			
		||||
          viewModeEnabled={viewModeEnabled}
 | 
			
		||||
 
 | 
			
		||||
@@ -14,102 +14,6 @@ import {
 | 
			
		||||
 | 
			
		||||
export const SVG_EXPORT_TAG = `<!-- svg-source:excalidraw -->`;
 | 
			
		||||
 | 
			
		||||
const getExactBoundingBox = async (
 | 
			
		||||
  elements: readonly NonDeletedExcalidrawElement[],
 | 
			
		||||
  appState: {
 | 
			
		||||
    exportBackground: boolean;
 | 
			
		||||
    exportPadding?: number;
 | 
			
		||||
    exportScale?: number;
 | 
			
		||||
    viewBackgroundColor: string;
 | 
			
		||||
    exportWithDarkMode?: boolean;
 | 
			
		||||
    exportEmbedScene?: boolean;
 | 
			
		||||
  },
 | 
			
		||||
  files: BinaryFiles,
 | 
			
		||||
): Promise<
 | 
			
		||||
  [offsetLeft: number, offsetTop: number, width: number, height: number]
 | 
			
		||||
> => {
 | 
			
		||||
  const padding = DEFAULT_EXPORT_PADDING;
 | 
			
		||||
  // const padding = 0;
 | 
			
		||||
  const [minX, minY, width, height] = getApproximateCanvasSize(
 | 
			
		||||
    elements,
 | 
			
		||||
    padding,
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  const canvas = document.createElement("canvas");
 | 
			
		||||
  canvas.width = width;
 | 
			
		||||
  canvas.height = height;
 | 
			
		||||
 | 
			
		||||
  const { imageCache } = await updateImageCache({
 | 
			
		||||
    imageCache: new Map(),
 | 
			
		||||
    fileIds: getInitializedImageElements(elements).map(
 | 
			
		||||
      (element) => element.fileId,
 | 
			
		||||
    ),
 | 
			
		||||
    files,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  const defaultAppState = getDefaultAppState();
 | 
			
		||||
 | 
			
		||||
  renderScene({
 | 
			
		||||
    elements,
 | 
			
		||||
    // @ts-ignore
 | 
			
		||||
    appState,
 | 
			
		||||
    scale: 1,
 | 
			
		||||
    rc: rough.canvas(canvas),
 | 
			
		||||
    canvas,
 | 
			
		||||
    renderConfig: {
 | 
			
		||||
      viewBackgroundColor: null,
 | 
			
		||||
      scrollX: -minX + padding,
 | 
			
		||||
      scrollY: -minY + padding,
 | 
			
		||||
      zoom: defaultAppState.zoom,
 | 
			
		||||
      remotePointerViewportCoords: {},
 | 
			
		||||
      remoteSelectedElementIds: {},
 | 
			
		||||
      shouldCacheIgnoreZoom: false,
 | 
			
		||||
      remotePointerUsernames: {},
 | 
			
		||||
      remotePointerUserStates: {},
 | 
			
		||||
      theme: "light",
 | 
			
		||||
      imageCache,
 | 
			
		||||
      renderScrollbars: false,
 | 
			
		||||
      renderSelection: false,
 | 
			
		||||
      renderGrid: false,
 | 
			
		||||
      isExporting: true,
 | 
			
		||||
    },
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  const ctx = canvas.getContext("2d")!;
 | 
			
		||||
  const { data } = ctx.getImageData(0, 0, width, height);
 | 
			
		||||
 | 
			
		||||
  let _minX = Infinity;
 | 
			
		||||
  let _minY = Infinity;
 | 
			
		||||
  let _maxX = -Infinity;
 | 
			
		||||
  let _maxY = -Infinity;
 | 
			
		||||
 | 
			
		||||
  const rows = [];
 | 
			
		||||
  let row: number[][] = [];
 | 
			
		||||
  for (let i = 0; i < data.length - 1; i = i + 4) {
 | 
			
		||||
    if (i && i % (width * 4) === 0) {
 | 
			
		||||
      rows.push(row);
 | 
			
		||||
      row = [];
 | 
			
		||||
    }
 | 
			
		||||
    const pixel = [data[i], data[i + 1], data[i + 2], data[i + 3]];
 | 
			
		||||
    row.push(pixel);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  for (const [y, row] of rows.entries()) {
 | 
			
		||||
    for (const [x, pixel] of row.entries()) {
 | 
			
		||||
      if (pixel[3] > 0) {
 | 
			
		||||
        _minX = Math.min(_minX, x);
 | 
			
		||||
        _minY = Math.min(_minY, y);
 | 
			
		||||
        _maxX = Math.max(_maxX, x);
 | 
			
		||||
        _maxY = Math.max(_maxY, y);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const offsetLeft = padding - _minX;
 | 
			
		||||
  const offsetTop = padding - _minY;
 | 
			
		||||
 | 
			
		||||
  return [offsetLeft, offsetTop, _maxX - _minX, _maxY - _minY];
 | 
			
		||||
};
 | 
			
		||||
export const exportToCanvas = async (
 | 
			
		||||
  elements: readonly NonDeletedExcalidrawElement[],
 | 
			
		||||
  appState: AppState,
 | 
			
		||||
@@ -133,12 +37,7 @@ export const exportToCanvas = async (
 | 
			
		||||
    return { canvas, scale: appState.exportScale };
 | 
			
		||||
  },
 | 
			
		||||
) => {
 | 
			
		||||
  const [scrollX, scrollY, width, height] = await getCanvasSize(
 | 
			
		||||
    elements,
 | 
			
		||||
    appState,
 | 
			
		||||
    files,
 | 
			
		||||
    exportPadding,
 | 
			
		||||
  );
 | 
			
		||||
  const [minX, minY, width, height] = getCanvasSize(elements, exportPadding);
 | 
			
		||||
 | 
			
		||||
  const { canvas, scale = 1 } = createCanvas(width, height);
 | 
			
		||||
 | 
			
		||||
@@ -160,8 +59,8 @@ export const exportToCanvas = async (
 | 
			
		||||
    canvas,
 | 
			
		||||
    renderConfig: {
 | 
			
		||||
      viewBackgroundColor: exportBackground ? viewBackgroundColor : null,
 | 
			
		||||
      scrollX,
 | 
			
		||||
      scrollY,
 | 
			
		||||
      scrollX: -minX + exportPadding,
 | 
			
		||||
      scrollY: -minY + exportPadding,
 | 
			
		||||
      zoom: defaultAppState.zoom,
 | 
			
		||||
      remotePointerViewportCoords: {},
 | 
			
		||||
      remoteSelectedElementIds: {},
 | 
			
		||||
@@ -210,12 +109,7 @@ export const exportToSvg = async (
 | 
			
		||||
      console.error(error);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  const [minX, minY, width, height] = await getCanvasSize(
 | 
			
		||||
    elements,
 | 
			
		||||
    appState,
 | 
			
		||||
    files || {},
 | 
			
		||||
    exportPadding,
 | 
			
		||||
  );
 | 
			
		||||
  const [minX, minY, width, height] = getCanvasSize(elements, exportPadding);
 | 
			
		||||
 | 
			
		||||
  // initialize SVG root
 | 
			
		||||
  const svgRoot = document.createElementNS(SVG_NS, "svg");
 | 
			
		||||
@@ -278,66 +172,26 @@ export const exportToSvg = async (
 | 
			
		||||
  return svgRoot;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const getApproximateCanvasSize = (
 | 
			
		||||
// calculate smallest area to fit the contents in
 | 
			
		||||
const getCanvasSize = (
 | 
			
		||||
  elements: readonly NonDeletedExcalidrawElement[],
 | 
			
		||||
  exportPadding: number,
 | 
			
		||||
): [number, number, number, number] => {
 | 
			
		||||
  const bounds = getCommonBounds(elements);
 | 
			
		||||
 | 
			
		||||
  const minX = Math.floor(bounds[0]);
 | 
			
		||||
  const minY = Math.floor(bounds[1]);
 | 
			
		||||
  const maxX = Math.ceil(bounds[2]);
 | 
			
		||||
  const maxY = Math.ceil(bounds[3]);
 | 
			
		||||
 | 
			
		||||
  const [minX, minY, maxX, maxY] = getCommonBounds(elements);
 | 
			
		||||
  const width = distance(minX, maxX) + exportPadding * 2;
 | 
			
		||||
  const height =
 | 
			
		||||
    Math.ceil(distance(minY, maxY)) + exportPadding + exportPadding;
 | 
			
		||||
  const height = distance(minY, maxY) + exportPadding + exportPadding;
 | 
			
		||||
 | 
			
		||||
  return [minX, minY, width, height];
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// calculate smallest area to fit the contents in
 | 
			
		||||
const getCanvasSize = async (
 | 
			
		||||
  elements: readonly NonDeletedExcalidrawElement[],
 | 
			
		||||
  appState: {
 | 
			
		||||
    exportBackground: boolean;
 | 
			
		||||
    exportPadding?: number;
 | 
			
		||||
    exportScale?: number;
 | 
			
		||||
    viewBackgroundColor: string;
 | 
			
		||||
    exportWithDarkMode?: boolean;
 | 
			
		||||
    exportEmbedScene?: boolean;
 | 
			
		||||
  },
 | 
			
		||||
  files: BinaryFiles,
 | 
			
		||||
  exportPadding: number,
 | 
			
		||||
): Promise<[number, number, number, number]> => {
 | 
			
		||||
  if (exportPadding) {
 | 
			
		||||
    const [minX, minY, width, height] = getApproximateCanvasSize(
 | 
			
		||||
      elements,
 | 
			
		||||
      exportPadding,
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    return [-minX + exportPadding, -minY + exportPadding, width, height];
 | 
			
		||||
  } else {
 | 
			
		||||
    const [minX, minY] = getApproximateCanvasSize(elements, exportPadding);
 | 
			
		||||
 | 
			
		||||
    const [offsetLeft, offsetRight, width, height] = await getExactBoundingBox(
 | 
			
		||||
      elements,
 | 
			
		||||
      appState,
 | 
			
		||||
      files,
 | 
			
		||||
    );
 | 
			
		||||
    return [-minX + offsetLeft, -minY + offsetRight, width, height];
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const getExportSize = (
 | 
			
		||||
  elements: readonly NonDeletedExcalidrawElement[],
 | 
			
		||||
  exportPadding: number,
 | 
			
		||||
  scale: number,
 | 
			
		||||
): [number, number] => {
 | 
			
		||||
  const [, , width, height] = getApproximateCanvasSize(
 | 
			
		||||
    elements,
 | 
			
		||||
    exportPadding,
 | 
			
		||||
  ).map((dimension) => Math.trunc(dimension * scale));
 | 
			
		||||
  const [, , width, height] = getCanvasSize(elements, exportPadding).map(
 | 
			
		||||
    (dimension) => Math.trunc(dimension * scale),
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  return [width, height];
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -3,6 +3,8 @@ import "jest-canvas-mock";
 | 
			
		||||
import dotenv from "dotenv";
 | 
			
		||||
import polyfill from "./polyfill";
 | 
			
		||||
 | 
			
		||||
require("fake-indexeddb/auto");
 | 
			
		||||
 | 
			
		||||
polyfill();
 | 
			
		||||
// jest doesn't know of .env.development so we need to init it ourselves
 | 
			
		||||
dotenv.config({
 | 
			
		||||
 
 | 
			
		||||
@@ -1951,7 +1951,9 @@ Object {
 | 
			
		||||
  "penDetected": false,
 | 
			
		||||
  "penMode": false,
 | 
			
		||||
  "pendingImageElementId": null,
 | 
			
		||||
  "previousSelectedElementIds": Object {},
 | 
			
		||||
  "previousSelectedElementIds": Object {
 | 
			
		||||
    "id0": true,
 | 
			
		||||
  },
 | 
			
		||||
  "resizingElement": null,
 | 
			
		||||
  "scrollX": 0,
 | 
			
		||||
  "scrollY": 0,
 | 
			
		||||
@@ -1988,7 +1990,7 @@ Object {
 | 
			
		||||
  "boundElements": null,
 | 
			
		||||
  "fillStyle": "hachure",
 | 
			
		||||
  "groupIds": Array [],
 | 
			
		||||
  "height": 15,
 | 
			
		||||
  "height": 10,
 | 
			
		||||
  "id": "id0",
 | 
			
		||||
  "isDeleted": false,
 | 
			
		||||
  "link": null,
 | 
			
		||||
@@ -2004,9 +2006,9 @@ Object {
 | 
			
		||||
  "updated": 1,
 | 
			
		||||
  "version": 3,
 | 
			
		||||
  "versionNonce": 453191,
 | 
			
		||||
  "width": 15,
 | 
			
		||||
  "x": 10,
 | 
			
		||||
  "y": 10,
 | 
			
		||||
  "width": 10,
 | 
			
		||||
  "x": 25,
 | 
			
		||||
  "y": 25,
 | 
			
		||||
}
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
@@ -2085,7 +2087,7 @@ Object {
 | 
			
		||||
          "boundElements": null,
 | 
			
		||||
          "fillStyle": "hachure",
 | 
			
		||||
          "groupIds": Array [],
 | 
			
		||||
          "height": 15,
 | 
			
		||||
          "height": 10,
 | 
			
		||||
          "id": "id0",
 | 
			
		||||
          "isDeleted": false,
 | 
			
		||||
          "link": null,
 | 
			
		||||
@@ -2101,9 +2103,9 @@ Object {
 | 
			
		||||
          "updated": 1,
 | 
			
		||||
          "version": 3,
 | 
			
		||||
          "versionNonce": 453191,
 | 
			
		||||
          "width": 15,
 | 
			
		||||
          "x": 10,
 | 
			
		||||
          "y": 10,
 | 
			
		||||
          "width": 10,
 | 
			
		||||
          "x": 25,
 | 
			
		||||
          "y": 25,
 | 
			
		||||
        },
 | 
			
		||||
      ],
 | 
			
		||||
    },
 | 
			
		||||
@@ -2682,7 +2684,9 @@ Object {
 | 
			
		||||
  "penDetected": false,
 | 
			
		||||
  "penMode": false,
 | 
			
		||||
  "pendingImageElementId": null,
 | 
			
		||||
  "previousSelectedElementIds": Object {},
 | 
			
		||||
  "previousSelectedElementIds": Object {
 | 
			
		||||
    "id0": true,
 | 
			
		||||
  },
 | 
			
		||||
  "resizingElement": null,
 | 
			
		||||
  "scrollX": 0,
 | 
			
		||||
  "scrollY": 0,
 | 
			
		||||
@@ -2713,6 +2717,35 @@ Object {
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
exports[`regression tests alt-drag duplicates an element: [end of test] element 0 1`] = `
 | 
			
		||||
Object {
 | 
			
		||||
  "angle": 0,
 | 
			
		||||
  "backgroundColor": "transparent",
 | 
			
		||||
  "boundElements": null,
 | 
			
		||||
  "fillStyle": "hachure",
 | 
			
		||||
  "groupIds": Array [],
 | 
			
		||||
  "height": 10,
 | 
			
		||||
  "id": "id0_copy",
 | 
			
		||||
  "isDeleted": false,
 | 
			
		||||
  "link": null,
 | 
			
		||||
  "locked": false,
 | 
			
		||||
  "opacity": 100,
 | 
			
		||||
  "roughness": 1,
 | 
			
		||||
  "seed": 401146281,
 | 
			
		||||
  "strokeColor": "#000000",
 | 
			
		||||
  "strokeSharpness": "sharp",
 | 
			
		||||
  "strokeStyle": "solid",
 | 
			
		||||
  "strokeWidth": 1,
 | 
			
		||||
  "type": "rectangle",
 | 
			
		||||
  "updated": 1,
 | 
			
		||||
  "version": 4,
 | 
			
		||||
  "versionNonce": 2019559783,
 | 
			
		||||
  "width": 10,
 | 
			
		||||
  "x": 10,
 | 
			
		||||
  "y": 10,
 | 
			
		||||
}
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
exports[`regression tests alt-drag duplicates an element: [end of test] element 1 1`] = `
 | 
			
		||||
Object {
 | 
			
		||||
  "angle": 0,
 | 
			
		||||
  "backgroundColor": "transparent",
 | 
			
		||||
@@ -2733,11 +2766,11 @@ Object {
 | 
			
		||||
  "strokeWidth": 1,
 | 
			
		||||
  "type": "rectangle",
 | 
			
		||||
  "updated": 1,
 | 
			
		||||
  "version": 2,
 | 
			
		||||
  "versionNonce": 1278240551,
 | 
			
		||||
  "version": 3,
 | 
			
		||||
  "versionNonce": 453191,
 | 
			
		||||
  "width": 10,
 | 
			
		||||
  "x": 10,
 | 
			
		||||
  "y": 10,
 | 
			
		||||
  "x": 20,
 | 
			
		||||
  "y": 20,
 | 
			
		||||
}
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
@@ -2797,11 +2830,78 @@ Object {
 | 
			
		||||
        },
 | 
			
		||||
      ],
 | 
			
		||||
    },
 | 
			
		||||
    Object {
 | 
			
		||||
      "appState": Object {
 | 
			
		||||
        "editingGroupId": null,
 | 
			
		||||
        "editingLinearElement": null,
 | 
			
		||||
        "name": "Untitled-201933152653",
 | 
			
		||||
        "selectedElementIds": Object {
 | 
			
		||||
          "id0": true,
 | 
			
		||||
          "id1": true,
 | 
			
		||||
        },
 | 
			
		||||
        "selectedGroupIds": Object {},
 | 
			
		||||
        "viewBackgroundColor": "#ffffff",
 | 
			
		||||
      },
 | 
			
		||||
      "elements": Array [
 | 
			
		||||
        Object {
 | 
			
		||||
          "angle": 0,
 | 
			
		||||
          "backgroundColor": "transparent",
 | 
			
		||||
          "boundElements": null,
 | 
			
		||||
          "fillStyle": "hachure",
 | 
			
		||||
          "groupIds": Array [],
 | 
			
		||||
          "height": 10,
 | 
			
		||||
          "id": "id0_copy",
 | 
			
		||||
          "isDeleted": false,
 | 
			
		||||
          "link": null,
 | 
			
		||||
          "locked": false,
 | 
			
		||||
          "opacity": 100,
 | 
			
		||||
          "roughness": 1,
 | 
			
		||||
          "seed": 401146281,
 | 
			
		||||
          "strokeColor": "#000000",
 | 
			
		||||
          "strokeSharpness": "sharp",
 | 
			
		||||
          "strokeStyle": "solid",
 | 
			
		||||
          "strokeWidth": 1,
 | 
			
		||||
          "type": "rectangle",
 | 
			
		||||
          "updated": 1,
 | 
			
		||||
          "version": 4,
 | 
			
		||||
          "versionNonce": 2019559783,
 | 
			
		||||
          "width": 10,
 | 
			
		||||
          "x": 10,
 | 
			
		||||
          "y": 10,
 | 
			
		||||
        },
 | 
			
		||||
        Object {
 | 
			
		||||
          "angle": 0,
 | 
			
		||||
          "backgroundColor": "transparent",
 | 
			
		||||
          "boundElements": null,
 | 
			
		||||
          "fillStyle": "hachure",
 | 
			
		||||
          "groupIds": Array [],
 | 
			
		||||
          "height": 10,
 | 
			
		||||
          "id": "id0",
 | 
			
		||||
          "isDeleted": false,
 | 
			
		||||
          "link": null,
 | 
			
		||||
          "locked": false,
 | 
			
		||||
          "opacity": 100,
 | 
			
		||||
          "roughness": 1,
 | 
			
		||||
          "seed": 337897,
 | 
			
		||||
          "strokeColor": "#000000",
 | 
			
		||||
          "strokeSharpness": "sharp",
 | 
			
		||||
          "strokeStyle": "solid",
 | 
			
		||||
          "strokeWidth": 1,
 | 
			
		||||
          "type": "rectangle",
 | 
			
		||||
          "updated": 1,
 | 
			
		||||
          "version": 3,
 | 
			
		||||
          "versionNonce": 453191,
 | 
			
		||||
          "width": 10,
 | 
			
		||||
          "x": 20,
 | 
			
		||||
          "y": 20,
 | 
			
		||||
        },
 | 
			
		||||
      ],
 | 
			
		||||
    },
 | 
			
		||||
  ],
 | 
			
		||||
}
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
exports[`regression tests alt-drag duplicates an element: [end of test] number of elements 1`] = `1`;
 | 
			
		||||
exports[`regression tests alt-drag duplicates an element: [end of test] number of elements 1`] = `2`;
 | 
			
		||||
 | 
			
		||||
exports[`regression tests alt-drag duplicates an element: [end of test] number of renders 1`] = `12`;
 | 
			
		||||
 | 
			
		||||
@@ -3737,6 +3837,230 @@ exports[`regression tests change the properties of a shape: [end of test] number
 | 
			
		||||
 | 
			
		||||
exports[`regression tests change the properties of a shape: [end of test] number of renders 1`] = `15`;
 | 
			
		||||
 | 
			
		||||
exports[`regression tests click on an element and drag it: [dragged] appState 1`] = `
 | 
			
		||||
Object {
 | 
			
		||||
  "activeTool": Object {
 | 
			
		||||
    "customType": null,
 | 
			
		||||
    "lastActiveToolBeforeEraser": null,
 | 
			
		||||
    "locked": false,
 | 
			
		||||
    "type": "selection",
 | 
			
		||||
  },
 | 
			
		||||
  "collaborators": Map {},
 | 
			
		||||
  "currentChartType": "bar",
 | 
			
		||||
  "currentItemBackgroundColor": "transparent",
 | 
			
		||||
  "currentItemEndArrowhead": "arrow",
 | 
			
		||||
  "currentItemFillStyle": "hachure",
 | 
			
		||||
  "currentItemFontFamily": 1,
 | 
			
		||||
  "currentItemFontSize": 20,
 | 
			
		||||
  "currentItemLinearStrokeSharpness": "round",
 | 
			
		||||
  "currentItemOpacity": 100,
 | 
			
		||||
  "currentItemRoughness": 1,
 | 
			
		||||
  "currentItemStartArrowhead": null,
 | 
			
		||||
  "currentItemStrokeColor": "#000000",
 | 
			
		||||
  "currentItemStrokeSharpness": "sharp",
 | 
			
		||||
  "currentItemStrokeStyle": "solid",
 | 
			
		||||
  "currentItemStrokeWidth": 1,
 | 
			
		||||
  "currentItemTextAlign": "left",
 | 
			
		||||
  "cursorButton": "up",
 | 
			
		||||
  "draggingElement": null,
 | 
			
		||||
  "editingElement": null,
 | 
			
		||||
  "editingGroupId": null,
 | 
			
		||||
  "editingLinearElement": null,
 | 
			
		||||
  "errorMessage": null,
 | 
			
		||||
  "exportBackground": true,
 | 
			
		||||
  "exportEmbedScene": false,
 | 
			
		||||
  "exportScale": 1,
 | 
			
		||||
  "exportWithDarkMode": false,
 | 
			
		||||
  "fileHandle": null,
 | 
			
		||||
  "gridSize": null,
 | 
			
		||||
  "height": 768,
 | 
			
		||||
  "isBindingEnabled": true,
 | 
			
		||||
  "isLoading": false,
 | 
			
		||||
  "isResizing": false,
 | 
			
		||||
  "isRotating": false,
 | 
			
		||||
  "isSidebarDocked": false,
 | 
			
		||||
  "lastPointerDownWith": "mouse",
 | 
			
		||||
  "multiElement": null,
 | 
			
		||||
  "name": "Untitled-201933152653",
 | 
			
		||||
  "offsetLeft": 0,
 | 
			
		||||
  "offsetTop": 0,
 | 
			
		||||
  "openDialog": null,
 | 
			
		||||
  "openMenu": null,
 | 
			
		||||
  "openPopup": null,
 | 
			
		||||
  "openSidebar": null,
 | 
			
		||||
  "pasteDialog": Object {
 | 
			
		||||
    "data": null,
 | 
			
		||||
    "shown": false,
 | 
			
		||||
  },
 | 
			
		||||
  "penDetected": false,
 | 
			
		||||
  "penMode": false,
 | 
			
		||||
  "pendingImageElementId": null,
 | 
			
		||||
  "previousSelectedElementIds": Object {
 | 
			
		||||
    "id0": true,
 | 
			
		||||
  },
 | 
			
		||||
  "resizingElement": null,
 | 
			
		||||
  "scrollX": 0,
 | 
			
		||||
  "scrollY": 0,
 | 
			
		||||
  "scrolledOutside": false,
 | 
			
		||||
  "selectedElementIds": Object {
 | 
			
		||||
    "id0": true,
 | 
			
		||||
    "id1": true,
 | 
			
		||||
  },
 | 
			
		||||
  "selectedGroupIds": Object {},
 | 
			
		||||
  "selectedLinearElement": null,
 | 
			
		||||
  "selectionElement": null,
 | 
			
		||||
  "shouldCacheIgnoreZoom": false,
 | 
			
		||||
  "showHyperlinkPopup": false,
 | 
			
		||||
  "showStats": false,
 | 
			
		||||
  "showWelcomeScreen": true,
 | 
			
		||||
  "startBoundElement": null,
 | 
			
		||||
  "suggestedBindings": Array [],
 | 
			
		||||
  "theme": "light",
 | 
			
		||||
  "toast": null,
 | 
			
		||||
  "viewBackgroundColor": "#ffffff",
 | 
			
		||||
  "viewModeEnabled": false,
 | 
			
		||||
  "width": 1024,
 | 
			
		||||
  "zenModeEnabled": false,
 | 
			
		||||
  "zoom": Object {
 | 
			
		||||
    "value": 1,
 | 
			
		||||
  },
 | 
			
		||||
}
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
exports[`regression tests click on an element and drag it: [dragged] element 0 1`] = `
 | 
			
		||||
Object {
 | 
			
		||||
  "angle": 0,
 | 
			
		||||
  "backgroundColor": "transparent",
 | 
			
		||||
  "boundElements": null,
 | 
			
		||||
  "fillStyle": "hachure",
 | 
			
		||||
  "groupIds": Array [],
 | 
			
		||||
  "height": 10,
 | 
			
		||||
  "id": "id0",
 | 
			
		||||
  "isDeleted": false,
 | 
			
		||||
  "link": null,
 | 
			
		||||
  "locked": false,
 | 
			
		||||
  "opacity": 100,
 | 
			
		||||
  "roughness": 1,
 | 
			
		||||
  "seed": 337897,
 | 
			
		||||
  "strokeColor": "#000000",
 | 
			
		||||
  "strokeSharpness": "sharp",
 | 
			
		||||
  "strokeStyle": "solid",
 | 
			
		||||
  "strokeWidth": 1,
 | 
			
		||||
  "type": "rectangle",
 | 
			
		||||
  "updated": 1,
 | 
			
		||||
  "version": 3,
 | 
			
		||||
  "versionNonce": 453191,
 | 
			
		||||
  "width": 10,
 | 
			
		||||
  "x": 20,
 | 
			
		||||
  "y": 20,
 | 
			
		||||
}
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
exports[`regression tests click on an element and drag it: [dragged] history 1`] = `
 | 
			
		||||
Object {
 | 
			
		||||
  "recording": false,
 | 
			
		||||
  "redoStack": Array [],
 | 
			
		||||
  "stateHistory": Array [
 | 
			
		||||
    Object {
 | 
			
		||||
      "appState": Object {
 | 
			
		||||
        "editingGroupId": null,
 | 
			
		||||
        "editingLinearElement": null,
 | 
			
		||||
        "name": "Untitled-201933152653",
 | 
			
		||||
        "selectedElementIds": Object {},
 | 
			
		||||
        "selectedGroupIds": Object {},
 | 
			
		||||
        "viewBackgroundColor": "#ffffff",
 | 
			
		||||
      },
 | 
			
		||||
      "elements": Array [],
 | 
			
		||||
    },
 | 
			
		||||
    Object {
 | 
			
		||||
      "appState": Object {
 | 
			
		||||
        "editingGroupId": null,
 | 
			
		||||
        "editingLinearElement": null,
 | 
			
		||||
        "name": "Untitled-201933152653",
 | 
			
		||||
        "selectedElementIds": Object {
 | 
			
		||||
          "id0": true,
 | 
			
		||||
        },
 | 
			
		||||
        "selectedGroupIds": Object {},
 | 
			
		||||
        "viewBackgroundColor": "#ffffff",
 | 
			
		||||
      },
 | 
			
		||||
      "elements": Array [
 | 
			
		||||
        Object {
 | 
			
		||||
          "angle": 0,
 | 
			
		||||
          "backgroundColor": "transparent",
 | 
			
		||||
          "boundElements": null,
 | 
			
		||||
          "fillStyle": "hachure",
 | 
			
		||||
          "groupIds": Array [],
 | 
			
		||||
          "height": 10,
 | 
			
		||||
          "id": "id0",
 | 
			
		||||
          "isDeleted": false,
 | 
			
		||||
          "link": null,
 | 
			
		||||
          "locked": false,
 | 
			
		||||
          "opacity": 100,
 | 
			
		||||
          "roughness": 1,
 | 
			
		||||
          "seed": 337897,
 | 
			
		||||
          "strokeColor": "#000000",
 | 
			
		||||
          "strokeSharpness": "sharp",
 | 
			
		||||
          "strokeStyle": "solid",
 | 
			
		||||
          "strokeWidth": 1,
 | 
			
		||||
          "type": "rectangle",
 | 
			
		||||
          "updated": 1,
 | 
			
		||||
          "version": 2,
 | 
			
		||||
          "versionNonce": 1278240551,
 | 
			
		||||
          "width": 10,
 | 
			
		||||
          "x": 10,
 | 
			
		||||
          "y": 10,
 | 
			
		||||
        },
 | 
			
		||||
      ],
 | 
			
		||||
    },
 | 
			
		||||
    Object {
 | 
			
		||||
      "appState": Object {
 | 
			
		||||
        "editingGroupId": null,
 | 
			
		||||
        "editingLinearElement": null,
 | 
			
		||||
        "name": "Untitled-201933152653",
 | 
			
		||||
        "selectedElementIds": Object {
 | 
			
		||||
          "id0": true,
 | 
			
		||||
          "id1": true,
 | 
			
		||||
        },
 | 
			
		||||
        "selectedGroupIds": Object {},
 | 
			
		||||
        "viewBackgroundColor": "#ffffff",
 | 
			
		||||
      },
 | 
			
		||||
      "elements": Array [
 | 
			
		||||
        Object {
 | 
			
		||||
          "angle": 0,
 | 
			
		||||
          "backgroundColor": "transparent",
 | 
			
		||||
          "boundElements": null,
 | 
			
		||||
          "fillStyle": "hachure",
 | 
			
		||||
          "groupIds": Array [],
 | 
			
		||||
          "height": 10,
 | 
			
		||||
          "id": "id0",
 | 
			
		||||
          "isDeleted": false,
 | 
			
		||||
          "link": null,
 | 
			
		||||
          "locked": false,
 | 
			
		||||
          "opacity": 100,
 | 
			
		||||
          "roughness": 1,
 | 
			
		||||
          "seed": 337897,
 | 
			
		||||
          "strokeColor": "#000000",
 | 
			
		||||
          "strokeSharpness": "sharp",
 | 
			
		||||
          "strokeStyle": "solid",
 | 
			
		||||
          "strokeWidth": 1,
 | 
			
		||||
          "type": "rectangle",
 | 
			
		||||
          "updated": 1,
 | 
			
		||||
          "version": 3,
 | 
			
		||||
          "versionNonce": 453191,
 | 
			
		||||
          "width": 10,
 | 
			
		||||
          "x": 20,
 | 
			
		||||
          "y": 20,
 | 
			
		||||
        },
 | 
			
		||||
      ],
 | 
			
		||||
    },
 | 
			
		||||
  ],
 | 
			
		||||
}
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
exports[`regression tests click on an element and drag it: [dragged] number of elements 1`] = `1`;
 | 
			
		||||
 | 
			
		||||
exports[`regression tests click on an element and drag it: [dragged] number of renders 1`] = `12`;
 | 
			
		||||
 | 
			
		||||
exports[`regression tests click on an element and drag it: [end of test] appState 1`] = `
 | 
			
		||||
Object {
 | 
			
		||||
  "activeTool": Object {
 | 
			
		||||
@@ -3795,7 +4119,10 @@ Object {
 | 
			
		||||
  "penDetected": false,
 | 
			
		||||
  "penMode": false,
 | 
			
		||||
  "pendingImageElementId": null,
 | 
			
		||||
  "previousSelectedElementIds": Object {},
 | 
			
		||||
  "previousSelectedElementIds": Object {
 | 
			
		||||
    "id0": true,
 | 
			
		||||
    "id1": true,
 | 
			
		||||
  },
 | 
			
		||||
  "resizingElement": null,
 | 
			
		||||
  "scrollX": 0,
 | 
			
		||||
  "scrollY": 0,
 | 
			
		||||
@@ -3803,6 +4130,7 @@ Object {
 | 
			
		||||
  "selectedElementIds": Object {
 | 
			
		||||
    "id0": true,
 | 
			
		||||
    "id1": true,
 | 
			
		||||
    "id2": true,
 | 
			
		||||
  },
 | 
			
		||||
  "selectedGroupIds": Object {},
 | 
			
		||||
  "selectedLinearElement": null,
 | 
			
		||||
@@ -3846,8 +4174,8 @@ Object {
 | 
			
		||||
  "strokeWidth": 1,
 | 
			
		||||
  "type": "rectangle",
 | 
			
		||||
  "updated": 1,
 | 
			
		||||
  "version": 2,
 | 
			
		||||
  "versionNonce": 1278240551,
 | 
			
		||||
  "version": 4,
 | 
			
		||||
  "versionNonce": 2019559783,
 | 
			
		||||
  "width": 10,
 | 
			
		||||
  "x": 10,
 | 
			
		||||
  "y": 10,
 | 
			
		||||
@@ -3910,13 +4238,96 @@ Object {
 | 
			
		||||
        },
 | 
			
		||||
      ],
 | 
			
		||||
    },
 | 
			
		||||
    Object {
 | 
			
		||||
      "appState": Object {
 | 
			
		||||
        "editingGroupId": null,
 | 
			
		||||
        "editingLinearElement": null,
 | 
			
		||||
        "name": "Untitled-201933152653",
 | 
			
		||||
        "selectedElementIds": Object {
 | 
			
		||||
          "id0": true,
 | 
			
		||||
          "id1": true,
 | 
			
		||||
        },
 | 
			
		||||
        "selectedGroupIds": Object {},
 | 
			
		||||
        "viewBackgroundColor": "#ffffff",
 | 
			
		||||
      },
 | 
			
		||||
      "elements": Array [
 | 
			
		||||
        Object {
 | 
			
		||||
          "angle": 0,
 | 
			
		||||
          "backgroundColor": "transparent",
 | 
			
		||||
          "boundElements": null,
 | 
			
		||||
          "fillStyle": "hachure",
 | 
			
		||||
          "groupIds": Array [],
 | 
			
		||||
          "height": 10,
 | 
			
		||||
          "id": "id0",
 | 
			
		||||
          "isDeleted": false,
 | 
			
		||||
          "link": null,
 | 
			
		||||
          "locked": false,
 | 
			
		||||
          "opacity": 100,
 | 
			
		||||
          "roughness": 1,
 | 
			
		||||
          "seed": 337897,
 | 
			
		||||
          "strokeColor": "#000000",
 | 
			
		||||
          "strokeSharpness": "sharp",
 | 
			
		||||
          "strokeStyle": "solid",
 | 
			
		||||
          "strokeWidth": 1,
 | 
			
		||||
          "type": "rectangle",
 | 
			
		||||
          "updated": 1,
 | 
			
		||||
          "version": 3,
 | 
			
		||||
          "versionNonce": 453191,
 | 
			
		||||
          "width": 10,
 | 
			
		||||
          "x": 20,
 | 
			
		||||
          "y": 20,
 | 
			
		||||
        },
 | 
			
		||||
      ],
 | 
			
		||||
    },
 | 
			
		||||
    Object {
 | 
			
		||||
      "appState": Object {
 | 
			
		||||
        "editingGroupId": null,
 | 
			
		||||
        "editingLinearElement": null,
 | 
			
		||||
        "name": "Untitled-201933152653",
 | 
			
		||||
        "selectedElementIds": Object {
 | 
			
		||||
          "id0": true,
 | 
			
		||||
          "id1": true,
 | 
			
		||||
          "id2": true,
 | 
			
		||||
        },
 | 
			
		||||
        "selectedGroupIds": Object {},
 | 
			
		||||
        "viewBackgroundColor": "#ffffff",
 | 
			
		||||
      },
 | 
			
		||||
      "elements": Array [
 | 
			
		||||
        Object {
 | 
			
		||||
          "angle": 0,
 | 
			
		||||
          "backgroundColor": "transparent",
 | 
			
		||||
          "boundElements": null,
 | 
			
		||||
          "fillStyle": "hachure",
 | 
			
		||||
          "groupIds": Array [],
 | 
			
		||||
          "height": 10,
 | 
			
		||||
          "id": "id0",
 | 
			
		||||
          "isDeleted": false,
 | 
			
		||||
          "link": null,
 | 
			
		||||
          "locked": false,
 | 
			
		||||
          "opacity": 100,
 | 
			
		||||
          "roughness": 1,
 | 
			
		||||
          "seed": 337897,
 | 
			
		||||
          "strokeColor": "#000000",
 | 
			
		||||
          "strokeSharpness": "sharp",
 | 
			
		||||
          "strokeStyle": "solid",
 | 
			
		||||
          "strokeWidth": 1,
 | 
			
		||||
          "type": "rectangle",
 | 
			
		||||
          "updated": 1,
 | 
			
		||||
          "version": 4,
 | 
			
		||||
          "versionNonce": 2019559783,
 | 
			
		||||
          "width": 10,
 | 
			
		||||
          "x": 10,
 | 
			
		||||
          "y": 10,
 | 
			
		||||
        },
 | 
			
		||||
      ],
 | 
			
		||||
    },
 | 
			
		||||
  ],
 | 
			
		||||
}
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
exports[`regression tests click on an element and drag it: [end of test] number of elements 1`] = `1`;
 | 
			
		||||
 | 
			
		||||
exports[`regression tests click on an element and drag it: [end of test] number of renders 1`] = `12`;
 | 
			
		||||
exports[`regression tests click on an element and drag it: [end of test] number of renders 1`] = `15`;
 | 
			
		||||
 | 
			
		||||
exports[`regression tests click to select a shape: [end of test] appState 1`] = `
 | 
			
		||||
Object {
 | 
			
		||||
@@ -5513,13 +5924,14 @@ Object {
 | 
			
		||||
  "penDetected": false,
 | 
			
		||||
  "penMode": false,
 | 
			
		||||
  "pendingImageElementId": null,
 | 
			
		||||
  "previousSelectedElementIds": Object {},
 | 
			
		||||
  "previousSelectedElementIds": Object {
 | 
			
		||||
    "id0": true,
 | 
			
		||||
  },
 | 
			
		||||
  "resizingElement": null,
 | 
			
		||||
  "scrollX": 0,
 | 
			
		||||
  "scrollY": 0,
 | 
			
		||||
  "scrolledOutside": false,
 | 
			
		||||
  "selectedElementIds": Object {
 | 
			
		||||
    "id0": true,
 | 
			
		||||
    "id1": true,
 | 
			
		||||
  },
 | 
			
		||||
  "selectedGroupIds": Object {},
 | 
			
		||||
@@ -13700,7 +14112,7 @@ Object {
 | 
			
		||||
 | 
			
		||||
exports[`regression tests rerenders UI on language change: [end of test] number of elements 1`] = `0`;
 | 
			
		||||
 | 
			
		||||
exports[`regression tests rerenders UI on language change: [end of test] number of renders 1`] = `6`;
 | 
			
		||||
exports[`regression tests rerenders UI on language change: [end of test] number of renders 1`] = `10`;
 | 
			
		||||
 | 
			
		||||
exports[`regression tests shift click on selected element should deselect it on pointer up: [end of test] appState 1`] = `
 | 
			
		||||
Object {
 | 
			
		||||
@@ -13760,13 +14172,15 @@ Object {
 | 
			
		||||
  "penDetected": false,
 | 
			
		||||
  "penMode": false,
 | 
			
		||||
  "pendingImageElementId": null,
 | 
			
		||||
  "previousSelectedElementIds": Object {},
 | 
			
		||||
  "previousSelectedElementIds": Object {
 | 
			
		||||
    "id0": true,
 | 
			
		||||
  },
 | 
			
		||||
  "resizingElement": null,
 | 
			
		||||
  "scrollX": 0,
 | 
			
		||||
  "scrollY": 0,
 | 
			
		||||
  "scrolledOutside": false,
 | 
			
		||||
  "selectedElementIds": Object {
 | 
			
		||||
    "id0": true,
 | 
			
		||||
    "id0": false,
 | 
			
		||||
    "id1": true,
 | 
			
		||||
  },
 | 
			
		||||
  "selectedGroupIds": Object {},
 | 
			
		||||
 
 | 
			
		||||
@@ -42,7 +42,7 @@ describe("Test dragCreate", () => {
 | 
			
		||||
      // finish (position does not matter)
 | 
			
		||||
      fireEvent.pointerUp(canvas);
 | 
			
		||||
 | 
			
		||||
      expect(renderScene).toHaveBeenCalledTimes(8);
 | 
			
		||||
      expect(renderScene).toHaveBeenCalledTimes(9);
 | 
			
		||||
      expect(h.state.selectionElement).toBeNull();
 | 
			
		||||
 | 
			
		||||
      expect(h.elements.length).toEqual(1);
 | 
			
		||||
@@ -73,7 +73,7 @@ describe("Test dragCreate", () => {
 | 
			
		||||
      // finish (position does not matter)
 | 
			
		||||
      fireEvent.pointerUp(canvas);
 | 
			
		||||
 | 
			
		||||
      expect(renderScene).toHaveBeenCalledTimes(8);
 | 
			
		||||
      expect(renderScene).toHaveBeenCalledTimes(9);
 | 
			
		||||
      expect(h.state.selectionElement).toBeNull();
 | 
			
		||||
 | 
			
		||||
      expect(h.elements.length).toEqual(1);
 | 
			
		||||
@@ -104,7 +104,7 @@ describe("Test dragCreate", () => {
 | 
			
		||||
      // finish (position does not matter)
 | 
			
		||||
      fireEvent.pointerUp(canvas);
 | 
			
		||||
 | 
			
		||||
      expect(renderScene).toHaveBeenCalledTimes(8);
 | 
			
		||||
      expect(renderScene).toHaveBeenCalledTimes(9);
 | 
			
		||||
      expect(h.state.selectionElement).toBeNull();
 | 
			
		||||
 | 
			
		||||
      expect(h.elements.length).toEqual(1);
 | 
			
		||||
@@ -135,7 +135,7 @@ describe("Test dragCreate", () => {
 | 
			
		||||
      // finish (position does not matter)
 | 
			
		||||
      fireEvent.pointerUp(canvas);
 | 
			
		||||
 | 
			
		||||
      expect(renderScene).toHaveBeenCalledTimes(8);
 | 
			
		||||
      expect(renderScene).toHaveBeenCalledTimes(9);
 | 
			
		||||
      expect(h.state.selectionElement).toBeNull();
 | 
			
		||||
 | 
			
		||||
      expect(h.elements.length).toEqual(1);
 | 
			
		||||
@@ -170,7 +170,7 @@ describe("Test dragCreate", () => {
 | 
			
		||||
      // finish (position does not matter)
 | 
			
		||||
      fireEvent.pointerUp(canvas);
 | 
			
		||||
 | 
			
		||||
      expect(renderScene).toHaveBeenCalledTimes(8);
 | 
			
		||||
      expect(renderScene).toHaveBeenCalledTimes(9);
 | 
			
		||||
      expect(h.state.selectionElement).toBeNull();
 | 
			
		||||
 | 
			
		||||
      expect(h.elements.length).toEqual(1);
 | 
			
		||||
@@ -210,7 +210,7 @@ describe("Test dragCreate", () => {
 | 
			
		||||
      // finish (position does not matter)
 | 
			
		||||
      fireEvent.pointerUp(canvas);
 | 
			
		||||
 | 
			
		||||
      expect(renderScene).toHaveBeenCalledTimes(6);
 | 
			
		||||
      expect(renderScene).toHaveBeenCalledTimes(7);
 | 
			
		||||
      expect(h.state.selectionElement).toBeNull();
 | 
			
		||||
      expect(h.elements.length).toEqual(0);
 | 
			
		||||
    });
 | 
			
		||||
@@ -229,7 +229,7 @@ describe("Test dragCreate", () => {
 | 
			
		||||
      // finish (position does not matter)
 | 
			
		||||
      fireEvent.pointerUp(canvas);
 | 
			
		||||
 | 
			
		||||
      expect(renderScene).toHaveBeenCalledTimes(6);
 | 
			
		||||
      expect(renderScene).toHaveBeenCalledTimes(7);
 | 
			
		||||
      expect(h.state.selectionElement).toBeNull();
 | 
			
		||||
      expect(h.elements.length).toEqual(0);
 | 
			
		||||
    });
 | 
			
		||||
@@ -248,7 +248,7 @@ describe("Test dragCreate", () => {
 | 
			
		||||
      // finish (position does not matter)
 | 
			
		||||
      fireEvent.pointerUp(canvas);
 | 
			
		||||
 | 
			
		||||
      expect(renderScene).toHaveBeenCalledTimes(6);
 | 
			
		||||
      expect(renderScene).toHaveBeenCalledTimes(7);
 | 
			
		||||
      expect(h.state.selectionElement).toBeNull();
 | 
			
		||||
      expect(h.elements.length).toEqual(0);
 | 
			
		||||
    });
 | 
			
		||||
@@ -272,7 +272,7 @@ describe("Test dragCreate", () => {
 | 
			
		||||
        key: KEYS.ENTER,
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      expect(renderScene).toHaveBeenCalledTimes(7);
 | 
			
		||||
      expect(renderScene).toHaveBeenCalledTimes(8);
 | 
			
		||||
      expect(h.state.selectionElement).toBeNull();
 | 
			
		||||
      expect(h.elements.length).toEqual(0);
 | 
			
		||||
    });
 | 
			
		||||
@@ -296,7 +296,7 @@ describe("Test dragCreate", () => {
 | 
			
		||||
        key: KEYS.ENTER,
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      expect(renderScene).toHaveBeenCalledTimes(7);
 | 
			
		||||
      expect(renderScene).toHaveBeenCalledTimes(8);
 | 
			
		||||
      expect(h.state.selectionElement).toBeNull();
 | 
			
		||||
      expect(h.elements.length).toEqual(0);
 | 
			
		||||
    });
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,6 @@
 | 
			
		||||
import { fireEvent, render, waitFor } from "./test-utils";
 | 
			
		||||
import { queryByTestId } from "@testing-library/react";
 | 
			
		||||
 | 
			
		||||
import ExcalidrawApp from "../excalidraw-app";
 | 
			
		||||
import { API } from "./helpers/api";
 | 
			
		||||
import { MIME_TYPES } from "../constants";
 | 
			
		||||
@@ -93,15 +95,11 @@ describe("library menu", () => {
 | 
			
		||||
    const latestLibrary = await h.app.library.getLatestLibrary();
 | 
			
		||||
    expect(latestLibrary.length).toBe(0);
 | 
			
		||||
 | 
			
		||||
    const libraryButton = container.querySelector(".ToolIcon__library");
 | 
			
		||||
    const libraryButton = container.querySelector(".library-button");
 | 
			
		||||
 | 
			
		||||
    fireEvent.click(libraryButton!);
 | 
			
		||||
 | 
			
		||||
    const loadLibraryButton = container.querySelector(
 | 
			
		||||
      ".library-actions .library-actions--load",
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    fireEvent.click(loadLibraryButton!);
 | 
			
		||||
    fireEvent.click(container.querySelector(".Sidebar__dropdown-btn")!);
 | 
			
		||||
    queryByTestId(container, "lib-dropdown--load")!.click();
 | 
			
		||||
 | 
			
		||||
    const libraryItems = parseLibraryJSON(await libraryJSONPromise);
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -38,7 +38,7 @@ describe("move element", () => {
 | 
			
		||||
      fireEvent.pointerMove(canvas, { clientX: 60, clientY: 70 });
 | 
			
		||||
      fireEvent.pointerUp(canvas);
 | 
			
		||||
 | 
			
		||||
      expect(renderScene).toHaveBeenCalledTimes(8);
 | 
			
		||||
      expect(renderScene).toHaveBeenCalledTimes(9);
 | 
			
		||||
      expect(h.state.selectionElement).toBeNull();
 | 
			
		||||
      expect(h.elements.length).toEqual(1);
 | 
			
		||||
      expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy();
 | 
			
		||||
@@ -77,7 +77,7 @@ describe("move element", () => {
 | 
			
		||||
    // select the second rectangles
 | 
			
		||||
    new Pointer("mouse").clickOn(rectB);
 | 
			
		||||
 | 
			
		||||
    expect(renderScene).toHaveBeenCalledTimes(22);
 | 
			
		||||
    expect(renderScene).toHaveBeenCalledTimes(23);
 | 
			
		||||
    expect(h.state.selectionElement).toBeNull();
 | 
			
		||||
    expect(h.elements.length).toEqual(3);
 | 
			
		||||
    expect(h.state.selectedElementIds[rectB.id]).toBeTruthy();
 | 
			
		||||
@@ -120,7 +120,7 @@ describe("duplicate element on move when ALT is clicked", () => {
 | 
			
		||||
      fireEvent.pointerMove(canvas, { clientX: 60, clientY: 70 });
 | 
			
		||||
      fireEvent.pointerUp(canvas);
 | 
			
		||||
 | 
			
		||||
      expect(renderScene).toHaveBeenCalledTimes(8);
 | 
			
		||||
      expect(renderScene).toHaveBeenCalledTimes(9);
 | 
			
		||||
      expect(h.state.selectionElement).toBeNull();
 | 
			
		||||
      expect(h.elements.length).toEqual(1);
 | 
			
		||||
      expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy();
 | 
			
		||||
 
 | 
			
		||||
@@ -42,7 +42,7 @@ describe("remove shape in non linear elements", () => {
 | 
			
		||||
    fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 });
 | 
			
		||||
    fireEvent.pointerUp(canvas, { clientX: 30, clientY: 30 });
 | 
			
		||||
 | 
			
		||||
    expect(renderScene).toHaveBeenCalledTimes(6);
 | 
			
		||||
    expect(renderScene).toHaveBeenCalledTimes(7);
 | 
			
		||||
    expect(h.elements.length).toEqual(0);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
@@ -56,7 +56,7 @@ describe("remove shape in non linear elements", () => {
 | 
			
		||||
    fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 });
 | 
			
		||||
    fireEvent.pointerUp(canvas, { clientX: 30, clientY: 30 });
 | 
			
		||||
 | 
			
		||||
    expect(renderScene).toHaveBeenCalledTimes(6);
 | 
			
		||||
    expect(renderScene).toHaveBeenCalledTimes(7);
 | 
			
		||||
    expect(h.elements.length).toEqual(0);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
@@ -70,7 +70,7 @@ describe("remove shape in non linear elements", () => {
 | 
			
		||||
    fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 });
 | 
			
		||||
    fireEvent.pointerUp(canvas, { clientX: 30, clientY: 30 });
 | 
			
		||||
 | 
			
		||||
    expect(renderScene).toHaveBeenCalledTimes(6);
 | 
			
		||||
    expect(renderScene).toHaveBeenCalledTimes(7);
 | 
			
		||||
    expect(h.elements.length).toEqual(0);
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
@@ -102,7 +102,7 @@ describe("multi point mode in linear elements", () => {
 | 
			
		||||
      key: KEYS.ENTER,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    expect(renderScene).toHaveBeenCalledTimes(14);
 | 
			
		||||
    expect(renderScene).toHaveBeenCalledTimes(15);
 | 
			
		||||
    expect(h.elements.length).toEqual(1);
 | 
			
		||||
 | 
			
		||||
    const element = h.elements[0] as ExcalidrawLinearElement;
 | 
			
		||||
@@ -145,7 +145,7 @@ describe("multi point mode in linear elements", () => {
 | 
			
		||||
      key: KEYS.ENTER,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    expect(renderScene).toHaveBeenCalledTimes(14);
 | 
			
		||||
    expect(renderScene).toHaveBeenCalledTimes(15);
 | 
			
		||||
    expect(h.elements.length).toEqual(1);
 | 
			
		||||
 | 
			
		||||
    const element = h.elements[0] as ExcalidrawLinearElement;
 | 
			
		||||
 
 | 
			
		||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -90,7 +90,10 @@ describe("<Excalidraw/>", () => {
 | 
			
		||||
  describe("Test theme prop", () => {
 | 
			
		||||
    it("should show the theme toggle by default", async () => {
 | 
			
		||||
      const { container } = await render(<Excalidraw />);
 | 
			
		||||
 | 
			
		||||
      expect(h.state.theme).toBe(THEME.LIGHT);
 | 
			
		||||
 | 
			
		||||
      queryByTestId(container, "menu-button")!.click();
 | 
			
		||||
      const darkModeToggle = queryByTestId(container, "toggle-dark-mode");
 | 
			
		||||
      expect(darkModeToggle).toBeTruthy();
 | 
			
		||||
    });
 | 
			
		||||
 
 | 
			
		||||
@@ -10,6 +10,7 @@ const toolMap = {
 | 
			
		||||
  line: "line",
 | 
			
		||||
  freedraw: "freedraw",
 | 
			
		||||
  text: "text",
 | 
			
		||||
  eraser: "eraser",
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type ToolName = keyof typeof toolMap;
 | 
			
		||||
 
 | 
			
		||||
@@ -174,7 +174,7 @@ describe("regression tests", () => {
 | 
			
		||||
    mouse.up(10, 10);
 | 
			
		||||
 | 
			
		||||
    const { x: prevX, y: prevY } = API.getSelectedElement();
 | 
			
		||||
    mouse.down(-10, -10);
 | 
			
		||||
    mouse.down(-8, -8);
 | 
			
		||||
    mouse.up(10, 10);
 | 
			
		||||
 | 
			
		||||
    const { x: nextX, y: nextY } = API.getSelectedElement();
 | 
			
		||||
@@ -201,7 +201,7 @@ describe("regression tests", () => {
 | 
			
		||||
    ).toBe(1);
 | 
			
		||||
 | 
			
		||||
    Keyboard.withModifierKeys({ alt: true }, () => {
 | 
			
		||||
      mouse.down(-10, -10);
 | 
			
		||||
      mouse.down(-8, -8);
 | 
			
		||||
      mouse.up(10, 10);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
@@ -446,6 +446,8 @@ describe("regression tests", () => {
 | 
			
		||||
    UI.clickTool("rectangle");
 | 
			
		||||
    // english lang should display `thin` label
 | 
			
		||||
    expect(screen.queryByTitle(/thin/i)).not.toBeNull();
 | 
			
		||||
    fireEvent.click(document.querySelector(".menu-button")!);
 | 
			
		||||
 | 
			
		||||
    fireEvent.change(document.querySelector(".dropdown-select__language")!, {
 | 
			
		||||
      target: { value: "de-DE" },
 | 
			
		||||
    });
 | 
			
		||||
@@ -672,9 +674,10 @@ describe("regression tests", () => {
 | 
			
		||||
    mouse.down();
 | 
			
		||||
    mouse.up(100, 100);
 | 
			
		||||
 | 
			
		||||
    // hits bounding box without hitting element
 | 
			
		||||
    mouse.down();
 | 
			
		||||
    expect(API.getSelectedElements().length).toBe(1);
 | 
			
		||||
 | 
			
		||||
    // hits bounding box without hitting element
 | 
			
		||||
    mouse.down(98, 98);
 | 
			
		||||
    mouse.up();
 | 
			
		||||
    expect(API.getSelectedElements().length).toBe(0);
 | 
			
		||||
  });
 | 
			
		||||
@@ -744,7 +747,7 @@ describe("regression tests", () => {
 | 
			
		||||
 | 
			
		||||
    // drag element from point on bounding box that doesn't hit element
 | 
			
		||||
    mouse.reset();
 | 
			
		||||
    mouse.down();
 | 
			
		||||
    mouse.down(8, 8);
 | 
			
		||||
    mouse.up(25, 25);
 | 
			
		||||
 | 
			
		||||
    expect(API.getSelectedElement().x).toEqual(prevX + 25);
 | 
			
		||||
@@ -1020,7 +1023,7 @@ describe("regression tests", () => {
 | 
			
		||||
    // Rectangle is already selected since creating
 | 
			
		||||
    // it was our last action
 | 
			
		||||
    Keyboard.withModifierKeys({ shift: true }, () => {
 | 
			
		||||
      mouse.down();
 | 
			
		||||
      mouse.down(-8, -8);
 | 
			
		||||
    });
 | 
			
		||||
    expect(API.getSelectedElements().length).toBe(1);
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -154,7 +154,7 @@ describe("selection element", () => {
 | 
			
		||||
    const canvas = container.querySelector("canvas")!;
 | 
			
		||||
    fireEvent.pointerDown(canvas, { clientX: 60, clientY: 100 });
 | 
			
		||||
 | 
			
		||||
    expect(renderScene).toHaveBeenCalledTimes(4);
 | 
			
		||||
    expect(renderScene).toHaveBeenCalledTimes(5);
 | 
			
		||||
    const selectionElement = h.state.selectionElement!;
 | 
			
		||||
    expect(selectionElement).not.toBeNull();
 | 
			
		||||
    expect(selectionElement.type).toEqual("selection");
 | 
			
		||||
@@ -175,7 +175,7 @@ describe("selection element", () => {
 | 
			
		||||
    fireEvent.pointerDown(canvas, { clientX: 60, clientY: 100 });
 | 
			
		||||
    fireEvent.pointerMove(canvas, { clientX: 150, clientY: 30 });
 | 
			
		||||
 | 
			
		||||
    expect(renderScene).toHaveBeenCalledTimes(5);
 | 
			
		||||
    expect(renderScene).toHaveBeenCalledTimes(6);
 | 
			
		||||
    const selectionElement = h.state.selectionElement!;
 | 
			
		||||
    expect(selectionElement).not.toBeNull();
 | 
			
		||||
    expect(selectionElement.type).toEqual("selection");
 | 
			
		||||
@@ -197,7 +197,7 @@ describe("selection element", () => {
 | 
			
		||||
    fireEvent.pointerMove(canvas, { clientX: 150, clientY: 30 });
 | 
			
		||||
    fireEvent.pointerUp(canvas);
 | 
			
		||||
 | 
			
		||||
    expect(renderScene).toHaveBeenCalledTimes(6);
 | 
			
		||||
    expect(renderScene).toHaveBeenCalledTimes(7);
 | 
			
		||||
    expect(h.state.selectionElement).toBeNull();
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
@@ -232,7 +232,7 @@ describe("select single element on the scene", () => {
 | 
			
		||||
    fireEvent.pointerDown(canvas, { clientX: 45, clientY: 20 });
 | 
			
		||||
    fireEvent.pointerUp(canvas);
 | 
			
		||||
 | 
			
		||||
    expect(renderScene).toHaveBeenCalledTimes(10);
 | 
			
		||||
    expect(renderScene).toHaveBeenCalledTimes(11);
 | 
			
		||||
    expect(h.state.selectionElement).toBeNull();
 | 
			
		||||
    expect(h.elements.length).toEqual(1);
 | 
			
		||||
    expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy();
 | 
			
		||||
@@ -261,7 +261,7 @@ describe("select single element on the scene", () => {
 | 
			
		||||
    fireEvent.pointerDown(canvas, { clientX: 45, clientY: 20 });
 | 
			
		||||
    fireEvent.pointerUp(canvas);
 | 
			
		||||
 | 
			
		||||
    expect(renderScene).toHaveBeenCalledTimes(10);
 | 
			
		||||
    expect(renderScene).toHaveBeenCalledTimes(11);
 | 
			
		||||
    expect(h.state.selectionElement).toBeNull();
 | 
			
		||||
    expect(h.elements.length).toEqual(1);
 | 
			
		||||
    expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy();
 | 
			
		||||
@@ -290,7 +290,7 @@ describe("select single element on the scene", () => {
 | 
			
		||||
    fireEvent.pointerDown(canvas, { clientX: 45, clientY: 20 });
 | 
			
		||||
    fireEvent.pointerUp(canvas);
 | 
			
		||||
 | 
			
		||||
    expect(renderScene).toHaveBeenCalledTimes(10);
 | 
			
		||||
    expect(renderScene).toHaveBeenCalledTimes(11);
 | 
			
		||||
    expect(h.state.selectionElement).toBeNull();
 | 
			
		||||
    expect(h.elements.length).toEqual(1);
 | 
			
		||||
    expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy();
 | 
			
		||||
@@ -332,7 +332,7 @@ describe("select single element on the scene", () => {
 | 
			
		||||
    fireEvent.pointerDown(canvas, { clientX: 40, clientY: 40 });
 | 
			
		||||
    fireEvent.pointerUp(canvas);
 | 
			
		||||
 | 
			
		||||
    expect(renderScene).toHaveBeenCalledTimes(10);
 | 
			
		||||
    expect(renderScene).toHaveBeenCalledTimes(11);
 | 
			
		||||
    expect(h.state.selectionElement).toBeNull();
 | 
			
		||||
    expect(h.elements.length).toEqual(1);
 | 
			
		||||
    expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy();
 | 
			
		||||
@@ -373,7 +373,7 @@ describe("select single element on the scene", () => {
 | 
			
		||||
    fireEvent.pointerDown(canvas, { clientX: 40, clientY: 40 });
 | 
			
		||||
    fireEvent.pointerUp(canvas);
 | 
			
		||||
 | 
			
		||||
    expect(renderScene).toHaveBeenCalledTimes(10);
 | 
			
		||||
    expect(renderScene).toHaveBeenCalledTimes(11);
 | 
			
		||||
    expect(h.state.selectionElement).toBeNull();
 | 
			
		||||
    expect(h.elements.length).toEqual(1);
 | 
			
		||||
    expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy();
 | 
			
		||||
 
 | 
			
		||||
@@ -16,8 +16,6 @@ import { SceneData } from "../types";
 | 
			
		||||
import { getSelectedElements } from "../scene/selection";
 | 
			
		||||
import { ExcalidrawElement } from "../element/types";
 | 
			
		||||
 | 
			
		||||
require("fake-indexeddb/auto");
 | 
			
		||||
 | 
			
		||||
const customQueries = {
 | 
			
		||||
  ...queries,
 | 
			
		||||
  ...toolQueries,
 | 
			
		||||
 
 | 
			
		||||
@@ -113,7 +113,6 @@ export type AppState = {
 | 
			
		||||
  exportEmbedScene: boolean;
 | 
			
		||||
  exportWithDarkMode: boolean;
 | 
			
		||||
  exportScale: number;
 | 
			
		||||
  exportPadding: number;
 | 
			
		||||
  currentItemStrokeColor: string;
 | 
			
		||||
  currentItemBackgroundColor: string;
 | 
			
		||||
  currentItemFillStyle: ExcalidrawElement["fillStyle"];
 | 
			
		||||
@@ -285,6 +284,10 @@ export interface ExcalidrawProps {
 | 
			
		||||
    isMobile: boolean,
 | 
			
		||||
    appState: AppState,
 | 
			
		||||
  ) => JSX.Element | null;
 | 
			
		||||
  renderMenuLinks?:
 | 
			
		||||
    | ((isMobile: boolean, appState: AppState) => JSX.Element | null)
 | 
			
		||||
    | null;
 | 
			
		||||
  hideWelcomeScreen?: boolean;
 | 
			
		||||
  renderFooter?: (isMobile: boolean, appState: AppState) => JSX.Element | null;
 | 
			
		||||
  langCode?: Language["code"];
 | 
			
		||||
  viewModeEnabled?: boolean;
 | 
			
		||||
@@ -300,6 +303,7 @@ export interface ExcalidrawProps {
 | 
			
		||||
  UIOptions?: {
 | 
			
		||||
    dockedSidebarBreakpoint?: number;
 | 
			
		||||
    canvasActions?: CanvasActions;
 | 
			
		||||
    showLanguageList?: boolean;
 | 
			
		||||
  };
 | 
			
		||||
  detectScroll?: boolean;
 | 
			
		||||
  handleKeyboardGlobally?: boolean;
 | 
			
		||||
@@ -372,6 +376,7 @@ export type AppProps = Merge<
 | 
			
		||||
    UIOptions: {
 | 
			
		||||
      canvasActions: Required<CanvasActions> & { export: ExportOpts };
 | 
			
		||||
      dockedSidebarBreakpoint?: number;
 | 
			
		||||
      showLanguageList?: boolean;
 | 
			
		||||
    };
 | 
			
		||||
    detectScroll: boolean;
 | 
			
		||||
    handleKeyboardGlobally: boolean;
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user