mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-10-24 00:14:25 +02:00
Compare commits
2 Commits
fix-zsvicz
...
dwelle/exp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fa9631617f | ||
|
|
0314e81396 |
@@ -1,6 +1,5 @@
|
||||
*
|
||||
!.env.development
|
||||
!.env.production
|
||||
!.env
|
||||
!.eslintrc.json
|
||||
!.npmrc
|
||||
!.prettierrc
|
||||
|
||||
@@ -20,5 +20,3 @@ REACT_APP_DEV_ENABLE_SW=
|
||||
# whether to disable live reload / HMR. Usuaully what you want to do when
|
||||
# debugging Service Workers.
|
||||
REACT_APP_DEV_DISABLE_LIVE_RELOAD=
|
||||
|
||||
FAST_REFRESH=false
|
||||
|
||||
@@ -4755,9 +4755,9 @@ loader-runner@^4.2.0:
|
||||
integrity sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==
|
||||
|
||||
loader-utils@^2.0.0:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.3.tgz#d4b15b8504c63d1fc3f2ade52d41bc8459d6ede1"
|
||||
integrity sha512-THWqIsn8QRnvLl0shHYVBN9syumU8pYWEHPTmkiVGd+7K5eFNVSY6AJhRvgGF70gg1Dz+l/k8WicvFCxdEs60A==
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.2.tgz#d6e3b4fb81870721ae4e0868ab11dd638368c129"
|
||||
integrity sha512-TM57VeHptv569d/GKh6TAYdzKblwDNiumOdkFnejjD0XwTH87K90w3O7AiJRqdQoXygvi1VQTJTLGhJl7WqA7A==
|
||||
dependencies:
|
||||
big.js "^5.2.2"
|
||||
emojis-list "^3.0.0"
|
||||
|
||||
@@ -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({
|
||||
name: "changeExportBackground",
|
||||
trackEvent: { category: "export", action: "toggleBackground" },
|
||||
|
||||
@@ -68,6 +68,7 @@ export type ActionName =
|
||||
| "finalize"
|
||||
| "changeProjectName"
|
||||
| "changeExportBackground"
|
||||
| "changeExportPadding"
|
||||
| "changeExportEmbedScene"
|
||||
| "changeExportScale"
|
||||
| "saveToActiveFile"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import oc from "open-color";
|
||||
import {
|
||||
DEFAULT_EXPORT_PADDING,
|
||||
DEFAULT_FONT_FAMILY,
|
||||
DEFAULT_FONT_SIZE,
|
||||
DEFAULT_TEXT_ALIGN,
|
||||
@@ -55,6 +56,7 @@ export const getDefaultAppState = (): Omit<
|
||||
exportScale: defaultExportScale,
|
||||
exportEmbedScene: false,
|
||||
exportWithDarkMode: false,
|
||||
exportPadding: DEFAULT_EXPORT_PADDING,
|
||||
fileHandle: null,
|
||||
gridSize: null,
|
||||
isBindingEnabled: true,
|
||||
@@ -145,6 +147,7 @@ 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 },
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
import { parseClipboard } from "./clipboard";
|
||||
|
||||
describe("Test parseClipboard", () => {
|
||||
it("should parse valid json correctly", async () => {
|
||||
let text = "123";
|
||||
|
||||
let clipboardData = await parseClipboard({
|
||||
//@ts-ignore
|
||||
clipboardData: {
|
||||
getData: () => text,
|
||||
},
|
||||
});
|
||||
|
||||
expect(clipboardData.text).toBe(text);
|
||||
|
||||
text = "[123]";
|
||||
|
||||
clipboardData = await parseClipboard({
|
||||
//@ts-ignore
|
||||
clipboardData: {
|
||||
getData: () => text,
|
||||
},
|
||||
});
|
||||
|
||||
expect(clipboardData.text).toBe(text);
|
||||
});
|
||||
});
|
||||
@@ -156,13 +156,15 @@ export const parseClipboard = async (
|
||||
files: systemClipboardData.files,
|
||||
};
|
||||
}
|
||||
} catch (e) {}
|
||||
// system clipboard doesn't contain excalidraw elements → return plaintext
|
||||
// unless we set a flag to prefer in-app clipboard because browser didn't
|
||||
// support storing to system clipboard on copy
|
||||
return PREFER_APP_CLIPBOARD && appClipboardData.elements
|
||||
? appClipboardData
|
||||
: { text: systemClipboard };
|
||||
return appClipboardData;
|
||||
} catch {
|
||||
// system clipboard doesn't contain excalidraw elements → return plaintext
|
||||
// unless we set a flag to prefer in-app clipboard because browser didn't
|
||||
// support storing to system clipboard on copy
|
||||
return PREFER_APP_CLIPBOARD && appClipboardData.elements
|
||||
? appClipboardData
|
||||
: { text: systemClipboard };
|
||||
}
|
||||
};
|
||||
|
||||
export const copyBlobToClipboardAsPng = async (blob: Blob | Promise<Blob>) => {
|
||||
|
||||
@@ -218,12 +218,13 @@ export const ShapesSwitcher = ({
|
||||
appState: AppState;
|
||||
}) => (
|
||||
<>
|
||||
{SHAPES.map(({ value, icon, key, numericKey, fillable }, index) => {
|
||||
{SHAPES.map(({ value, icon, key, fillable }, index) => {
|
||||
const numberKey = value === "eraser" ? 0 : index + 1;
|
||||
const label = t(`toolBar.${value}`);
|
||||
const letter = key && (typeof key === "string" ? key : key[0]);
|
||||
const shortcut = letter
|
||||
? `${capitalizeString(letter)} ${t("helpDialog.or")} ${numericKey}`
|
||||
: `${numericKey}`;
|
||||
? `${capitalizeString(letter)} ${t("helpDialog.or")} ${numberKey}`
|
||||
: `${numberKey}`;
|
||||
return (
|
||||
<ToolButton
|
||||
className={clsx("Shape", { fillable })}
|
||||
@@ -233,7 +234,7 @@ export const ShapesSwitcher = ({
|
||||
checked={activeTool.type === value}
|
||||
name="editor-current-shape"
|
||||
title={`${capitalizeString(label)} — ${shortcut}`}
|
||||
keyBindingLabel={numericKey}
|
||||
keyBindingLabel={`${numberKey}`}
|
||||
aria-label={capitalizeString(label)}
|
||||
aria-keyshortcuts={shortcut}
|
||||
data-testid={`toolbar-${value}`}
|
||||
|
||||
@@ -1912,6 +1912,18 @@ class App extends React.Component<AppProps, AppState> {
|
||||
this.setState({ isBindingEnabled: false });
|
||||
}
|
||||
|
||||
if (event.code === CODES.ZERO) {
|
||||
const nextState = this.toggleMenu("library");
|
||||
// track only openings
|
||||
if (nextState) {
|
||||
trackEvent(
|
||||
"library",
|
||||
"toggleLibrary (open)",
|
||||
`keyboard (${this.device.isMobile ? "mobile" : "desktop"})`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (isArrowKey(event.key)) {
|
||||
const step =
|
||||
(this.state.gridSize &&
|
||||
@@ -5231,7 +5243,6 @@ class App extends React.Component<AppProps, AppState> {
|
||||
id: fileId,
|
||||
dataURL,
|
||||
created: Date.now(),
|
||||
lastRetrieved: Date.now(),
|
||||
},
|
||||
};
|
||||
const cachedImageData = this.imageCache.get(fileId);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from "react";
|
||||
import { t } from "../i18n";
|
||||
import { isDarwin, isWindows, KEYS } from "../keys";
|
||||
import { isDarwin, isWindows } from "../keys";
|
||||
import { Dialog } from "./Dialog";
|
||||
import { getShortcutKey } from "../utils";
|
||||
import "./HelpDialog.scss";
|
||||
@@ -118,42 +118,22 @@ export const HelpDialog = ({ onClose }: { onClose?: () => void }) => {
|
||||
className="HelpDialog__island--tools"
|
||||
caption={t("helpDialog.tools")}
|
||||
>
|
||||
<Shortcut
|
||||
label={t("toolBar.selection")}
|
||||
shortcuts={[KEYS.V, KEYS["1"]]}
|
||||
/>
|
||||
<Shortcut
|
||||
label={t("toolBar.rectangle")}
|
||||
shortcuts={[KEYS.R, KEYS["2"]]}
|
||||
/>
|
||||
<Shortcut
|
||||
label={t("toolBar.diamond")}
|
||||
shortcuts={[KEYS.D, KEYS["3"]]}
|
||||
/>
|
||||
<Shortcut
|
||||
label={t("toolBar.ellipse")}
|
||||
shortcuts={[KEYS.O, KEYS["4"]]}
|
||||
/>
|
||||
<Shortcut
|
||||
label={t("toolBar.arrow")}
|
||||
shortcuts={[KEYS.A, KEYS["5"]]}
|
||||
/>
|
||||
<Shortcut
|
||||
label={t("toolBar.line")}
|
||||
shortcuts={[KEYS.P, KEYS["6"]]}
|
||||
/>
|
||||
<Shortcut label={t("toolBar.selection")} shortcuts={["V", "1"]} />
|
||||
<Shortcut label={t("toolBar.rectangle")} shortcuts={["R", "2"]} />
|
||||
<Shortcut label={t("toolBar.diamond")} shortcuts={["D", "3"]} />
|
||||
<Shortcut label={t("toolBar.ellipse")} shortcuts={["O", "4"]} />
|
||||
<Shortcut label={t("toolBar.arrow")} shortcuts={["A", "5"]} />
|
||||
<Shortcut label={t("toolBar.line")} shortcuts={["P", "6"]} />
|
||||
<Shortcut
|
||||
label={t("toolBar.freedraw")}
|
||||
shortcuts={["Shift + P", KEYS["7"]]}
|
||||
shortcuts={["Shift + P", "X", "7"]}
|
||||
/>
|
||||
<Shortcut
|
||||
label={t("toolBar.text")}
|
||||
shortcuts={[KEYS.T, KEYS["8"]]}
|
||||
/>
|
||||
<Shortcut label={t("toolBar.image")} shortcuts={[KEYS["9"]]} />
|
||||
<Shortcut label={t("toolBar.text")} shortcuts={["T", "8"]} />
|
||||
<Shortcut label={t("toolBar.image")} shortcuts={["9"]} />
|
||||
<Shortcut label={t("toolBar.library")} shortcuts={["0"]} />
|
||||
<Shortcut
|
||||
label={t("toolBar.eraser")}
|
||||
shortcuts={[KEYS.E, KEYS["0"]]}
|
||||
shortcuts={[getShortcutKey("E")]}
|
||||
/>
|
||||
<Shortcut
|
||||
label={t("helpDialog.editSelectedShape")}
|
||||
@@ -193,7 +173,7 @@ export const HelpDialog = ({ onClose }: { onClose?: () => void }) => {
|
||||
]}
|
||||
isOr={false}
|
||||
/>
|
||||
<Shortcut label={t("toolBar.lock")} shortcuts={[KEYS.Q]} />
|
||||
<Shortcut label={t("toolBar.lock")} shortcuts={["Q"]} />
|
||||
<Shortcut
|
||||
label={t("helpDialog.preventBinding")}
|
||||
shortcuts={[getShortcutKey("CtrlOrCmd")]}
|
||||
|
||||
@@ -79,7 +79,6 @@ const ImageExportModal = ({
|
||||
elements,
|
||||
appState,
|
||||
files,
|
||||
exportPadding = DEFAULT_EXPORT_PADDING,
|
||||
actionManager,
|
||||
onExportToPng,
|
||||
onExportToSvg,
|
||||
@@ -88,7 +87,6 @@ const ImageExportModal = ({
|
||||
appState: AppState;
|
||||
elements: readonly NonDeletedExcalidrawElement[];
|
||||
files: BinaryFiles;
|
||||
exportPadding?: number;
|
||||
actionManager: ActionManager;
|
||||
onExportToPng: ExportCB;
|
||||
onExportToSvg: ExportCB;
|
||||
@@ -116,7 +114,7 @@ const ImageExportModal = ({
|
||||
exportToCanvas(exportedElements, appState, files, {
|
||||
exportBackground,
|
||||
viewBackgroundColor,
|
||||
exportPadding,
|
||||
exportPadding: appState.exportPadding,
|
||||
})
|
||||
.then((canvas) => {
|
||||
// if converting to blob fails, there's some problem that will
|
||||
@@ -134,7 +132,6 @@ const ImageExportModal = ({
|
||||
files,
|
||||
exportedElements,
|
||||
exportBackground,
|
||||
exportPadding,
|
||||
viewBackgroundColor,
|
||||
]);
|
||||
|
||||
@@ -151,8 +148,10 @@ 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
|
||||
@@ -221,7 +220,6 @@ export const ImageExportDialog = ({
|
||||
appState,
|
||||
setAppState,
|
||||
files,
|
||||
exportPadding = DEFAULT_EXPORT_PADDING,
|
||||
actionManager,
|
||||
onExportToPng,
|
||||
onExportToSvg,
|
||||
@@ -231,7 +229,6 @@ export const ImageExportDialog = ({
|
||||
setAppState: React.Component<any, AppState>["setState"];
|
||||
elements: readonly NonDeletedExcalidrawElement[];
|
||||
files: BinaryFiles;
|
||||
exportPadding?: number;
|
||||
actionManager: ActionManager;
|
||||
onExportToPng: ExportCB;
|
||||
onExportToSvg: ExportCB;
|
||||
@@ -249,7 +246,6 @@ export const ImageExportDialog = ({
|
||||
elements={elements}
|
||||
appState={appState}
|
||||
files={files}
|
||||
exportPadding={exportPadding}
|
||||
actionManager={actionManager}
|
||||
onExportToPng={onExportToPng}
|
||||
onExportToSvg={onExportToSvg}
|
||||
|
||||
@@ -144,6 +144,7 @@ const LayerUI = ({
|
||||
exportBackground: appState.exportBackground,
|
||||
name: appState.name,
|
||||
viewBackgroundColor: appState.viewBackgroundColor,
|
||||
exportPadding: appState.exportPadding,
|
||||
},
|
||||
)
|
||||
.catch(muteFSAbortError)
|
||||
@@ -196,7 +197,6 @@ const LayerUI = ({
|
||||
})}
|
||||
onClick={() => setIsMenuOpen(!isMenuOpen)}
|
||||
type="button"
|
||||
data-testid="menu-button"
|
||||
>
|
||||
{HamburgerMenuIcon}
|
||||
</button>
|
||||
@@ -221,15 +221,13 @@ const LayerUI = ({
|
||||
{appState.fileHandle &&
|
||||
actionManager.renderAction("saveToActiveFile")}
|
||||
{renderJSONExportDialog()}
|
||||
{UIOptions.canvasActions.saveAsImage && (
|
||||
<MenuItem
|
||||
label={t("buttons.exportImage")}
|
||||
icon={ExportImageIcon}
|
||||
dataTestId="image-export-button"
|
||||
onClick={() => setAppState({ openDialog: "imageExport" })}
|
||||
shortcut={getShortcutFromShortcutName("imageExport")}
|
||||
/>
|
||||
)}
|
||||
<MenuItem
|
||||
label={t("buttons.exportImage")}
|
||||
icon={ExportImageIcon}
|
||||
dataTestId="image-export-button"
|
||||
onClick={() => setAppState({ openDialog: "imageExport" })}
|
||||
shortcut={getShortcutFromShortcutName("imageExport")}
|
||||
/>
|
||||
{onCollabButtonClick && (
|
||||
<CollabButton
|
||||
isCollaborating={isCollaborating}
|
||||
@@ -411,8 +409,7 @@ const LayerUI = ({
|
||||
onClick={onCollabButtonClick}
|
||||
/>
|
||||
)}
|
||||
{!appState.viewModeEnabled &&
|
||||
renderTopRightUI?.(device.isMobile, appState)}
|
||||
{renderTopRightUI?.(device.isMobile, appState)}
|
||||
{!appState.viewModeEnabled && (
|
||||
<LibraryButton appState={appState} setAppState={setAppState} />
|
||||
)}
|
||||
|
||||
@@ -22,7 +22,7 @@ export const LibraryButton: React.FC<{
|
||||
}
|
||||
|
||||
return (
|
||||
<label title={`${capitalizeString(t("toolBar.library"))}`}>
|
||||
<label title={`${capitalizeString(t("toolBar.library"))} — 0`}>
|
||||
<input
|
||||
className="ToolIcon_type_checkbox"
|
||||
type="checkbox"
|
||||
|
||||
@@ -111,9 +111,8 @@ export const MobileMenu = ({
|
||||
/>
|
||||
</Stack.Row>
|
||||
</Island>
|
||||
{renderTopRightUI && renderTopRightUI(true, appState)}
|
||||
<div className="mobile-misc-tools-container">
|
||||
{!appState.viewModeEnabled &&
|
||||
renderTopRightUI?.(true, appState)}
|
||||
<PenModeButton
|
||||
checked={appState.penMode}
|
||||
onChange={onPenModeToggle}
|
||||
|
||||
@@ -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);
|
||||
fireEvent.click(closeButton!.querySelector("button")!);
|
||||
await waitFor(() => {
|
||||
expect(container.querySelector<HTMLElement>(".test-sidebar")).toBe(null);
|
||||
expect(onClose).toHaveBeenCalled();
|
||||
|
||||
@@ -1470,11 +1470,11 @@ export const TextAlignRightIcon = createIcon(
|
||||
export const TextAlignTopIcon = React.memo(({ theme }: { theme: Theme }) =>
|
||||
createIcon(
|
||||
<g
|
||||
strokeWidth="1.5"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<line x1="4" y1="4" x2="20" y2="4" />
|
||||
@@ -1488,11 +1488,11 @@ export const TextAlignTopIcon = React.memo(({ theme }: { theme: Theme }) =>
|
||||
export const TextAlignBottomIcon = React.memo(({ theme }: { theme: Theme }) =>
|
||||
createIcon(
|
||||
<g
|
||||
strokeWidth="2"
|
||||
stroke-width="2"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<line x1="4" y1="20" x2="20" y2="20" />
|
||||
@@ -1506,11 +1506,11 @@ export const TextAlignBottomIcon = React.memo(({ theme }: { theme: Theme }) =>
|
||||
export const TextAlignMiddleIcon = React.memo(({ theme }: { theme: Theme }) =>
|
||||
createIcon(
|
||||
<g
|
||||
strokeWidth="1.5"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<line x1="4" y1="12" x2="9" y2="12" />
|
||||
|
||||
@@ -492,7 +492,7 @@ export const getElementBounds = (
|
||||
|
||||
export const getCommonBounds = (
|
||||
elements: readonly ExcalidrawElement[],
|
||||
): [number, number, number, number] => {
|
||||
): [minX: number, minY: number, maxX: number, maxY: number] => {
|
||||
if (!elements.length) {
|
||||
return [0, 0, 0, 0];
|
||||
}
|
||||
|
||||
@@ -169,7 +169,8 @@ const getAdjustedDimensions = (
|
||||
let maxWidth = null;
|
||||
const container = getContainerElement(element);
|
||||
if (container) {
|
||||
maxWidth = getMaxContainerWidth(container);
|
||||
const containerDims = getContainerDims(container);
|
||||
maxWidth = containerDims.width - BOUND_TEXT_PADDING * 2;
|
||||
}
|
||||
const {
|
||||
width: nextWidth,
|
||||
@@ -257,6 +258,7 @@ export const refreshTextDimensions = (
|
||||
) => {
|
||||
const container = getContainerElement(textElement);
|
||||
if (container) {
|
||||
// text = wrapText(text, getFontString(textElement), container.width);
|
||||
text = wrapText(
|
||||
text,
|
||||
getFontString(textElement),
|
||||
|
||||
@@ -19,12 +19,13 @@ export const redrawTextBoundingBox = (
|
||||
) => {
|
||||
let maxWidth = undefined;
|
||||
let text = textElement.text;
|
||||
|
||||
if (container) {
|
||||
maxWidth = getMaxContainerWidth(container);
|
||||
text = wrapText(
|
||||
textElement.originalText,
|
||||
getFontString(textElement),
|
||||
maxWidth,
|
||||
getMaxContainerWidth(container),
|
||||
);
|
||||
}
|
||||
const metrics = measureText(
|
||||
@@ -229,9 +230,10 @@ export const measureText = (
|
||||
const baseline = span.offsetTop + span.offsetHeight;
|
||||
// Since span adds 1px extra width to the container
|
||||
const width = container.offsetWidth + 1;
|
||||
const height = container.offsetHeight;
|
||||
|
||||
const height = container.offsetHeight;
|
||||
document.body.removeChild(container);
|
||||
|
||||
return { width, height, baseline };
|
||||
};
|
||||
|
||||
|
||||
@@ -10,13 +10,11 @@ import { BOUND_TEXT_PADDING, FONT_FAMILY } from "../constants";
|
||||
import {
|
||||
ExcalidrawTextElement,
|
||||
ExcalidrawTextElementWithContainer,
|
||||
FontString,
|
||||
} from "./types";
|
||||
import * as textElementUtils from "./textElement";
|
||||
import { API } from "../tests/helpers/api";
|
||||
import { mutateElement } from "./mutateElement";
|
||||
import { resize } from "../tests/utils";
|
||||
import { getMaxContainerWidth } from "./newElement";
|
||||
// Unmount ReactDOM from root
|
||||
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
||||
|
||||
@@ -435,42 +433,6 @@ describe("textWysiwyg", () => {
|
||||
);
|
||||
expect(h.state.zoom.value).toBe(1);
|
||||
});
|
||||
|
||||
it("should paste text correctly", async () => {
|
||||
Keyboard.keyPress(KEYS.ENTER);
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
let text = "A quick brown fox jumps over the lazy dog.";
|
||||
|
||||
//@ts-ignore
|
||||
textarea.onpaste({
|
||||
preventDefault: () => {},
|
||||
//@ts-ignore
|
||||
clipboardData: {
|
||||
getData: () => text,
|
||||
},
|
||||
});
|
||||
|
||||
await new Promise((cb) => setTimeout(cb, 0));
|
||||
textarea.blur();
|
||||
expect(textElement.text).toBe(text);
|
||||
|
||||
Keyboard.keyPress(KEYS.ENTER);
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
text = "Hello this text should get merged with the existing one";
|
||||
//@ts-ignore
|
||||
textarea.onpaste({
|
||||
preventDefault: () => {},
|
||||
//@ts-ignore
|
||||
clipboardData: {
|
||||
getData: () => text,
|
||||
},
|
||||
});
|
||||
await new Promise((cb) => setTimeout(cb, 0));
|
||||
textarea.blur();
|
||||
expect(textElement.text).toMatchInlineSnapshot(
|
||||
`"A quick brown fox jumps over the lazy dog.Hello this text should get merged with the existing one"`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Test container-bound text", () => {
|
||||
@@ -914,125 +876,5 @@ describe("textWysiwyg", () => {
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it("should compute the dimensions correctly when text pasted", async () => {
|
||||
Keyboard.keyPress(KEYS.ENTER);
|
||||
const editor = document.querySelector(
|
||||
".excalidraw-textEditorContainer > textarea",
|
||||
) as HTMLTextAreaElement;
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
const font = "20px Cascadia, width: Segoe UI Emoji" as FontString;
|
||||
let text =
|
||||
"Wikipedia is hosted by the Wikimedia Foundation, a non-profit organization that also hosts a range of other projects.";
|
||||
|
||||
let wrappedText = textElementUtils.wrapText(
|
||||
text,
|
||||
font,
|
||||
getMaxContainerWidth(rectangle),
|
||||
);
|
||||
|
||||
jest
|
||||
.spyOn(textElementUtils, "measureText")
|
||||
.mockImplementation((text, font, maxWidth) => {
|
||||
if (text === wrappedText) {
|
||||
return { width: rectangle.width, height: 200, baseline: 30 };
|
||||
}
|
||||
return { width: 0, height: 0, baseline: 0 };
|
||||
});
|
||||
|
||||
//@ts-ignore
|
||||
editor.onpaste({
|
||||
preventDefault: () => {},
|
||||
//@ts-ignore
|
||||
clipboardData: {
|
||||
getData: () => text,
|
||||
},
|
||||
});
|
||||
|
||||
await new Promise((cb) => setTimeout(cb, 0));
|
||||
editor.blur();
|
||||
expect(rectangle.width).toBe(100);
|
||||
expect(rectangle.height).toBe(210);
|
||||
expect((h.elements[1] as ExcalidrawTextElement).text)
|
||||
.toMatchInlineSnapshot(`
|
||||
"Wikipedi
|
||||
a is
|
||||
hosted
|
||||
by the
|
||||
Wikimedi
|
||||
a
|
||||
Foundati
|
||||
on, a
|
||||
non-prof
|
||||
it
|
||||
organiza
|
||||
tion
|
||||
that
|
||||
also
|
||||
hosts a
|
||||
range of
|
||||
other
|
||||
projects
|
||||
."
|
||||
`);
|
||||
expect(
|
||||
(h.elements[1] as ExcalidrawTextElement).originalText,
|
||||
).toMatchInlineSnapshot(
|
||||
`"Wikipedia is hosted by the Wikimedia Foundation, a non-profit organization that also hosts a range of other projects."`,
|
||||
);
|
||||
|
||||
text = "Hello this text should get merged with the existing one";
|
||||
wrappedText = textElementUtils.wrapText(
|
||||
text,
|
||||
font,
|
||||
getMaxContainerWidth(rectangle),
|
||||
);
|
||||
//@ts-ignore
|
||||
editor.onpaste({
|
||||
preventDefault: () => {},
|
||||
//@ts-ignore
|
||||
clipboardData: {
|
||||
getData: () => text,
|
||||
},
|
||||
});
|
||||
|
||||
await new Promise((cb) => setTimeout(cb, 0));
|
||||
editor.blur();
|
||||
expect((h.elements[1] as ExcalidrawTextElement).text)
|
||||
.toMatchInlineSnapshot(`
|
||||
"Wikipedi
|
||||
a is
|
||||
hosted
|
||||
by the
|
||||
Wikimedi
|
||||
a
|
||||
Foundati
|
||||
on, a
|
||||
non-prof
|
||||
it
|
||||
organiza
|
||||
tion
|
||||
that
|
||||
also
|
||||
hosts a
|
||||
range of
|
||||
other
|
||||
projects
|
||||
.Hello
|
||||
this
|
||||
text
|
||||
should
|
||||
get
|
||||
merged
|
||||
with the
|
||||
existing
|
||||
one"
|
||||
`);
|
||||
expect(
|
||||
(h.elements[1] as ExcalidrawTextElement).originalText,
|
||||
).toMatchInlineSnapshot(
|
||||
`"Wikipedia is hosted by the Wikimedia Foundation, a non-profit organization that also hosts a range of other projects.Hello this text should get merged with the existing one"`,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -20,7 +20,6 @@ import {
|
||||
getBoundTextElementId,
|
||||
getContainerDims,
|
||||
getContainerElement,
|
||||
measureText,
|
||||
wrapText,
|
||||
} from "./textElement";
|
||||
import {
|
||||
@@ -30,7 +29,6 @@ import {
|
||||
import { actionZoomIn, actionZoomOut } from "../actions/actionCanvas";
|
||||
import App from "../components/App";
|
||||
import { getMaxContainerWidth } from "./newElement";
|
||||
import { parseClipboard } from "../clipboard";
|
||||
|
||||
const normalizeText = (text: string) => {
|
||||
return (
|
||||
@@ -277,38 +275,6 @@ export const textWysiwyg = ({
|
||||
updateWysiwygStyle();
|
||||
|
||||
if (onChange) {
|
||||
editable.onpaste = async (event) => {
|
||||
event.preventDefault();
|
||||
const clipboardData = await parseClipboard(event);
|
||||
if (!clipboardData.text) {
|
||||
return;
|
||||
}
|
||||
const data = normalizeText(clipboardData.text);
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
|
||||
const text = editable.value;
|
||||
const start = Math.min(editable.selectionStart, editable.selectionEnd);
|
||||
const end = Math.max(editable.selectionStart, editable.selectionEnd);
|
||||
const newText = `${text.substring(0, start)}${data}${text.substring(
|
||||
end,
|
||||
)}`;
|
||||
|
||||
const container = getContainerElement(element);
|
||||
|
||||
const font = getFontString({
|
||||
fontSize: app.state.currentItemFontSize,
|
||||
fontFamily: app.state.currentItemFontFamily,
|
||||
});
|
||||
|
||||
const wrappedText = container
|
||||
? wrapText(newText, font, getMaxContainerWidth(container))
|
||||
: newText;
|
||||
const dimensions = measureText(wrappedText, font);
|
||||
editable.style.height = `${dimensions.height}px`;
|
||||
onChange(newText);
|
||||
};
|
||||
editable.oninput = () => {
|
||||
const updatedTextElement = Scene.getScene(element)?.getElement(
|
||||
id,
|
||||
|
||||
@@ -310,27 +310,16 @@ class Collab extends PureComponent<Props, CollabState> {
|
||||
}
|
||||
};
|
||||
|
||||
private fetchImageFilesFromFirebase = async (opts: {
|
||||
private fetchImageFilesFromFirebase = async (scene: {
|
||||
elements: readonly ExcalidrawElement[];
|
||||
/**
|
||||
* Indicates whether to fetch files that are errored or pending and older
|
||||
* than 10 seconds.
|
||||
*
|
||||
* Use this as a machanism to fetch files which may be ok but for some
|
||||
* reason their status was not updated correctly.
|
||||
*/
|
||||
forceFetchFiles?: boolean;
|
||||
}) => {
|
||||
const unfetchedImages = opts.elements
|
||||
const unfetchedImages = scene.elements
|
||||
.filter((element) => {
|
||||
return (
|
||||
isInitializedImageElement(element) &&
|
||||
!this.fileManager.isFileHandled(element.fileId) &&
|
||||
!element.isDeleted &&
|
||||
(opts.forceFetchFiles
|
||||
? element.status !== "pending" ||
|
||||
Date.now() - element.updated > 10000
|
||||
: element.status === "saved")
|
||||
element.status === "saved"
|
||||
);
|
||||
})
|
||||
.map((element) => (element as InitializedExcalidrawImageElement).fileId);
|
||||
|
||||
@@ -195,7 +195,6 @@ export const encodeFilesForUpload = async ({
|
||||
id,
|
||||
mimeType: fileData.mimeType,
|
||||
created: Date.now(),
|
||||
lastRetrieved: Date.now(),
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
* (localStorage, indexedDB).
|
||||
*/
|
||||
|
||||
import { createStore, entries, del, getMany, set, setMany } from "idb-keyval";
|
||||
import { createStore, keys, del, getMany, set } from "idb-keyval";
|
||||
import { clearAppStateForLocalStorage } from "../../appState";
|
||||
import { clearElementsForLocalStorage } from "../../element";
|
||||
import { ExcalidrawElement, FileId } from "../../element/types";
|
||||
@@ -25,21 +25,12 @@ const filesStore = createStore("files-db", "files-store");
|
||||
|
||||
class LocalFileManager extends FileManager {
|
||||
clearObsoleteFiles = async (opts: { currentFileIds: FileId[] }) => {
|
||||
await entries(filesStore).then((entries) => {
|
||||
for (const [id, imageData] of entries as [FileId, BinaryFileData][]) {
|
||||
// if image is unused (not on canvas) & is older than 1 day, delete it
|
||||
// from storage. We check `lastRetrieved` we care about the last time
|
||||
// the image was used (loaded on canvas), not when it was initially
|
||||
// created.
|
||||
if (
|
||||
(!imageData.lastRetrieved ||
|
||||
Date.now() - imageData.lastRetrieved > 24 * 3600 * 1000) &&
|
||||
!opts.currentFileIds.includes(id as FileId)
|
||||
) {
|
||||
del(id, filesStore);
|
||||
}
|
||||
const allIds = await keys(filesStore);
|
||||
for (const id of allIds) {
|
||||
if (!opts.currentFileIds.includes(id as FileId)) {
|
||||
del(id, filesStore);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -120,33 +111,18 @@ export class LocalData {
|
||||
static fileStorage = new LocalFileManager({
|
||||
getFiles(ids) {
|
||||
return getMany(ids, filesStore).then(
|
||||
async (filesData: (BinaryFileData | undefined)[]) => {
|
||||
(filesData: (BinaryFileData | undefined)[]) => {
|
||||
const loadedFiles: BinaryFileData[] = [];
|
||||
const erroredFiles = new Map<FileId, true>();
|
||||
|
||||
const filesToSave: [FileId, BinaryFileData][] = [];
|
||||
|
||||
filesData.forEach((data, index) => {
|
||||
const id = ids[index];
|
||||
if (data) {
|
||||
const _data: BinaryFileData = {
|
||||
...data,
|
||||
lastRetrieved: Date.now(),
|
||||
};
|
||||
filesToSave.push([id, _data]);
|
||||
loadedFiles.push(_data);
|
||||
loadedFiles.push(data);
|
||||
} else {
|
||||
erroredFiles.set(id, true);
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
// save loaded files back to storage with updated `lastRetrieved`
|
||||
setMany(filesToSave, filesStore);
|
||||
} catch (error) {
|
||||
console.warn(error);
|
||||
}
|
||||
|
||||
return { loadedFiles, erroredFiles };
|
||||
},
|
||||
);
|
||||
|
||||
@@ -330,7 +330,6 @@ export const loadFilesFromFirebase = async (
|
||||
id,
|
||||
dataURL,
|
||||
created: metadata?.created || Date.now(),
|
||||
lastRetrieved: metadata?.created || Date.now(),
|
||||
});
|
||||
} else {
|
||||
erroredFiles.set(id, true);
|
||||
|
||||
@@ -285,7 +285,6 @@ const ExcalidrawWrapper = () => {
|
||||
collabAPI
|
||||
.fetchImageFilesFromFirebase({
|
||||
elements: data.scene.elements,
|
||||
forceFetchFiles: true,
|
||||
})
|
||||
.then(({ loadedFiles, erroredFiles }) => {
|
||||
excalidrawAPI.addFiles(loadedFiles);
|
||||
|
||||
11
src/keys.ts
11
src/keys.ts
@@ -63,17 +63,6 @@ export const KEYS = {
|
||||
Y: "y",
|
||||
Z: "z",
|
||||
K: "k",
|
||||
|
||||
0: "0",
|
||||
1: "1",
|
||||
2: "2",
|
||||
3: "3",
|
||||
4: "4",
|
||||
5: "5",
|
||||
6: "6",
|
||||
7: "7",
|
||||
8: "8",
|
||||
9: "9",
|
||||
} as const;
|
||||
|
||||
export type Key = keyof typeof KEYS;
|
||||
|
||||
@@ -148,7 +148,6 @@ export default function App() {
|
||||
dataURL: reader.result as BinaryFileData["dataURL"],
|
||||
mimeType: MIME_TYPES.jpg,
|
||||
created: 1644915140367,
|
||||
lastRetrieved: 1644915140367,
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -1873,7 +1873,7 @@ compression@^1.7.4:
|
||||
concat-map@0.0.1:
|
||||
version "0.0.1"
|
||||
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
|
||||
integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==
|
||||
integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=
|
||||
|
||||
connect-history-api-fallback@^2.0.0:
|
||||
version "2.0.0"
|
||||
@@ -2697,9 +2697,9 @@ loader-runner@^4.2.0:
|
||||
integrity sha512-92+huvxMvYlMzMt0iIOukcwYBFpkYJdpl2xsZ7LrlayO7E8SOv+JJUEK17B/dJIHAOLMfh2dZZ/Y18WgmGtYNw==
|
||||
|
||||
loader-utils@^2.0.0:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.3.tgz#d4b15b8504c63d1fc3f2ade52d41bc8459d6ede1"
|
||||
integrity sha512-THWqIsn8QRnvLl0shHYVBN9syumU8pYWEHPTmkiVGd+7K5eFNVSY6AJhRvgGF70gg1Dz+l/k8WicvFCxdEs60A==
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.2.tgz#d6e3b4fb81870721ae4e0868ab11dd638368c129"
|
||||
integrity sha512-TM57VeHptv569d/GKh6TAYdzKblwDNiumOdkFnejjD0XwTH87K90w3O7AiJRqdQoXygvi1VQTJTLGhJl7WqA7A==
|
||||
dependencies:
|
||||
big.js "^5.2.2"
|
||||
emojis-list "^3.0.0"
|
||||
@@ -2823,9 +2823,9 @@ minimalistic-assert@^1.0.0:
|
||||
integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==
|
||||
|
||||
minimatch@^3.0.4:
|
||||
version "3.1.2"
|
||||
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b"
|
||||
integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==
|
||||
version "3.0.4"
|
||||
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
|
||||
integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==
|
||||
dependencies:
|
||||
brace-expansion "^1.1.7"
|
||||
|
||||
|
||||
@@ -1915,9 +1915,9 @@ loader-runner@^4.2.0:
|
||||
integrity sha512-92+huvxMvYlMzMt0iIOukcwYBFpkYJdpl2xsZ7LrlayO7E8SOv+JJUEK17B/dJIHAOLMfh2dZZ/Y18WgmGtYNw==
|
||||
|
||||
loader-utils@^2.0.0:
|
||||
version "2.0.4"
|
||||
resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.4.tgz#8b5cb38b5c34a9a018ee1fc0e6a066d1dfcc528c"
|
||||
integrity sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.0.tgz#e4cace5b816d425a166b5f097e10cd12b36064b0"
|
||||
integrity sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==
|
||||
dependencies:
|
||||
big.js "^5.2.2"
|
||||
emojis-list "^3.0.0"
|
||||
|
||||
@@ -406,8 +406,6 @@ export const _renderScene = ({
|
||||
}),
|
||||
);
|
||||
|
||||
let editingLinearElement: NonDeleted<ExcalidrawLinearElement> | undefined =
|
||||
undefined;
|
||||
visibleElements.forEach((element) => {
|
||||
try {
|
||||
renderElement(element, rc, context, renderConfig);
|
||||
@@ -416,10 +414,15 @@ export const _renderScene = ({
|
||||
// correct element from visible elements
|
||||
if (appState.editingLinearElement?.elementId === element.id) {
|
||||
if (element) {
|
||||
editingLinearElement =
|
||||
element as NonDeleted<ExcalidrawLinearElement>;
|
||||
renderLinearPointHandles(
|
||||
context,
|
||||
appState,
|
||||
renderConfig,
|
||||
element as NonDeleted<ExcalidrawLinearElement>,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!isExporting) {
|
||||
renderLinkIcon(element, context, appState);
|
||||
}
|
||||
@@ -428,15 +431,6 @@ export const _renderScene = ({
|
||||
}
|
||||
});
|
||||
|
||||
if (editingLinearElement) {
|
||||
renderLinearPointHandles(
|
||||
context,
|
||||
appState,
|
||||
renderConfig,
|
||||
editingLinearElement,
|
||||
);
|
||||
}
|
||||
|
||||
// Paint selection element
|
||||
if (appState.selectionElement) {
|
||||
try {
|
||||
|
||||
@@ -14,6 +14,102 @@ 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,
|
||||
@@ -37,7 +133,12 @@ export const exportToCanvas = async (
|
||||
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);
|
||||
|
||||
@@ -59,8 +160,8 @@ export const exportToCanvas = async (
|
||||
canvas,
|
||||
renderConfig: {
|
||||
viewBackgroundColor: exportBackground ? viewBackgroundColor : null,
|
||||
scrollX: -minX + exportPadding,
|
||||
scrollY: -minY + exportPadding,
|
||||
scrollX,
|
||||
scrollY,
|
||||
zoom: defaultAppState.zoom,
|
||||
remotePointerViewportCoords: {},
|
||||
remoteSelectedElementIds: {},
|
||||
@@ -109,7 +210,12 @@ export const exportToSvg = async (
|
||||
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
|
||||
const svgRoot = document.createElementNS(SVG_NS, "svg");
|
||||
@@ -172,26 +278,66 @@ export const exportToSvg = async (
|
||||
return svgRoot;
|
||||
};
|
||||
|
||||
// calculate smallest area to fit the contents in
|
||||
const getCanvasSize = (
|
||||
const getApproximateCanvasSize = (
|
||||
elements: readonly NonDeletedExcalidrawElement[],
|
||||
exportPadding: 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 height = distance(minY, maxY) + exportPadding + exportPadding;
|
||||
const height =
|
||||
Math.ceil(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] = getCanvasSize(elements, exportPadding).map(
|
||||
(dimension) => Math.trunc(dimension * scale),
|
||||
);
|
||||
const [, , width, height] = getApproximateCanvasSize(
|
||||
elements,
|
||||
exportPadding,
|
||||
).map((dimension) => Math.trunc(dimension * scale));
|
||||
|
||||
return [width, height];
|
||||
};
|
||||
|
||||
@@ -3,8 +3,6 @@ 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({
|
||||
|
||||
@@ -17,70 +17,60 @@ export const SHAPES = [
|
||||
icon: SelectionIcon,
|
||||
value: "selection",
|
||||
key: KEYS.V,
|
||||
numericKey: KEYS["1"],
|
||||
fillable: true,
|
||||
},
|
||||
{
|
||||
icon: RectangleIcon,
|
||||
value: "rectangle",
|
||||
key: KEYS.R,
|
||||
numericKey: KEYS["2"],
|
||||
fillable: true,
|
||||
},
|
||||
{
|
||||
icon: DiamondIcon,
|
||||
value: "diamond",
|
||||
key: KEYS.D,
|
||||
numericKey: KEYS["3"],
|
||||
fillable: true,
|
||||
},
|
||||
{
|
||||
icon: EllipseIcon,
|
||||
value: "ellipse",
|
||||
key: KEYS.O,
|
||||
numericKey: KEYS["4"],
|
||||
fillable: true,
|
||||
},
|
||||
{
|
||||
icon: ArrowIcon,
|
||||
value: "arrow",
|
||||
key: KEYS.A,
|
||||
numericKey: KEYS["5"],
|
||||
fillable: true,
|
||||
},
|
||||
{
|
||||
icon: LineIcon,
|
||||
value: "line",
|
||||
key: KEYS.L,
|
||||
numericKey: KEYS["6"],
|
||||
key: [KEYS.P, KEYS.L],
|
||||
fillable: true,
|
||||
},
|
||||
{
|
||||
icon: FreedrawIcon,
|
||||
value: "freedraw",
|
||||
key: [KEYS.P, KEYS.X],
|
||||
numericKey: KEYS["7"],
|
||||
key: [KEYS.X, KEYS.P.toUpperCase()],
|
||||
fillable: false,
|
||||
},
|
||||
{
|
||||
icon: TextIcon,
|
||||
value: "text",
|
||||
key: KEYS.T,
|
||||
numericKey: KEYS["8"],
|
||||
fillable: false,
|
||||
},
|
||||
{
|
||||
icon: ImageIcon,
|
||||
value: "image",
|
||||
key: null,
|
||||
numericKey: KEYS["9"],
|
||||
fillable: false,
|
||||
},
|
||||
{
|
||||
icon: EraserIcon,
|
||||
value: "eraser",
|
||||
key: KEYS.E,
|
||||
numericKey: KEYS["0"],
|
||||
fillable: false,
|
||||
},
|
||||
] as const;
|
||||
@@ -88,7 +78,7 @@ export const SHAPES = [
|
||||
export const findShapeByKey = (key: string) => {
|
||||
const shape = SHAPES.find((shape, index) => {
|
||||
return (
|
||||
(shape.numericKey != null && key === shape.numericKey.toString()) ||
|
||||
key === (shape.value === "eraser" ? 0 : index + 1).toString() ||
|
||||
(shape.key &&
|
||||
(typeof shape.key === "string"
|
||||
? shape.key === key
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -42,7 +42,7 @@ describe("Test dragCreate", () => {
|
||||
// finish (position does not matter)
|
||||
fireEvent.pointerUp(canvas);
|
||||
|
||||
expect(renderScene).toHaveBeenCalledTimes(9);
|
||||
expect(renderScene).toHaveBeenCalledTimes(8);
|
||||
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(9);
|
||||
expect(renderScene).toHaveBeenCalledTimes(8);
|
||||
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(9);
|
||||
expect(renderScene).toHaveBeenCalledTimes(8);
|
||||
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(9);
|
||||
expect(renderScene).toHaveBeenCalledTimes(8);
|
||||
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(9);
|
||||
expect(renderScene).toHaveBeenCalledTimes(8);
|
||||
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(7);
|
||||
expect(renderScene).toHaveBeenCalledTimes(6);
|
||||
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(7);
|
||||
expect(renderScene).toHaveBeenCalledTimes(6);
|
||||
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(7);
|
||||
expect(renderScene).toHaveBeenCalledTimes(6);
|
||||
expect(h.state.selectionElement).toBeNull();
|
||||
expect(h.elements.length).toEqual(0);
|
||||
});
|
||||
@@ -272,7 +272,7 @@ describe("Test dragCreate", () => {
|
||||
key: KEYS.ENTER,
|
||||
});
|
||||
|
||||
expect(renderScene).toHaveBeenCalledTimes(8);
|
||||
expect(renderScene).toHaveBeenCalledTimes(7);
|
||||
expect(h.state.selectionElement).toBeNull();
|
||||
expect(h.elements.length).toEqual(0);
|
||||
});
|
||||
@@ -296,7 +296,7 @@ describe("Test dragCreate", () => {
|
||||
key: KEYS.ENTER,
|
||||
});
|
||||
|
||||
expect(renderScene).toHaveBeenCalledTimes(8);
|
||||
expect(renderScene).toHaveBeenCalledTimes(7);
|
||||
expect(h.state.selectionElement).toBeNull();
|
||||
expect(h.elements.length).toEqual(0);
|
||||
});
|
||||
|
||||
@@ -158,7 +158,6 @@ describe("export", () => {
|
||||
dataURL: await getDataURL(await API.loadFile("./fixtures/deer.png")),
|
||||
mimeType: "image/png",
|
||||
created: Date.now(),
|
||||
lastRetrieved: Date.now(),
|
||||
},
|
||||
} as const;
|
||||
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
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";
|
||||
@@ -95,11 +93,15 @@ describe("library menu", () => {
|
||||
const latestLibrary = await h.app.library.getLatestLibrary();
|
||||
expect(latestLibrary.length).toBe(0);
|
||||
|
||||
const libraryButton = container.querySelector(".library-button");
|
||||
const libraryButton = container.querySelector(".ToolIcon__library");
|
||||
|
||||
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);
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ const renderScene = jest.spyOn(Renderer, "renderScene");
|
||||
|
||||
const { h } = window;
|
||||
|
||||
describe("Test Linear Elements", () => {
|
||||
describe(" Test Linear Elements", () => {
|
||||
let container: HTMLElement;
|
||||
let canvas: HTMLCanvasElement;
|
||||
|
||||
@@ -89,15 +89,8 @@ describe("Test Linear Elements", () => {
|
||||
mouse.clickAt(p1[0], p1[1]);
|
||||
};
|
||||
|
||||
const enterLineEditingMode = (
|
||||
line: ExcalidrawLinearElement,
|
||||
selectProgrammatically = false,
|
||||
) => {
|
||||
if (selectProgrammatically) {
|
||||
API.setSelectedElements([line]);
|
||||
} else {
|
||||
mouse.clickAt(p1[0], p1[1]);
|
||||
}
|
||||
const enterLineEditingMode = (line: ExcalidrawLinearElement) => {
|
||||
mouse.clickAt(p1[0], p1[1]);
|
||||
Keyboard.keyPress(KEYS.ENTER);
|
||||
expect(h.state.editingLinearElement?.elementId).toEqual(line.id);
|
||||
};
|
||||
@@ -611,43 +604,5 @@ describe("Test Linear Elements", () => {
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
it("in-editor dragging a line point covered by another element", () => {
|
||||
createTwoPointerLinearElement("line");
|
||||
const line = h.elements[0] as ExcalidrawLinearElement;
|
||||
h.elements = [
|
||||
line,
|
||||
API.createElement({
|
||||
type: "rectangle",
|
||||
x: line.x - 50,
|
||||
y: line.y - 50,
|
||||
width: 100,
|
||||
height: 100,
|
||||
backgroundColor: "red",
|
||||
fillStyle: "solid",
|
||||
}),
|
||||
];
|
||||
|
||||
const origPoints = line.points.map((point) => [...point]);
|
||||
const dragEndPositionOffset = [100, 100] as const;
|
||||
API.setSelectedElements([line]);
|
||||
enterLineEditingMode(line, true);
|
||||
drag(
|
||||
[line.points[0][0] + line.x, line.points[0][1] + line.y],
|
||||
[dragEndPositionOffset[0] + line.x, dragEndPositionOffset[1] + line.y],
|
||||
);
|
||||
expect(line.points).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Array [
|
||||
0,
|
||||
0,
|
||||
],
|
||||
Array [
|
||||
${origPoints[1][0] - dragEndPositionOffset[0]},
|
||||
${origPoints[1][1] - dragEndPositionOffset[1]},
|
||||
],
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -38,7 +38,7 @@ describe("move element", () => {
|
||||
fireEvent.pointerMove(canvas, { clientX: 60, clientY: 70 });
|
||||
fireEvent.pointerUp(canvas);
|
||||
|
||||
expect(renderScene).toHaveBeenCalledTimes(9);
|
||||
expect(renderScene).toHaveBeenCalledTimes(8);
|
||||
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(23);
|
||||
expect(renderScene).toHaveBeenCalledTimes(22);
|
||||
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(9);
|
||||
expect(renderScene).toHaveBeenCalledTimes(8);
|
||||
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(7);
|
||||
expect(renderScene).toHaveBeenCalledTimes(6);
|
||||
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(7);
|
||||
expect(renderScene).toHaveBeenCalledTimes(6);
|
||||
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(7);
|
||||
expect(renderScene).toHaveBeenCalledTimes(6);
|
||||
expect(h.elements.length).toEqual(0);
|
||||
});
|
||||
});
|
||||
@@ -102,7 +102,7 @@ describe("multi point mode in linear elements", () => {
|
||||
key: KEYS.ENTER,
|
||||
});
|
||||
|
||||
expect(renderScene).toHaveBeenCalledTimes(15);
|
||||
expect(renderScene).toHaveBeenCalledTimes(14);
|
||||
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(15);
|
||||
expect(renderScene).toHaveBeenCalledTimes(14);
|
||||
expect(h.elements.length).toEqual(1);
|
||||
|
||||
const element = h.elements[0] as ExcalidrawLinearElement;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -90,10 +90,7 @@ 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,7 +10,6 @@ const toolMap = {
|
||||
line: "line",
|
||||
freedraw: "freedraw",
|
||||
text: "text",
|
||||
eraser: "eraser",
|
||||
};
|
||||
|
||||
export type ToolName = keyof typeof toolMap;
|
||||
|
||||
@@ -138,7 +138,7 @@ describe("regression tests", () => {
|
||||
[`4${KEYS.O}`, "ellipse", true],
|
||||
[`5${KEYS.A}`, "arrow", true],
|
||||
[`6${KEYS.L}`, "line", true],
|
||||
[`7${KEYS.P}`, "freedraw", false],
|
||||
[`7${KEYS.X}`, "freedraw", false],
|
||||
] as [string, ExcalidrawElement["type"], boolean][]) {
|
||||
for (const key of keys) {
|
||||
it(`key ${key} selects ${shape} tool`, () => {
|
||||
@@ -174,7 +174,7 @@ describe("regression tests", () => {
|
||||
mouse.up(10, 10);
|
||||
|
||||
const { x: prevX, y: prevY } = API.getSelectedElement();
|
||||
mouse.down(-8, -8);
|
||||
mouse.down(-10, -10);
|
||||
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(-8, -8);
|
||||
mouse.down(-10, -10);
|
||||
mouse.up(10, 10);
|
||||
});
|
||||
|
||||
@@ -446,8 +446,6 @@ 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" },
|
||||
});
|
||||
@@ -674,10 +672,9 @@ describe("regression tests", () => {
|
||||
mouse.down();
|
||||
mouse.up(100, 100);
|
||||
|
||||
expect(API.getSelectedElements().length).toBe(1);
|
||||
|
||||
// hits bounding box without hitting element
|
||||
mouse.down(98, 98);
|
||||
mouse.down();
|
||||
expect(API.getSelectedElements().length).toBe(1);
|
||||
mouse.up();
|
||||
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
|
||||
mouse.reset();
|
||||
mouse.down(8, 8);
|
||||
mouse.down();
|
||||
mouse.up(25, 25);
|
||||
|
||||
expect(API.getSelectedElement().x).toEqual(prevX + 25);
|
||||
@@ -1023,7 +1020,7 @@ describe("regression tests", () => {
|
||||
// Rectangle is already selected since creating
|
||||
// it was our last action
|
||||
Keyboard.withModifierKeys({ shift: true }, () => {
|
||||
mouse.down(-8, -8);
|
||||
mouse.down();
|
||||
});
|
||||
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(5);
|
||||
expect(renderScene).toHaveBeenCalledTimes(4);
|
||||
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(6);
|
||||
expect(renderScene).toHaveBeenCalledTimes(5);
|
||||
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(7);
|
||||
expect(renderScene).toHaveBeenCalledTimes(6);
|
||||
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(11);
|
||||
expect(renderScene).toHaveBeenCalledTimes(10);
|
||||
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(11);
|
||||
expect(renderScene).toHaveBeenCalledTimes(10);
|
||||
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(11);
|
||||
expect(renderScene).toHaveBeenCalledTimes(10);
|
||||
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(11);
|
||||
expect(renderScene).toHaveBeenCalledTimes(10);
|
||||
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(11);
|
||||
expect(renderScene).toHaveBeenCalledTimes(10);
|
||||
expect(h.state.selectionElement).toBeNull();
|
||||
expect(h.elements.length).toEqual(1);
|
||||
expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy();
|
||||
|
||||
@@ -16,6 +16,8 @@ import { SceneData } from "../types";
|
||||
import { getSelectedElements } from "../scene/selection";
|
||||
import { ExcalidrawElement } from "../element/types";
|
||||
|
||||
require("fake-indexeddb/auto");
|
||||
|
||||
const customQueries = {
|
||||
...queries,
|
||||
...toolQueries,
|
||||
|
||||
12
src/types.ts
12
src/types.ts
@@ -61,18 +61,7 @@ export type BinaryFileData = {
|
||||
| typeof MIME_TYPES.binary;
|
||||
id: FileId;
|
||||
dataURL: DataURL;
|
||||
/**
|
||||
* Epoch timestamp in milliseconds
|
||||
*/
|
||||
created: number;
|
||||
/**
|
||||
* Indicates when the file was last retrieved from storage to be loaded
|
||||
* onto the scene. We use this flag to determine whether to delete unused
|
||||
* files from storage.
|
||||
*
|
||||
* Epoch timestamp in milliseconds.
|
||||
*/
|
||||
lastRetrieved?: number;
|
||||
};
|
||||
|
||||
export type BinaryFileMetadata = Omit<BinaryFileData, "dataURL">;
|
||||
@@ -124,6 +113,7 @@ export type AppState = {
|
||||
exportEmbedScene: boolean;
|
||||
exportWithDarkMode: boolean;
|
||||
exportScale: number;
|
||||
exportPadding: number;
|
||||
currentItemStrokeColor: string;
|
||||
currentItemBackgroundColor: string;
|
||||
currentItemFillStyle: ExcalidrawElement["fillStyle"];
|
||||
|
||||
@@ -11263,9 +11263,9 @@ socket.io-client@2.3.1:
|
||||
to-array "0.1.4"
|
||||
|
||||
socket.io-parser@~3.3.0:
|
||||
version "3.3.3"
|
||||
resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-3.3.3.tgz#3a8b84823eba87f3f7624e64a8aaab6d6318a72f"
|
||||
integrity sha512-qOg87q1PMWWTeO01768Yh9ogn7chB9zkKtQnya41Y355S0UmpXgpcrFwAgjYJxu9BdKug5r5e9YtVSeWhKBUZg==
|
||||
version "3.3.2"
|
||||
resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-3.3.2.tgz#ef872009d0adcf704f2fbe830191a14752ad50b6"
|
||||
integrity sha512-FJvDBuOALxdCI9qwRrO/Rfp9yfndRtc1jSgVgV8FDraihmSP/MLGD5PEuJrNfjALvcQ+vMDM/33AWOYP/JSjDg==
|
||||
dependencies:
|
||||
component-emitter "~1.3.0"
|
||||
debug "~3.1.0"
|
||||
|
||||
Reference in New Issue
Block a user