Compare commits

..

2 Commits

Author SHA1 Message Date
dwelle fa9631617f wip: get exact bounding box from bitmap when exporting w/o padding 2022-11-03 18:34:17 +01:00
dwelle 0314e81396 feat: support toggling export padding 2022-11-03 17:31:41 +01:00
25 changed files with 504 additions and 1896 deletions
+22
View File
@@ -89,6 +89,28 @@ 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({ export const actionChangeExportBackground = register({
name: "changeExportBackground", name: "changeExportBackground",
trackEvent: { category: "export", action: "toggleBackground" }, trackEvent: { category: "export", action: "toggleBackground" },
+1
View File
@@ -68,6 +68,7 @@ export type ActionName =
| "finalize" | "finalize"
| "changeProjectName" | "changeProjectName"
| "changeExportBackground" | "changeExportBackground"
| "changeExportPadding"
| "changeExportEmbedScene" | "changeExportEmbedScene"
| "changeExportScale" | "changeExportScale"
| "saveToActiveFile" | "saveToActiveFile"
+3
View File
@@ -1,5 +1,6 @@
import oc from "open-color"; import oc from "open-color";
import { import {
DEFAULT_EXPORT_PADDING,
DEFAULT_FONT_FAMILY, DEFAULT_FONT_FAMILY,
DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE,
DEFAULT_TEXT_ALIGN, DEFAULT_TEXT_ALIGN,
@@ -55,6 +56,7 @@ export const getDefaultAppState = (): Omit<
exportScale: defaultExportScale, exportScale: defaultExportScale,
exportEmbedScene: false, exportEmbedScene: false,
exportWithDarkMode: false, exportWithDarkMode: false,
exportPadding: DEFAULT_EXPORT_PADDING,
fileHandle: null, fileHandle: null,
gridSize: null, gridSize: null,
isBindingEnabled: true, isBindingEnabled: true,
@@ -145,6 +147,7 @@ const APP_STATE_STORAGE_CONF = (<
exportBackground: { browser: true, export: false, server: false }, exportBackground: { browser: true, export: false, server: false },
exportEmbedScene: { browser: true, export: false, server: false }, exportEmbedScene: { browser: true, export: false, server: false },
exportScale: { 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 }, exportWithDarkMode: { browser: true, export: false, server: false },
fileHandle: { browser: false, export: false, server: false }, fileHandle: { browser: false, export: false, server: false },
gridSize: { browser: true, export: true, server: true }, gridSize: { browser: true, export: true, server: true },
-3
View File
@@ -516,7 +516,6 @@ class App extends React.Component<AppProps, AppState> {
const { const {
onCollabButtonClick, onCollabButtonClick,
renderTopRightUI, renderTopRightUI,
renderMenuLinks,
renderFooter, renderFooter,
renderCustomStats, renderCustomStats,
} = this.props; } = this.props;
@@ -563,7 +562,6 @@ class App extends React.Component<AppProps, AppState> {
langCode={getLanguage().code} langCode={getLanguage().code}
isCollaborating={this.props.isCollaborating} isCollaborating={this.props.isCollaborating}
renderTopRightUI={renderTopRightUI} renderTopRightUI={renderTopRightUI}
renderMenuLinks={renderMenuLinks}
renderCustomFooter={renderFooter} renderCustomFooter={renderFooter}
renderCustomStats={renderCustomStats} renderCustomStats={renderCustomStats}
renderCustomSidebar={this.props.renderSidebar} renderCustomSidebar={this.props.renderSidebar}
@@ -578,7 +576,6 @@ class App extends React.Component<AppProps, AppState> {
id={this.id} id={this.id}
onImageAction={this.onImageAction} onImageAction={this.onImageAction}
renderWelcomeScreen={ renderWelcomeScreen={
this.props.hideWelcomeScreen !== true &&
this.state.showWelcomeScreen && this.state.showWelcomeScreen &&
this.state.activeTool.type === "selection" && this.state.activeTool.type === "selection" &&
!this.scene.getElementsIncludingDeleted().length !this.scene.getElementsIncludingDeleted().length
+3 -7
View File
@@ -79,7 +79,6 @@ const ImageExportModal = ({
elements, elements,
appState, appState,
files, files,
exportPadding = DEFAULT_EXPORT_PADDING,
actionManager, actionManager,
onExportToPng, onExportToPng,
onExportToSvg, onExportToSvg,
@@ -88,7 +87,6 @@ const ImageExportModal = ({
appState: AppState; appState: AppState;
elements: readonly NonDeletedExcalidrawElement[]; elements: readonly NonDeletedExcalidrawElement[];
files: BinaryFiles; files: BinaryFiles;
exportPadding?: number;
actionManager: ActionManager; actionManager: ActionManager;
onExportToPng: ExportCB; onExportToPng: ExportCB;
onExportToSvg: ExportCB; onExportToSvg: ExportCB;
@@ -116,7 +114,7 @@ const ImageExportModal = ({
exportToCanvas(exportedElements, appState, files, { exportToCanvas(exportedElements, appState, files, {
exportBackground, exportBackground,
viewBackgroundColor, viewBackgroundColor,
exportPadding, exportPadding: appState.exportPadding,
}) })
.then((canvas) => { .then((canvas) => {
// if converting to blob fails, there's some problem that will // if converting to blob fails, there's some problem that will
@@ -134,7 +132,6 @@ const ImageExportModal = ({
files, files,
exportedElements, exportedElements,
exportBackground, exportBackground,
exportPadding,
viewBackgroundColor, viewBackgroundColor,
]); ]);
@@ -151,8 +148,10 @@ const ImageExportModal = ({
// dunno why this is needed, but when the items wrap it creates // dunno why this is needed, but when the items wrap it creates
// an overflow // an overflow
overflow: "hidden", overflow: "hidden",
gap: ".6rem",
}} }}
> >
{actionManager.renderAction("changeExportPadding")}
{actionManager.renderAction("changeExportBackground")} {actionManager.renderAction("changeExportBackground")}
{someElementIsSelected && ( {someElementIsSelected && (
<CheckboxItem <CheckboxItem
@@ -221,7 +220,6 @@ export const ImageExportDialog = ({
appState, appState,
setAppState, setAppState,
files, files,
exportPadding = DEFAULT_EXPORT_PADDING,
actionManager, actionManager,
onExportToPng, onExportToPng,
onExportToSvg, onExportToSvg,
@@ -231,7 +229,6 @@ export const ImageExportDialog = ({
setAppState: React.Component<any, AppState>["setState"]; setAppState: React.Component<any, AppState>["setState"];
elements: readonly NonDeletedExcalidrawElement[]; elements: readonly NonDeletedExcalidrawElement[];
files: BinaryFiles; files: BinaryFiles;
exportPadding?: number;
actionManager: ActionManager; actionManager: ActionManager;
onExportToPng: ExportCB; onExportToPng: ExportCB;
onExportToSvg: ExportCB; onExportToSvg: ExportCB;
@@ -249,7 +246,6 @@ export const ImageExportDialog = ({
elements={elements} elements={elements}
appState={appState} appState={appState}
files={files} files={files}
exportPadding={exportPadding}
actionManager={actionManager} actionManager={actionManager}
onExportToPng={onExportToPng} onExportToPng={onExportToPng}
onExportToSvg={onExportToSvg} onExportToSvg={onExportToSvg}
+14 -31
View File
@@ -71,7 +71,6 @@ interface LayerUIProps {
langCode: Language["code"]; langCode: Language["code"];
isCollaborating: boolean; isCollaborating: boolean;
renderTopRightUI?: ExcalidrawProps["renderTopRightUI"]; renderTopRightUI?: ExcalidrawProps["renderTopRightUI"];
renderMenuLinks?: ExcalidrawProps["renderMenuLinks"];
renderCustomFooter?: ExcalidrawProps["renderFooter"]; renderCustomFooter?: ExcalidrawProps["renderFooter"];
renderCustomStats?: ExcalidrawProps["renderCustomStats"]; renderCustomStats?: ExcalidrawProps["renderCustomStats"];
renderCustomSidebar?: ExcalidrawProps["renderSidebar"]; renderCustomSidebar?: ExcalidrawProps["renderSidebar"];
@@ -97,7 +96,6 @@ const LayerUI = ({
showExitZenModeBtn, showExitZenModeBtn,
isCollaborating, isCollaborating,
renderTopRightUI, renderTopRightUI,
renderMenuLinks,
renderCustomFooter, renderCustomFooter,
renderCustomStats, renderCustomStats,
renderCustomSidebar, renderCustomSidebar,
@@ -146,6 +144,7 @@ const LayerUI = ({
exportBackground: appState.exportBackground, exportBackground: appState.exportBackground,
name: appState.name, name: appState.name,
viewBackgroundColor: appState.viewBackgroundColor, viewBackgroundColor: appState.viewBackgroundColor,
exportPadding: appState.exportPadding,
}, },
) )
.catch(muteFSAbortError) .catch(muteFSAbortError)
@@ -198,7 +197,6 @@ const LayerUI = ({
})} })}
onClick={() => setIsMenuOpen(!isMenuOpen)} onClick={() => setIsMenuOpen(!isMenuOpen)}
type="button" type="button"
data-testid="menu-button"
> >
{HamburgerMenuIcon} {HamburgerMenuIcon}
</button> </button>
@@ -220,19 +218,16 @@ const LayerUI = ({
actionManager.renderAction("loadScene")} actionManager.renderAction("loadScene")}
{/* // TODO barnabasmolnar/editor-redesign */} {/* // TODO barnabasmolnar/editor-redesign */}
{/* is this fine here? */} {/* is this fine here? */}
{UIOptions.canvasActions.saveToActiveFile && {appState.fileHandle &&
appState.fileHandle &&
actionManager.renderAction("saveToActiveFile")} actionManager.renderAction("saveToActiveFile")}
{renderJSONExportDialog()} {renderJSONExportDialog()}
{UIOptions.canvasActions.saveAsImage && ( <MenuItem
<MenuItem label={t("buttons.exportImage")}
label={t("buttons.exportImage")} icon={ExportImageIcon}
icon={ExportImageIcon} dataTestId="image-export-button"
dataTestId="image-export-button" onClick={() => setAppState({ openDialog: "imageExport" })}
onClick={() => setAppState({ openDialog: "imageExport" })} shortcut={getShortcutFromShortcutName("imageExport")}
shortcut={getShortcutFromShortcutName("imageExport")} />
/>
)}
{onCollabButtonClick && ( {onCollabButtonClick && (
<CollabButton <CollabButton
isCollaborating={isCollaborating} isCollaborating={isCollaborating}
@@ -243,16 +238,8 @@ const LayerUI = ({
{actionManager.renderAction("toggleShortcuts", undefined, true)} {actionManager.renderAction("toggleShortcuts", undefined, true)}
{!appState.viewModeEnabled && {!appState.viewModeEnabled &&
actionManager.renderAction("clearCanvas")} actionManager.renderAction("clearCanvas")}
{typeof renderMenuLinks === "undefined" ? ( //zsviczian <Separator />
<Separator /> <MenuLinks />
) : (
renderMenuLinks && <Separator />
)}
{typeof renderMenuLinks === "undefined" ? ( //zsviczian
<MenuLinks />
) : (
renderMenuLinks && renderMenuLinks(device.isMobile, appState)
)}
<Separator /> <Separator />
<div <div
style={{ style={{
@@ -262,11 +249,9 @@ const LayerUI = ({
}} }}
> >
<div>{actionManager.renderAction("toggleTheme")}</div> <div>{actionManager.renderAction("toggleTheme")}</div>
{UIOptions.showLanguageList !== false && ( <div style={{ padding: "0 0.625rem" }}>
<div style={{ padding: "0 0.625rem" }}> <LanguageList style={{ width: "100%" }} />
<LanguageList style={{ width: "100%" }} /> </div>
</div>
)}
{!appState.viewModeEnabled && ( {!appState.viewModeEnabled && (
<div> <div>
<div style={{ fontSize: ".75rem", marginBottom: ".5rem" }}> <div style={{ fontSize: ".75rem", marginBottom: ".5rem" }}>
@@ -497,11 +482,9 @@ const LayerUI = ({
renderCustomFooter={renderCustomFooter} renderCustomFooter={renderCustomFooter}
onImageAction={onImageAction} onImageAction={onImageAction}
renderTopRightUI={renderTopRightUI} renderTopRightUI={renderTopRightUI}
renderMenuLinks={renderMenuLinks}
renderCustomStats={renderCustomStats} renderCustomStats={renderCustomStats}
renderSidebars={renderSidebars} renderSidebars={renderSidebars}
device={device} device={device}
UIOptions={UIOptions}
/> />
)} )}
+10 -24
View File
@@ -1,5 +1,5 @@
import React from "react"; import React from "react";
import { AppProps, AppState, Device, ExcalidrawProps } from "../types"; import { AppState, Device, ExcalidrawProps } from "../types";
import { ActionManager } from "../actions/manager"; import { ActionManager } from "../actions/manager";
import { t } from "../i18n"; import { t } from "../i18n";
import Stack from "./Stack"; import Stack from "./Stack";
@@ -45,12 +45,10 @@ type MobileMenuProps = {
isMobile: boolean, isMobile: boolean,
appState: AppState, appState: AppState,
) => JSX.Element | null; ) => JSX.Element | null;
renderMenuLinks?: ExcalidrawProps["renderMenuLinks"];
renderCustomStats?: ExcalidrawProps["renderCustomStats"]; renderCustomStats?: ExcalidrawProps["renderCustomStats"];
renderSidebars: () => JSX.Element | null; renderSidebars: () => JSX.Element | null;
device: Device; device: Device;
renderWelcomeScreen?: boolean; renderWelcomeScreen?: boolean;
UIOptions: AppProps["UIOptions"];
}; };
export const MobileMenu = ({ export const MobileMenu = ({
@@ -68,12 +66,10 @@ export const MobileMenu = ({
renderCustomFooter, renderCustomFooter,
onImageAction, onImageAction,
renderTopRightUI, renderTopRightUI,
renderMenuLinks,
renderCustomStats, renderCustomStats,
renderSidebars, renderSidebars,
device, device,
renderWelcomeScreen, renderWelcomeScreen,
UIOptions,
}: MobileMenuProps) => { }: MobileMenuProps) => {
const renderToolbar = () => { const renderToolbar = () => {
return ( return (
@@ -115,8 +111,8 @@ export const MobileMenu = ({
/> />
</Stack.Row> </Stack.Row>
</Island> </Island>
{renderTopRightUI && renderTopRightUI(true, appState)}
<div className="mobile-misc-tools-container"> <div className="mobile-misc-tools-container">
{renderTopRightUI && renderTopRightUI(true, appState)}
<PenModeButton <PenModeButton
checked={appState.penMode} checked={appState.penMode}
onChange={onPenModeToggle} onChange={onPenModeToggle}
@@ -196,14 +192,12 @@ export const MobileMenu = ({
{!appState.viewModeEnabled && actionManager.renderAction("loadScene")} {!appState.viewModeEnabled && actionManager.renderAction("loadScene")}
{renderJSONExportDialog()} {renderJSONExportDialog()}
{renderImageExportDialog()} {renderImageExportDialog()}
{UIOptions.canvasActions.saveAsImage && ( <MenuItem
<MenuItem label={t("buttons.exportImage")}
label={t("buttons.exportImage")} icon={ExportImageIcon}
icon={ExportImageIcon} dataTestId="image-export-button"
dataTestId="image-export-button" onClick={() => setAppState({ openDialog: "imageExport" })}
onClick={() => setAppState({ openDialog: "imageExport" })} />
/>
)}
{onCollabButtonClick && ( {onCollabButtonClick && (
<CollabButton <CollabButton
isCollaborating={isCollaborating} isCollaborating={isCollaborating}
@@ -213,16 +207,8 @@ export const MobileMenu = ({
)} )}
{actionManager.renderAction("toggleShortcuts", undefined, true)} {actionManager.renderAction("toggleShortcuts", undefined, true)}
{!appState.viewModeEnabled && actionManager.renderAction("clearCanvas")} {!appState.viewModeEnabled && actionManager.renderAction("clearCanvas")}
{typeof renderMenuLinks === "undefined" ? ( //zsviczian <Separator />
<Separator /> <MenuLinks />
) : (
renderMenuLinks && <Separator />
)}
{typeof renderMenuLinks === "undefined" ? ( //zsviczian
<MenuLinks />
) : (
renderMenuLinks && renderMenuLinks(device.isMobile, appState)
)}
<Separator /> <Separator />
{!appState.viewModeEnabled && ( {!appState.viewModeEnabled && (
<div style={{ marginBottom: ".5rem" }}> <div style={{ marginBottom: ".5rem" }}>
+2 -2
View File
@@ -90,10 +90,10 @@ describe("Sidebar", () => {
const sidebar = container.querySelector<HTMLElement>(".test-sidebar"); const sidebar = container.querySelector<HTMLElement>(".test-sidebar");
expect(sidebar).not.toBe(null); expect(sidebar).not.toBe(null);
const closeButton = queryByTestId(sidebar!, "sidebar-close")!; const closeButton = queryByTestId(sidebar!, "sidebar-close");
expect(closeButton).not.toBe(null); expect(closeButton).not.toBe(null);
fireEvent.click(closeButton); fireEvent.click(closeButton!.querySelector("button")!);
await waitFor(() => { await waitFor(() => {
expect(container.querySelector<HTMLElement>(".test-sidebar")).toBe(null); expect(container.querySelector<HTMLElement>(".test-sidebar")).toBe(null);
expect(onClose).toHaveBeenCalled(); expect(onClose).toHaveBeenCalled();
+1 -1
View File
@@ -492,7 +492,7 @@ export const getElementBounds = (
export const getCommonBounds = ( export const getCommonBounds = (
elements: readonly ExcalidrawElement[], elements: readonly ExcalidrawElement[],
): [number, number, number, number] => { ): [minX: number, minY: number, maxX: number, maxY: number] => {
if (!elements.length) { if (!elements.length) {
return [0, 0, 0, 0]; return [0, 0, 0, 0];
} }
+1 -23
View File
@@ -392,8 +392,6 @@ No, Excalidraw package doesn't come with collaboration built in, since the imple
| [`onPointerUpdate`](#onPointerUpdate) | Function | | Callback triggered when mouse pointer is updated. | | [`onPointerUpdate`](#onPointerUpdate) | Function | | Callback triggered when mouse pointer is updated. |
| [`langCode`](#langCode) | string | `en` | Language code string | | [`langCode`](#langCode) | string | `en` | Language code string |
| [`renderTopRightUI`](#renderTopRightUI) | Function | | Function that renders custom UI in top right corner | | [`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 | | [`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. | | [`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. | | [`renderSIdebar`](#renderSIdebar) | Function | | Render function that renders custom sidebar. |
@@ -615,22 +613,6 @@ import { defaultLang, languages } from "@excalidraw/excalidraw";
A function returning JSX to render custom UI in the top right corner of the app. 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` #### `renderFooter`
<pre> <pre>
@@ -703,7 +685,7 @@ This prop sets the name of the drawing which will be used when exporting the dra
#### `UIOptions` #### `UIOptions`
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 This prop can be used to customise UI of Excalidraw. Currently we support customising [`canvasActions`](#canvasActions) and [`dockedSidebarBreakpoint`](dockedSidebarBreakpoint). It accepts the below parameters
<pre> <pre>
{ canvasActions: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L208"> CanvasActions<a/> } { canvasActions: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L208"> CanvasActions<a/> }
@@ -727,10 +709,6 @@ This prop indicates at what point should we break to a docked, permanent sidebar
![image](https://user-images.githubusercontent.com/11256141/174664866-c698c3fa-197b-43ff-956c-d79852c7b326.png) ![image](https://user-images.githubusercontent.com/11256141/174664866-c698c3fa-197b-43ff-956c-d79852c7b326.png)
### `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` #### `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. 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.
-4
View File
@@ -20,8 +20,6 @@ const ExcalidrawBase = (props: ExcalidrawProps) => {
isCollaborating = false, isCollaborating = false,
onPointerUpdate, onPointerUpdate,
renderTopRightUI, renderTopRightUI,
hideWelcomeScreen,
renderMenuLinks,
renderFooter, renderFooter,
renderSidebar, renderSidebar,
langCode = defaultLang.code, langCode = defaultLang.code,
@@ -95,8 +93,6 @@ const ExcalidrawBase = (props: ExcalidrawProps) => {
isCollaborating={isCollaborating} isCollaborating={isCollaborating}
onPointerUpdate={onPointerUpdate} onPointerUpdate={onPointerUpdate}
renderTopRightUI={renderTopRightUI} renderTopRightUI={renderTopRightUI}
hideWelcomeScreen={hideWelcomeScreen}
renderMenuLinks={renderMenuLinks}
renderFooter={renderFooter} renderFooter={renderFooter}
langCode={langCode} langCode={langCode}
viewModeEnabled={viewModeEnabled} viewModeEnabled={viewModeEnabled}
+157 -11
View File
@@ -14,6 +14,102 @@ import {
export const SVG_EXPORT_TAG = `<!-- svg-source:excalidraw -->`; 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 ( export const exportToCanvas = async (
elements: readonly NonDeletedExcalidrawElement[], elements: readonly NonDeletedExcalidrawElement[],
appState: AppState, appState: AppState,
@@ -37,7 +133,12 @@ export const exportToCanvas = async (
return { canvas, scale: appState.exportScale }; return { canvas, scale: appState.exportScale };
}, },
) => { ) => {
const [minX, minY, width, height] = getCanvasSize(elements, exportPadding); const [scrollX, scrollY, width, height] = await getCanvasSize(
elements,
appState,
files,
exportPadding,
);
const { canvas, scale = 1 } = createCanvas(width, height); const { canvas, scale = 1 } = createCanvas(width, height);
@@ -59,8 +160,8 @@ export const exportToCanvas = async (
canvas, canvas,
renderConfig: { renderConfig: {
viewBackgroundColor: exportBackground ? viewBackgroundColor : null, viewBackgroundColor: exportBackground ? viewBackgroundColor : null,
scrollX: -minX + exportPadding, scrollX,
scrollY: -minY + exportPadding, scrollY,
zoom: defaultAppState.zoom, zoom: defaultAppState.zoom,
remotePointerViewportCoords: {}, remotePointerViewportCoords: {},
remoteSelectedElementIds: {}, remoteSelectedElementIds: {},
@@ -109,7 +210,12 @@ export const exportToSvg = async (
console.error(error); console.error(error);
} }
} }
const [minX, minY, width, height] = getCanvasSize(elements, exportPadding); const [minX, minY, width, height] = await getCanvasSize(
elements,
appState,
files || {},
exportPadding,
);
// initialize SVG root // initialize SVG root
const svgRoot = document.createElementNS(SVG_NS, "svg"); const svgRoot = document.createElementNS(SVG_NS, "svg");
@@ -172,26 +278,66 @@ export const exportToSvg = async (
return svgRoot; return svgRoot;
}; };
// calculate smallest area to fit the contents in const getApproximateCanvasSize = (
const getCanvasSize = (
elements: readonly NonDeletedExcalidrawElement[], elements: readonly NonDeletedExcalidrawElement[],
exportPadding: number, exportPadding: number,
): [number, number, number, number] => { ): [number, number, number, number] => {
const [minX, minY, maxX, maxY] = getCommonBounds(elements); 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 width = distance(minX, maxX) + exportPadding * 2; const width = distance(minX, maxX) + exportPadding * 2;
const height = distance(minY, maxY) + exportPadding + exportPadding; const height =
Math.ceil(distance(minY, maxY)) + exportPadding + exportPadding;
return [minX, minY, width, height]; 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 = ( export const getExportSize = (
elements: readonly NonDeletedExcalidrawElement[], elements: readonly NonDeletedExcalidrawElement[],
exportPadding: number, exportPadding: number,
scale: number, scale: number,
): [number, number] => { ): [number, number] => {
const [, , width, height] = getCanvasSize(elements, exportPadding).map( const [, , width, height] = getApproximateCanvasSize(
(dimension) => Math.trunc(dimension * scale), elements,
); exportPadding,
).map((dimension) => Math.trunc(dimension * scale));
return [width, height]; return [width, height];
}; };
-2
View File
@@ -3,8 +3,6 @@ import "jest-canvas-mock";
import dotenv from "dotenv"; import dotenv from "dotenv";
import polyfill from "./polyfill"; import polyfill from "./polyfill";
require("fake-indexeddb/auto");
polyfill(); polyfill();
// jest doesn't know of .env.development so we need to init it ourselves // jest doesn't know of .env.development so we need to init it ourselves
dotenv.config({ dotenv.config({
@@ -1951,9 +1951,7 @@ Object {
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"pendingImageElementId": null, "pendingImageElementId": null,
"previousSelectedElementIds": Object { "previousSelectedElementIds": Object {},
"id0": true,
},
"resizingElement": null, "resizingElement": null,
"scrollX": 0, "scrollX": 0,
"scrollY": 0, "scrollY": 0,
@@ -1990,7 +1988,7 @@ Object {
"boundElements": null, "boundElements": null,
"fillStyle": "hachure", "fillStyle": "hachure",
"groupIds": Array [], "groupIds": Array [],
"height": 10, "height": 15,
"id": "id0", "id": "id0",
"isDeleted": false, "isDeleted": false,
"link": null, "link": null,
@@ -2006,9 +2004,9 @@ Object {
"updated": 1, "updated": 1,
"version": 3, "version": 3,
"versionNonce": 453191, "versionNonce": 453191,
"width": 10, "width": 15,
"x": 25, "x": 10,
"y": 25, "y": 10,
} }
`; `;
@@ -2087,7 +2085,7 @@ Object {
"boundElements": null, "boundElements": null,
"fillStyle": "hachure", "fillStyle": "hachure",
"groupIds": Array [], "groupIds": Array [],
"height": 10, "height": 15,
"id": "id0", "id": "id0",
"isDeleted": false, "isDeleted": false,
"link": null, "link": null,
@@ -2103,9 +2101,9 @@ Object {
"updated": 1, "updated": 1,
"version": 3, "version": 3,
"versionNonce": 453191, "versionNonce": 453191,
"width": 10, "width": 15,
"x": 25, "x": 10,
"y": 25, "y": 10,
}, },
], ],
}, },
@@ -2684,9 +2682,7 @@ Object {
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"pendingImageElementId": null, "pendingImageElementId": null,
"previousSelectedElementIds": Object { "previousSelectedElementIds": Object {},
"id0": true,
},
"resizingElement": null, "resizingElement": null,
"scrollX": 0, "scrollX": 0,
"scrollY": 0, "scrollY": 0,
@@ -2717,35 +2713,6 @@ Object {
`; `;
exports[`regression tests alt-drag duplicates an element: [end of test] element 0 1`] = ` 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 { Object {
"angle": 0, "angle": 0,
"backgroundColor": "transparent", "backgroundColor": "transparent",
@@ -2766,11 +2733,11 @@ Object {
"strokeWidth": 1, "strokeWidth": 1,
"type": "rectangle", "type": "rectangle",
"updated": 1, "updated": 1,
"version": 3, "version": 2,
"versionNonce": 453191, "versionNonce": 1278240551,
"width": 10, "width": 10,
"x": 20, "x": 10,
"y": 20, "y": 10,
} }
`; `;
@@ -2830,78 +2797,11 @@ 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`] = `2`; 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 renders 1`] = `12`; exports[`regression tests alt-drag duplicates an element: [end of test] number of renders 1`] = `12`;
@@ -3837,230 +3737,6 @@ 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 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`] = ` exports[`regression tests click on an element and drag it: [end of test] appState 1`] = `
Object { Object {
"activeTool": Object { "activeTool": Object {
@@ -4119,10 +3795,7 @@ Object {
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"pendingImageElementId": null, "pendingImageElementId": null,
"previousSelectedElementIds": Object { "previousSelectedElementIds": Object {},
"id0": true,
"id1": true,
},
"resizingElement": null, "resizingElement": null,
"scrollX": 0, "scrollX": 0,
"scrollY": 0, "scrollY": 0,
@@ -4130,7 +3803,6 @@ Object {
"selectedElementIds": Object { "selectedElementIds": Object {
"id0": true, "id0": true,
"id1": true, "id1": true,
"id2": true,
}, },
"selectedGroupIds": Object {}, "selectedGroupIds": Object {},
"selectedLinearElement": null, "selectedLinearElement": null,
@@ -4174,8 +3846,8 @@ Object {
"strokeWidth": 1, "strokeWidth": 1,
"type": "rectangle", "type": "rectangle",
"updated": 1, "updated": 1,
"version": 4, "version": 2,
"versionNonce": 2019559783, "versionNonce": 1278240551,
"width": 10, "width": 10,
"x": 10, "x": 10,
"y": 10, "y": 10,
@@ -4238,96 +3910,13 @@ 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 elements 1`] = `1`;
exports[`regression tests click on an element and drag it: [end of test] number of renders 1`] = `15`; exports[`regression tests click on an element and drag it: [end of test] number of renders 1`] = `12`;
exports[`regression tests click to select a shape: [end of test] appState 1`] = ` exports[`regression tests click to select a shape: [end of test] appState 1`] = `
Object { Object {
@@ -5924,14 +5513,13 @@ Object {
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"pendingImageElementId": null, "pendingImageElementId": null,
"previousSelectedElementIds": Object { "previousSelectedElementIds": Object {},
"id0": true,
},
"resizingElement": null, "resizingElement": null,
"scrollX": 0, "scrollX": 0,
"scrollY": 0, "scrollY": 0,
"scrolledOutside": false, "scrolledOutside": false,
"selectedElementIds": Object { "selectedElementIds": Object {
"id0": true,
"id1": true, "id1": true,
}, },
"selectedGroupIds": Object {}, "selectedGroupIds": Object {},
@@ -14112,7 +13700,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 elements 1`] = `0`;
exports[`regression tests rerenders UI on language change: [end of test] number of renders 1`] = `10`; exports[`regression tests rerenders UI on language change: [end of test] number of renders 1`] = `6`;
exports[`regression tests shift click on selected element should deselect it on pointer up: [end of test] appState 1`] = ` exports[`regression tests shift click on selected element should deselect it on pointer up: [end of test] appState 1`] = `
Object { Object {
@@ -14172,15 +13760,13 @@ Object {
"penDetected": false, "penDetected": false,
"penMode": false, "penMode": false,
"pendingImageElementId": null, "pendingImageElementId": null,
"previousSelectedElementIds": Object { "previousSelectedElementIds": Object {},
"id0": true,
},
"resizingElement": null, "resizingElement": null,
"scrollX": 0, "scrollX": 0,
"scrollY": 0, "scrollY": 0,
"scrolledOutside": false, "scrolledOutside": false,
"selectedElementIds": Object { "selectedElementIds": Object {
"id0": false, "id0": true,
"id1": true, "id1": true,
}, },
"selectedGroupIds": Object {}, "selectedGroupIds": Object {},
+10 -10
View File
@@ -42,7 +42,7 @@ describe("Test dragCreate", () => {
// finish (position does not matter) // finish (position does not matter)
fireEvent.pointerUp(canvas); fireEvent.pointerUp(canvas);
expect(renderScene).toHaveBeenCalledTimes(9); expect(renderScene).toHaveBeenCalledTimes(8);
expect(h.state.selectionElement).toBeNull(); expect(h.state.selectionElement).toBeNull();
expect(h.elements.length).toEqual(1); expect(h.elements.length).toEqual(1);
@@ -73,7 +73,7 @@ describe("Test dragCreate", () => {
// finish (position does not matter) // finish (position does not matter)
fireEvent.pointerUp(canvas); fireEvent.pointerUp(canvas);
expect(renderScene).toHaveBeenCalledTimes(9); expect(renderScene).toHaveBeenCalledTimes(8);
expect(h.state.selectionElement).toBeNull(); expect(h.state.selectionElement).toBeNull();
expect(h.elements.length).toEqual(1); expect(h.elements.length).toEqual(1);
@@ -104,7 +104,7 @@ describe("Test dragCreate", () => {
// finish (position does not matter) // finish (position does not matter)
fireEvent.pointerUp(canvas); fireEvent.pointerUp(canvas);
expect(renderScene).toHaveBeenCalledTimes(9); expect(renderScene).toHaveBeenCalledTimes(8);
expect(h.state.selectionElement).toBeNull(); expect(h.state.selectionElement).toBeNull();
expect(h.elements.length).toEqual(1); expect(h.elements.length).toEqual(1);
@@ -135,7 +135,7 @@ describe("Test dragCreate", () => {
// finish (position does not matter) // finish (position does not matter)
fireEvent.pointerUp(canvas); fireEvent.pointerUp(canvas);
expect(renderScene).toHaveBeenCalledTimes(9); expect(renderScene).toHaveBeenCalledTimes(8);
expect(h.state.selectionElement).toBeNull(); expect(h.state.selectionElement).toBeNull();
expect(h.elements.length).toEqual(1); expect(h.elements.length).toEqual(1);
@@ -170,7 +170,7 @@ describe("Test dragCreate", () => {
// finish (position does not matter) // finish (position does not matter)
fireEvent.pointerUp(canvas); fireEvent.pointerUp(canvas);
expect(renderScene).toHaveBeenCalledTimes(9); expect(renderScene).toHaveBeenCalledTimes(8);
expect(h.state.selectionElement).toBeNull(); expect(h.state.selectionElement).toBeNull();
expect(h.elements.length).toEqual(1); expect(h.elements.length).toEqual(1);
@@ -210,7 +210,7 @@ describe("Test dragCreate", () => {
// finish (position does not matter) // finish (position does not matter)
fireEvent.pointerUp(canvas); fireEvent.pointerUp(canvas);
expect(renderScene).toHaveBeenCalledTimes(7); expect(renderScene).toHaveBeenCalledTimes(6);
expect(h.state.selectionElement).toBeNull(); expect(h.state.selectionElement).toBeNull();
expect(h.elements.length).toEqual(0); expect(h.elements.length).toEqual(0);
}); });
@@ -229,7 +229,7 @@ describe("Test dragCreate", () => {
// finish (position does not matter) // finish (position does not matter)
fireEvent.pointerUp(canvas); fireEvent.pointerUp(canvas);
expect(renderScene).toHaveBeenCalledTimes(7); expect(renderScene).toHaveBeenCalledTimes(6);
expect(h.state.selectionElement).toBeNull(); expect(h.state.selectionElement).toBeNull();
expect(h.elements.length).toEqual(0); expect(h.elements.length).toEqual(0);
}); });
@@ -248,7 +248,7 @@ describe("Test dragCreate", () => {
// finish (position does not matter) // finish (position does not matter)
fireEvent.pointerUp(canvas); fireEvent.pointerUp(canvas);
expect(renderScene).toHaveBeenCalledTimes(7); expect(renderScene).toHaveBeenCalledTimes(6);
expect(h.state.selectionElement).toBeNull(); expect(h.state.selectionElement).toBeNull();
expect(h.elements.length).toEqual(0); expect(h.elements.length).toEqual(0);
}); });
@@ -272,7 +272,7 @@ describe("Test dragCreate", () => {
key: KEYS.ENTER, key: KEYS.ENTER,
}); });
expect(renderScene).toHaveBeenCalledTimes(8); expect(renderScene).toHaveBeenCalledTimes(7);
expect(h.state.selectionElement).toBeNull(); expect(h.state.selectionElement).toBeNull();
expect(h.elements.length).toEqual(0); expect(h.elements.length).toEqual(0);
}); });
@@ -296,7 +296,7 @@ describe("Test dragCreate", () => {
key: KEYS.ENTER, key: KEYS.ENTER,
}); });
expect(renderScene).toHaveBeenCalledTimes(8); expect(renderScene).toHaveBeenCalledTimes(7);
expect(h.state.selectionElement).toBeNull(); expect(h.state.selectionElement).toBeNull();
expect(h.elements.length).toEqual(0); expect(h.elements.length).toEqual(0);
}); });
+7 -5
View File
@@ -1,6 +1,4 @@
import { fireEvent, render, waitFor } from "./test-utils"; import { fireEvent, render, waitFor } from "./test-utils";
import { queryByTestId } from "@testing-library/react";
import ExcalidrawApp from "../excalidraw-app"; import ExcalidrawApp from "../excalidraw-app";
import { API } from "./helpers/api"; import { API } from "./helpers/api";
import { MIME_TYPES } from "../constants"; import { MIME_TYPES } from "../constants";
@@ -95,11 +93,15 @@ describe("library menu", () => {
const latestLibrary = await h.app.library.getLatestLibrary(); const latestLibrary = await h.app.library.getLatestLibrary();
expect(latestLibrary.length).toBe(0); expect(latestLibrary.length).toBe(0);
const libraryButton = container.querySelector(".library-button"); const libraryButton = container.querySelector(".ToolIcon__library");
fireEvent.click(libraryButton!); fireEvent.click(libraryButton!);
fireEvent.click(container.querySelector(".Sidebar__dropdown-btn")!);
queryByTestId(container, "lib-dropdown--load")!.click(); const loadLibraryButton = container.querySelector(
".library-actions .library-actions--load",
);
fireEvent.click(loadLibraryButton!);
const libraryItems = parseLibraryJSON(await libraryJSONPromise); const libraryItems = parseLibraryJSON(await libraryJSONPromise);
+3 -3
View File
@@ -38,7 +38,7 @@ describe("move element", () => {
fireEvent.pointerMove(canvas, { clientX: 60, clientY: 70 }); fireEvent.pointerMove(canvas, { clientX: 60, clientY: 70 });
fireEvent.pointerUp(canvas); fireEvent.pointerUp(canvas);
expect(renderScene).toHaveBeenCalledTimes(9); expect(renderScene).toHaveBeenCalledTimes(8);
expect(h.state.selectionElement).toBeNull(); expect(h.state.selectionElement).toBeNull();
expect(h.elements.length).toEqual(1); expect(h.elements.length).toEqual(1);
expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy(); expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy();
@@ -77,7 +77,7 @@ describe("move element", () => {
// select the second rectangles // select the second rectangles
new Pointer("mouse").clickOn(rectB); new Pointer("mouse").clickOn(rectB);
expect(renderScene).toHaveBeenCalledTimes(23); expect(renderScene).toHaveBeenCalledTimes(22);
expect(h.state.selectionElement).toBeNull(); expect(h.state.selectionElement).toBeNull();
expect(h.elements.length).toEqual(3); expect(h.elements.length).toEqual(3);
expect(h.state.selectedElementIds[rectB.id]).toBeTruthy(); 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.pointerMove(canvas, { clientX: 60, clientY: 70 });
fireEvent.pointerUp(canvas); fireEvent.pointerUp(canvas);
expect(renderScene).toHaveBeenCalledTimes(9); expect(renderScene).toHaveBeenCalledTimes(8);
expect(h.state.selectionElement).toBeNull(); expect(h.state.selectionElement).toBeNull();
expect(h.elements.length).toEqual(1); expect(h.elements.length).toEqual(1);
expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy(); expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy();
+5 -5
View File
@@ -42,7 +42,7 @@ describe("remove shape in non linear elements", () => {
fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 }); fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 });
fireEvent.pointerUp(canvas, { clientX: 30, clientY: 30 }); fireEvent.pointerUp(canvas, { clientX: 30, clientY: 30 });
expect(renderScene).toHaveBeenCalledTimes(7); expect(renderScene).toHaveBeenCalledTimes(6);
expect(h.elements.length).toEqual(0); 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.pointerDown(canvas, { clientX: 30, clientY: 20 });
fireEvent.pointerUp(canvas, { clientX: 30, clientY: 30 }); fireEvent.pointerUp(canvas, { clientX: 30, clientY: 30 });
expect(renderScene).toHaveBeenCalledTimes(7); expect(renderScene).toHaveBeenCalledTimes(6);
expect(h.elements.length).toEqual(0); 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.pointerDown(canvas, { clientX: 30, clientY: 20 });
fireEvent.pointerUp(canvas, { clientX: 30, clientY: 30 }); fireEvent.pointerUp(canvas, { clientX: 30, clientY: 30 });
expect(renderScene).toHaveBeenCalledTimes(7); expect(renderScene).toHaveBeenCalledTimes(6);
expect(h.elements.length).toEqual(0); expect(h.elements.length).toEqual(0);
}); });
}); });
@@ -102,7 +102,7 @@ describe("multi point mode in linear elements", () => {
key: KEYS.ENTER, key: KEYS.ENTER,
}); });
expect(renderScene).toHaveBeenCalledTimes(15); expect(renderScene).toHaveBeenCalledTimes(14);
expect(h.elements.length).toEqual(1); expect(h.elements.length).toEqual(1);
const element = h.elements[0] as ExcalidrawLinearElement; const element = h.elements[0] as ExcalidrawLinearElement;
@@ -145,7 +145,7 @@ describe("multi point mode in linear elements", () => {
key: KEYS.ENTER, key: KEYS.ENTER,
}); });
expect(renderScene).toHaveBeenCalledTimes(15); expect(renderScene).toHaveBeenCalledTimes(14);
expect(h.elements.length).toEqual(1); expect(h.elements.length).toEqual(1);
const element = h.elements[0] as ExcalidrawLinearElement; const element = h.elements[0] as ExcalidrawLinearElement;
File diff suppressed because it is too large Load Diff
-3
View File
@@ -90,10 +90,7 @@ describe("<Excalidraw/>", () => {
describe("Test theme prop", () => { describe("Test theme prop", () => {
it("should show the theme toggle by default", async () => { it("should show the theme toggle by default", async () => {
const { container } = await render(<Excalidraw />); const { container } = await render(<Excalidraw />);
expect(h.state.theme).toBe(THEME.LIGHT); expect(h.state.theme).toBe(THEME.LIGHT);
queryByTestId(container, "menu-button")!.click();
const darkModeToggle = queryByTestId(container, "toggle-dark-mode"); const darkModeToggle = queryByTestId(container, "toggle-dark-mode");
expect(darkModeToggle).toBeTruthy(); expect(darkModeToggle).toBeTruthy();
}); });
-1
View File
@@ -10,7 +10,6 @@ const toolMap = {
line: "line", line: "line",
freedraw: "freedraw", freedraw: "freedraw",
text: "text", text: "text",
eraser: "eraser",
}; };
export type ToolName = keyof typeof toolMap; export type ToolName = keyof typeof toolMap;
+6 -9
View File
@@ -174,7 +174,7 @@ describe("regression tests", () => {
mouse.up(10, 10); mouse.up(10, 10);
const { x: prevX, y: prevY } = API.getSelectedElement(); const { x: prevX, y: prevY } = API.getSelectedElement();
mouse.down(-8, -8); mouse.down(-10, -10);
mouse.up(10, 10); mouse.up(10, 10);
const { x: nextX, y: nextY } = API.getSelectedElement(); const { x: nextX, y: nextY } = API.getSelectedElement();
@@ -201,7 +201,7 @@ describe("regression tests", () => {
).toBe(1); ).toBe(1);
Keyboard.withModifierKeys({ alt: true }, () => { Keyboard.withModifierKeys({ alt: true }, () => {
mouse.down(-8, -8); mouse.down(-10, -10);
mouse.up(10, 10); mouse.up(10, 10);
}); });
@@ -446,8 +446,6 @@ describe("regression tests", () => {
UI.clickTool("rectangle"); UI.clickTool("rectangle");
// english lang should display `thin` label // english lang should display `thin` label
expect(screen.queryByTitle(/thin/i)).not.toBeNull(); expect(screen.queryByTitle(/thin/i)).not.toBeNull();
fireEvent.click(document.querySelector(".menu-button")!);
fireEvent.change(document.querySelector(".dropdown-select__language")!, { fireEvent.change(document.querySelector(".dropdown-select__language")!, {
target: { value: "de-DE" }, target: { value: "de-DE" },
}); });
@@ -674,10 +672,9 @@ describe("regression tests", () => {
mouse.down(); mouse.down();
mouse.up(100, 100); mouse.up(100, 100);
expect(API.getSelectedElements().length).toBe(1);
// hits bounding box without hitting element // hits bounding box without hitting element
mouse.down(98, 98); mouse.down();
expect(API.getSelectedElements().length).toBe(1);
mouse.up(); mouse.up();
expect(API.getSelectedElements().length).toBe(0); expect(API.getSelectedElements().length).toBe(0);
}); });
@@ -747,7 +744,7 @@ describe("regression tests", () => {
// drag element from point on bounding box that doesn't hit element // drag element from point on bounding box that doesn't hit element
mouse.reset(); mouse.reset();
mouse.down(8, 8); mouse.down();
mouse.up(25, 25); mouse.up(25, 25);
expect(API.getSelectedElement().x).toEqual(prevX + 25); expect(API.getSelectedElement().x).toEqual(prevX + 25);
@@ -1023,7 +1020,7 @@ describe("regression tests", () => {
// Rectangle is already selected since creating // Rectangle is already selected since creating
// it was our last action // it was our last action
Keyboard.withModifierKeys({ shift: true }, () => { Keyboard.withModifierKeys({ shift: true }, () => {
mouse.down(-8, -8); mouse.down();
}); });
expect(API.getSelectedElements().length).toBe(1); expect(API.getSelectedElements().length).toBe(1);
+8 -8
View File
@@ -154,7 +154,7 @@ describe("selection element", () => {
const canvas = container.querySelector("canvas")!; const canvas = container.querySelector("canvas")!;
fireEvent.pointerDown(canvas, { clientX: 60, clientY: 100 }); fireEvent.pointerDown(canvas, { clientX: 60, clientY: 100 });
expect(renderScene).toHaveBeenCalledTimes(5); expect(renderScene).toHaveBeenCalledTimes(4);
const selectionElement = h.state.selectionElement!; const selectionElement = h.state.selectionElement!;
expect(selectionElement).not.toBeNull(); expect(selectionElement).not.toBeNull();
expect(selectionElement.type).toEqual("selection"); expect(selectionElement.type).toEqual("selection");
@@ -175,7 +175,7 @@ describe("selection element", () => {
fireEvent.pointerDown(canvas, { clientX: 60, clientY: 100 }); fireEvent.pointerDown(canvas, { clientX: 60, clientY: 100 });
fireEvent.pointerMove(canvas, { clientX: 150, clientY: 30 }); fireEvent.pointerMove(canvas, { clientX: 150, clientY: 30 });
expect(renderScene).toHaveBeenCalledTimes(6); expect(renderScene).toHaveBeenCalledTimes(5);
const selectionElement = h.state.selectionElement!; const selectionElement = h.state.selectionElement!;
expect(selectionElement).not.toBeNull(); expect(selectionElement).not.toBeNull();
expect(selectionElement.type).toEqual("selection"); expect(selectionElement.type).toEqual("selection");
@@ -197,7 +197,7 @@ describe("selection element", () => {
fireEvent.pointerMove(canvas, { clientX: 150, clientY: 30 }); fireEvent.pointerMove(canvas, { clientX: 150, clientY: 30 });
fireEvent.pointerUp(canvas); fireEvent.pointerUp(canvas);
expect(renderScene).toHaveBeenCalledTimes(7); expect(renderScene).toHaveBeenCalledTimes(6);
expect(h.state.selectionElement).toBeNull(); expect(h.state.selectionElement).toBeNull();
}); });
}); });
@@ -232,7 +232,7 @@ describe("select single element on the scene", () => {
fireEvent.pointerDown(canvas, { clientX: 45, clientY: 20 }); fireEvent.pointerDown(canvas, { clientX: 45, clientY: 20 });
fireEvent.pointerUp(canvas); fireEvent.pointerUp(canvas);
expect(renderScene).toHaveBeenCalledTimes(11); expect(renderScene).toHaveBeenCalledTimes(10);
expect(h.state.selectionElement).toBeNull(); expect(h.state.selectionElement).toBeNull();
expect(h.elements.length).toEqual(1); expect(h.elements.length).toEqual(1);
expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy(); 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.pointerDown(canvas, { clientX: 45, clientY: 20 });
fireEvent.pointerUp(canvas); fireEvent.pointerUp(canvas);
expect(renderScene).toHaveBeenCalledTimes(11); expect(renderScene).toHaveBeenCalledTimes(10);
expect(h.state.selectionElement).toBeNull(); expect(h.state.selectionElement).toBeNull();
expect(h.elements.length).toEqual(1); expect(h.elements.length).toEqual(1);
expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy(); 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.pointerDown(canvas, { clientX: 45, clientY: 20 });
fireEvent.pointerUp(canvas); fireEvent.pointerUp(canvas);
expect(renderScene).toHaveBeenCalledTimes(11); expect(renderScene).toHaveBeenCalledTimes(10);
expect(h.state.selectionElement).toBeNull(); expect(h.state.selectionElement).toBeNull();
expect(h.elements.length).toEqual(1); expect(h.elements.length).toEqual(1);
expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy(); 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.pointerDown(canvas, { clientX: 40, clientY: 40 });
fireEvent.pointerUp(canvas); fireEvent.pointerUp(canvas);
expect(renderScene).toHaveBeenCalledTimes(11); expect(renderScene).toHaveBeenCalledTimes(10);
expect(h.state.selectionElement).toBeNull(); expect(h.state.selectionElement).toBeNull();
expect(h.elements.length).toEqual(1); expect(h.elements.length).toEqual(1);
expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy(); 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.pointerDown(canvas, { clientX: 40, clientY: 40 });
fireEvent.pointerUp(canvas); fireEvent.pointerUp(canvas);
expect(renderScene).toHaveBeenCalledTimes(11); expect(renderScene).toHaveBeenCalledTimes(10);
expect(h.state.selectionElement).toBeNull(); expect(h.state.selectionElement).toBeNull();
expect(h.elements.length).toEqual(1); expect(h.elements.length).toEqual(1);
expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy(); expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy();
+2
View File
@@ -16,6 +16,8 @@ import { SceneData } from "../types";
import { getSelectedElements } from "../scene/selection"; import { getSelectedElements } from "../scene/selection";
import { ExcalidrawElement } from "../element/types"; import { ExcalidrawElement } from "../element/types";
require("fake-indexeddb/auto");
const customQueries = { const customQueries = {
...queries, ...queries,
...toolQueries, ...toolQueries,
+1 -6
View File
@@ -113,6 +113,7 @@ export type AppState = {
exportEmbedScene: boolean; exportEmbedScene: boolean;
exportWithDarkMode: boolean; exportWithDarkMode: boolean;
exportScale: number; exportScale: number;
exportPadding: number;
currentItemStrokeColor: string; currentItemStrokeColor: string;
currentItemBackgroundColor: string; currentItemBackgroundColor: string;
currentItemFillStyle: ExcalidrawElement["fillStyle"]; currentItemFillStyle: ExcalidrawElement["fillStyle"];
@@ -284,10 +285,6 @@ export interface ExcalidrawProps {
isMobile: boolean, isMobile: boolean,
appState: AppState, appState: AppState,
) => JSX.Element | null; ) => JSX.Element | null;
renderMenuLinks?:
| ((isMobile: boolean, appState: AppState) => JSX.Element | null)
| null;
hideWelcomeScreen?: boolean;
renderFooter?: (isMobile: boolean, appState: AppState) => JSX.Element | null; renderFooter?: (isMobile: boolean, appState: AppState) => JSX.Element | null;
langCode?: Language["code"]; langCode?: Language["code"];
viewModeEnabled?: boolean; viewModeEnabled?: boolean;
@@ -303,7 +300,6 @@ export interface ExcalidrawProps {
UIOptions?: { UIOptions?: {
dockedSidebarBreakpoint?: number; dockedSidebarBreakpoint?: number;
canvasActions?: CanvasActions; canvasActions?: CanvasActions;
showLanguageList?: boolean;
}; };
detectScroll?: boolean; detectScroll?: boolean;
handleKeyboardGlobally?: boolean; handleKeyboardGlobally?: boolean;
@@ -376,7 +372,6 @@ export type AppProps = Merge<
UIOptions: { UIOptions: {
canvasActions: Required<CanvasActions> & { export: ExportOpts }; canvasActions: Required<CanvasActions> & { export: ExportOpts };
dockedSidebarBreakpoint?: number; dockedSidebarBreakpoint?: number;
showLanguageList?: boolean;
}; };
detectScroll: boolean; detectScroll: boolean;
handleKeyboardGlobally: boolean; handleKeyboardGlobally: boolean;