mirror of
https://github.com/excalidraw/excalidraw
synced 2025-07-25 13:58:22 +08:00
Compare commits
19 Commits
zsviczian-
...
fix-zsvicz
Author | SHA1 | Date | |
---|---|---|---|
b412e742e6 | |||
c246ccf9d9 | |||
3c0b29d85f | |||
bfbaeae67f | |||
74b9885955 | |||
2cbe869a13 | |||
a48607eb25 | |||
7831b6e74b | |||
640affe7c0 | |||
335aff8838 | |||
dc97dc30bf | |||
a0ecfed4cd | |||
e201e79cd0 | |||
e1c5c706c6 | |||
bdc56090d7 | |||
58accc9310 | |||
b91158198e | |||
938ce241ff | |||
0228646507 |
@ -1,5 +1,6 @@
|
||||
*
|
||||
!.env
|
||||
!.env.development
|
||||
!.env.production
|
||||
!.eslintrc.json
|
||||
!.npmrc
|
||||
!.prettierrc
|
||||
|
@ -20,3 +20,5 @@ 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.2"
|
||||
resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.2.tgz#d6e3b4fb81870721ae4e0868ab11dd638368c129"
|
||||
integrity sha512-TM57VeHptv569d/GKh6TAYdzKblwDNiumOdkFnejjD0XwTH87K90w3O7AiJRqdQoXygvi1VQTJTLGhJl7WqA7A==
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.3.tgz#d4b15b8504c63d1fc3f2ade52d41bc8459d6ede1"
|
||||
integrity sha512-THWqIsn8QRnvLl0shHYVBN9syumU8pYWEHPTmkiVGd+7K5eFNVSY6AJhRvgGF70gg1Dz+l/k8WicvFCxdEs60A==
|
||||
dependencies:
|
||||
big.js "^5.2.2"
|
||||
emojis-list "^3.0.0"
|
||||
|
27
src/clipboard.test.ts
Normal file
27
src/clipboard.test.ts
Normal file
@ -0,0 +1,27 @@
|
||||
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,15 +156,13 @@ export const parseClipboard = async (
|
||||
files: systemClipboardData.files,
|
||||
};
|
||||
}
|
||||
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 };
|
||||
}
|
||||
} 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 };
|
||||
};
|
||||
|
||||
export const copyBlobToClipboardAsPng = async (blob: Blob | Promise<Blob>) => {
|
||||
|
@ -218,13 +218,12 @@ export const ShapesSwitcher = ({
|
||||
appState: AppState;
|
||||
}) => (
|
||||
<>
|
||||
{SHAPES.map(({ value, icon, key, fillable }, index) => {
|
||||
const numberKey = value === "eraser" ? 0 : index + 1;
|
||||
{SHAPES.map(({ value, icon, key, numericKey, fillable }, index) => {
|
||||
const label = t(`toolBar.${value}`);
|
||||
const letter = key && (typeof key === "string" ? key : key[0]);
|
||||
const shortcut = letter
|
||||
? `${capitalizeString(letter)} ${t("helpDialog.or")} ${numberKey}`
|
||||
: `${numberKey}`;
|
||||
? `${capitalizeString(letter)} ${t("helpDialog.or")} ${numericKey}`
|
||||
: `${numericKey}`;
|
||||
return (
|
||||
<ToolButton
|
||||
className={clsx("Shape", { fillable })}
|
||||
@ -234,7 +233,7 @@ export const ShapesSwitcher = ({
|
||||
checked={activeTool.type === value}
|
||||
name="editor-current-shape"
|
||||
title={`${capitalizeString(label)} — ${shortcut}`}
|
||||
keyBindingLabel={`${numberKey}`}
|
||||
keyBindingLabel={numericKey}
|
||||
aria-label={capitalizeString(label)}
|
||||
aria-keyshortcuts={shortcut}
|
||||
data-testid={`toolbar-${value}`}
|
||||
|
@ -516,7 +516,6 @@ class App extends React.Component<AppProps, AppState> {
|
||||
const {
|
||||
onCollabButtonClick,
|
||||
renderTopRightUI,
|
||||
renderMenuLinks,
|
||||
renderFooter,
|
||||
renderCustomStats,
|
||||
} = this.props;
|
||||
@ -563,7 +562,6 @@ class App extends React.Component<AppProps, AppState> {
|
||||
langCode={getLanguage().code}
|
||||
isCollaborating={this.props.isCollaborating}
|
||||
renderTopRightUI={renderTopRightUI}
|
||||
renderMenuLinks={renderMenuLinks}
|
||||
renderCustomFooter={renderFooter}
|
||||
renderCustomStats={renderCustomStats}
|
||||
renderCustomSidebar={this.props.renderSidebar}
|
||||
@ -578,7 +576,6 @@ class App extends React.Component<AppProps, AppState> {
|
||||
id={this.id}
|
||||
onImageAction={this.onImageAction}
|
||||
renderWelcomeScreen={
|
||||
this.props.hideWelcomeScreen !== true &&
|
||||
this.state.showWelcomeScreen &&
|
||||
this.state.activeTool.type === "selection" &&
|
||||
!this.scene.getElementsIncludingDeleted().length
|
||||
@ -1915,18 +1912,6 @@ 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 &&
|
||||
@ -5246,6 +5231,7 @@ 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 } from "../keys";
|
||||
import { isDarwin, isWindows, KEYS } from "../keys";
|
||||
import { Dialog } from "./Dialog";
|
||||
import { getShortcutKey } from "../utils";
|
||||
import "./HelpDialog.scss";
|
||||
@ -118,22 +118,42 @@ export const HelpDialog = ({ onClose }: { onClose?: () => void }) => {
|
||||
className="HelpDialog__island--tools"
|
||||
caption={t("helpDialog.tools")}
|
||||
>
|
||||
<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.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.freedraw")}
|
||||
shortcuts={["Shift + P", "X", "7"]}
|
||||
shortcuts={["Shift + P", KEYS["7"]]}
|
||||
/>
|
||||
<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.text")}
|
||||
shortcuts={[KEYS.T, KEYS["8"]]}
|
||||
/>
|
||||
<Shortcut label={t("toolBar.image")} shortcuts={[KEYS["9"]]} />
|
||||
<Shortcut
|
||||
label={t("toolBar.eraser")}
|
||||
shortcuts={[getShortcutKey("E")]}
|
||||
shortcuts={[KEYS.E, KEYS["0"]]}
|
||||
/>
|
||||
<Shortcut
|
||||
label={t("helpDialog.editSelectedShape")}
|
||||
@ -173,7 +193,7 @@ export const HelpDialog = ({ onClose }: { onClose?: () => void }) => {
|
||||
]}
|
||||
isOr={false}
|
||||
/>
|
||||
<Shortcut label={t("toolBar.lock")} shortcuts={["Q"]} />
|
||||
<Shortcut label={t("toolBar.lock")} shortcuts={[KEYS.Q]} />
|
||||
<Shortcut
|
||||
label={t("helpDialog.preventBinding")}
|
||||
shortcuts={[getShortcutKey("CtrlOrCmd")]}
|
||||
|
@ -71,7 +71,6 @@ interface LayerUIProps {
|
||||
langCode: Language["code"];
|
||||
isCollaborating: boolean;
|
||||
renderTopRightUI?: ExcalidrawProps["renderTopRightUI"];
|
||||
renderMenuLinks?: ExcalidrawProps["renderMenuLinks"];
|
||||
renderCustomFooter?: ExcalidrawProps["renderFooter"];
|
||||
renderCustomStats?: ExcalidrawProps["renderCustomStats"];
|
||||
renderCustomSidebar?: ExcalidrawProps["renderSidebar"];
|
||||
@ -97,7 +96,6 @@ const LayerUI = ({
|
||||
showExitZenModeBtn,
|
||||
isCollaborating,
|
||||
renderTopRightUI,
|
||||
renderMenuLinks,
|
||||
renderCustomFooter,
|
||||
renderCustomStats,
|
||||
renderCustomSidebar,
|
||||
@ -220,8 +218,7 @@ const LayerUI = ({
|
||||
actionManager.renderAction("loadScene")}
|
||||
{/* // TODO barnabasmolnar/editor-redesign */}
|
||||
{/* is this fine here? */}
|
||||
{UIOptions.canvasActions.saveToActiveFile &&
|
||||
appState.fileHandle &&
|
||||
{appState.fileHandle &&
|
||||
actionManager.renderAction("saveToActiveFile")}
|
||||
{renderJSONExportDialog()}
|
||||
{UIOptions.canvasActions.saveAsImage && (
|
||||
@ -243,16 +240,8 @@ const LayerUI = ({
|
||||
{actionManager.renderAction("toggleShortcuts", undefined, true)}
|
||||
{!appState.viewModeEnabled &&
|
||||
actionManager.renderAction("clearCanvas")}
|
||||
{typeof renderMenuLinks === "undefined" ? ( //zsviczian
|
||||
<Separator />
|
||||
) : (
|
||||
renderMenuLinks && <Separator />
|
||||
)}
|
||||
{typeof renderMenuLinks === "undefined" ? ( //zsviczian
|
||||
<MenuLinks />
|
||||
) : (
|
||||
renderMenuLinks && renderMenuLinks(device.isMobile, appState)
|
||||
)}
|
||||
<Separator />
|
||||
<MenuLinks />
|
||||
<Separator />
|
||||
<div
|
||||
style={{
|
||||
@ -262,11 +251,9 @@ const LayerUI = ({
|
||||
}}
|
||||
>
|
||||
<div>{actionManager.renderAction("toggleTheme")}</div>
|
||||
{UIOptions.showLanguageList !== false && (
|
||||
<div style={{ padding: "0 0.625rem" }}>
|
||||
<LanguageList style={{ width: "100%" }} />
|
||||
</div>
|
||||
)}
|
||||
<div style={{ padding: "0 0.625rem" }}>
|
||||
<LanguageList style={{ width: "100%" }} />
|
||||
</div>
|
||||
{!appState.viewModeEnabled && (
|
||||
<div>
|
||||
<div style={{ fontSize: ".75rem", marginBottom: ".5rem" }}>
|
||||
@ -424,7 +411,8 @@ const LayerUI = ({
|
||||
onClick={onCollabButtonClick}
|
||||
/>
|
||||
)}
|
||||
{renderTopRightUI?.(device.isMobile, appState)}
|
||||
{!appState.viewModeEnabled &&
|
||||
renderTopRightUI?.(device.isMobile, appState)}
|
||||
{!appState.viewModeEnabled && (
|
||||
<LibraryButton appState={appState} setAppState={setAppState} />
|
||||
)}
|
||||
@ -497,11 +485,9 @@ const LayerUI = ({
|
||||
renderCustomFooter={renderCustomFooter}
|
||||
onImageAction={onImageAction}
|
||||
renderTopRightUI={renderTopRightUI}
|
||||
renderMenuLinks={renderMenuLinks}
|
||||
renderCustomStats={renderCustomStats}
|
||||
renderSidebars={renderSidebars}
|
||||
device={device}
|
||||
UIOptions={UIOptions}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
@ -22,7 +22,7 @@ export const LibraryButton: React.FC<{
|
||||
}
|
||||
|
||||
return (
|
||||
<label title={`${capitalizeString(t("toolBar.library"))} — 0`}>
|
||||
<label title={`${capitalizeString(t("toolBar.library"))}`}>
|
||||
<input
|
||||
className="ToolIcon_type_checkbox"
|
||||
type="checkbox"
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React from "react";
|
||||
import { AppProps, AppState, Device, ExcalidrawProps } from "../types";
|
||||
import { AppState, Device, ExcalidrawProps } from "../types";
|
||||
import { ActionManager } from "../actions/manager";
|
||||
import { t } from "../i18n";
|
||||
import Stack from "./Stack";
|
||||
@ -45,12 +45,10 @@ type MobileMenuProps = {
|
||||
isMobile: boolean,
|
||||
appState: AppState,
|
||||
) => JSX.Element | null;
|
||||
renderMenuLinks?: ExcalidrawProps["renderMenuLinks"];
|
||||
renderCustomStats?: ExcalidrawProps["renderCustomStats"];
|
||||
renderSidebars: () => JSX.Element | null;
|
||||
device: Device;
|
||||
renderWelcomeScreen?: boolean;
|
||||
UIOptions: AppProps["UIOptions"];
|
||||
};
|
||||
|
||||
export const MobileMenu = ({
|
||||
@ -68,12 +66,10 @@ export const MobileMenu = ({
|
||||
renderCustomFooter,
|
||||
onImageAction,
|
||||
renderTopRightUI,
|
||||
renderMenuLinks,
|
||||
renderCustomStats,
|
||||
renderSidebars,
|
||||
device,
|
||||
renderWelcomeScreen,
|
||||
UIOptions,
|
||||
}: MobileMenuProps) => {
|
||||
const renderToolbar = () => {
|
||||
return (
|
||||
@ -116,7 +112,8 @@ export const MobileMenu = ({
|
||||
</Stack.Row>
|
||||
</Island>
|
||||
<div className="mobile-misc-tools-container">
|
||||
{renderTopRightUI && renderTopRightUI(true, appState)}
|
||||
{!appState.viewModeEnabled &&
|
||||
renderTopRightUI?.(true, appState)}
|
||||
<PenModeButton
|
||||
checked={appState.penMode}
|
||||
onChange={onPenModeToggle}
|
||||
@ -196,14 +193,12 @@ export const MobileMenu = ({
|
||||
{!appState.viewModeEnabled && actionManager.renderAction("loadScene")}
|
||||
{renderJSONExportDialog()}
|
||||
{renderImageExportDialog()}
|
||||
{UIOptions.canvasActions.saveAsImage && (
|
||||
<MenuItem
|
||||
label={t("buttons.exportImage")}
|
||||
icon={ExportImageIcon}
|
||||
dataTestId="image-export-button"
|
||||
onClick={() => setAppState({ openDialog: "imageExport" })}
|
||||
/>
|
||||
)}
|
||||
<MenuItem
|
||||
label={t("buttons.exportImage")}
|
||||
icon={ExportImageIcon}
|
||||
dataTestId="image-export-button"
|
||||
onClick={() => setAppState({ openDialog: "imageExport" })}
|
||||
/>
|
||||
{onCollabButtonClick && (
|
||||
<CollabButton
|
||||
isCollaborating={isCollaborating}
|
||||
@ -213,16 +208,8 @@ export const MobileMenu = ({
|
||||
)}
|
||||
{actionManager.renderAction("toggleShortcuts", undefined, true)}
|
||||
{!appState.viewModeEnabled && actionManager.renderAction("clearCanvas")}
|
||||
{typeof renderMenuLinks === "undefined" ? ( //zsviczian
|
||||
<Separator />
|
||||
) : (
|
||||
renderMenuLinks && <Separator />
|
||||
)}
|
||||
{typeof renderMenuLinks === "undefined" ? ( //zsviczian
|
||||
<MenuLinks />
|
||||
) : (
|
||||
renderMenuLinks && renderMenuLinks(device.isMobile, appState)
|
||||
)}
|
||||
<Separator />
|
||||
<MenuLinks />
|
||||
<Separator />
|
||||
{!appState.viewModeEnabled && (
|
||||
<div style={{ marginBottom: ".5rem" }}>
|
||||
|
@ -1470,11 +1470,11 @@ export const TextAlignRightIcon = createIcon(
|
||||
export const TextAlignTopIcon = React.memo(({ theme }: { theme: Theme }) =>
|
||||
createIcon(
|
||||
<g
|
||||
stroke-width="1.5"
|
||||
strokeWidth="1.5"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="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
|
||||
stroke-width="2"
|
||||
strokeWidth="2"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="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
|
||||
stroke-width="1.5"
|
||||
strokeWidth="1.5"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<line x1="4" y1="12" x2="9" y2="12" />
|
||||
|
@ -169,8 +169,7 @@ const getAdjustedDimensions = (
|
||||
let maxWidth = null;
|
||||
const container = getContainerElement(element);
|
||||
if (container) {
|
||||
const containerDims = getContainerDims(container);
|
||||
maxWidth = containerDims.width - BOUND_TEXT_PADDING * 2;
|
||||
maxWidth = getMaxContainerWidth(container);
|
||||
}
|
||||
const {
|
||||
width: nextWidth,
|
||||
@ -258,7 +257,6 @@ export const refreshTextDimensions = (
|
||||
) => {
|
||||
const container = getContainerElement(textElement);
|
||||
if (container) {
|
||||
// text = wrapText(text, getFontString(textElement), container.width);
|
||||
text = wrapText(
|
||||
text,
|
||||
getFontString(textElement),
|
||||
|
@ -19,13 +19,12 @@ export const redrawTextBoundingBox = (
|
||||
) => {
|
||||
let maxWidth = undefined;
|
||||
let text = textElement.text;
|
||||
|
||||
if (container) {
|
||||
maxWidth = getMaxContainerWidth(container);
|
||||
text = wrapText(
|
||||
textElement.originalText,
|
||||
getFontString(textElement),
|
||||
getMaxContainerWidth(container),
|
||||
maxWidth,
|
||||
);
|
||||
}
|
||||
const metrics = measureText(
|
||||
@ -230,10 +229,9 @@ 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;
|
||||
document.body.removeChild(container);
|
||||
|
||||
document.body.removeChild(container);
|
||||
return { width, height, baseline };
|
||||
};
|
||||
|
||||
|
@ -10,11 +10,13 @@ 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")!);
|
||||
|
||||
@ -433,6 +435,42 @@ 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", () => {
|
||||
@ -876,5 +914,125 @@ 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,6 +20,7 @@ import {
|
||||
getBoundTextElementId,
|
||||
getContainerDims,
|
||||
getContainerElement,
|
||||
measureText,
|
||||
wrapText,
|
||||
} from "./textElement";
|
||||
import {
|
||||
@ -29,6 +30,7 @@ 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 (
|
||||
@ -275,6 +277,38 @@ 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,16 +310,27 @@ class Collab extends PureComponent<Props, CollabState> {
|
||||
}
|
||||
};
|
||||
|
||||
private fetchImageFilesFromFirebase = async (scene: {
|
||||
private fetchImageFilesFromFirebase = async (opts: {
|
||||
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 = scene.elements
|
||||
const unfetchedImages = opts.elements
|
||||
.filter((element) => {
|
||||
return (
|
||||
isInitializedImageElement(element) &&
|
||||
!this.fileManager.isFileHandled(element.fileId) &&
|
||||
!element.isDeleted &&
|
||||
element.status === "saved"
|
||||
(opts.forceFetchFiles
|
||||
? element.status !== "pending" ||
|
||||
Date.now() - element.updated > 10000
|
||||
: element.status === "saved")
|
||||
);
|
||||
})
|
||||
.map((element) => (element as InitializedExcalidrawImageElement).fileId);
|
||||
|
@ -195,6 +195,7 @@ export const encodeFilesForUpload = async ({
|
||||
id,
|
||||
mimeType: fileData.mimeType,
|
||||
created: Date.now(),
|
||||
lastRetrieved: Date.now(),
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -10,7 +10,7 @@
|
||||
* (localStorage, indexedDB).
|
||||
*/
|
||||
|
||||
import { createStore, keys, del, getMany, set } from "idb-keyval";
|
||||
import { createStore, entries, del, getMany, set, setMany } from "idb-keyval";
|
||||
import { clearAppStateForLocalStorage } from "../../appState";
|
||||
import { clearElementsForLocalStorage } from "../../element";
|
||||
import { ExcalidrawElement, FileId } from "../../element/types";
|
||||
@ -25,12 +25,21 @@ const filesStore = createStore("files-db", "files-store");
|
||||
|
||||
class LocalFileManager extends FileManager {
|
||||
clearObsoleteFiles = async (opts: { currentFileIds: FileId[] }) => {
|
||||
const allIds = await keys(filesStore);
|
||||
for (const id of allIds) {
|
||||
if (!opts.currentFileIds.includes(id as FileId)) {
|
||||
del(id, filesStore);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
@ -111,18 +120,33 @@ export class LocalData {
|
||||
static fileStorage = new LocalFileManager({
|
||||
getFiles(ids) {
|
||||
return getMany(ids, filesStore).then(
|
||||
(filesData: (BinaryFileData | undefined)[]) => {
|
||||
async (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) {
|
||||
loadedFiles.push(data);
|
||||
const _data: BinaryFileData = {
|
||||
...data,
|
||||
lastRetrieved: Date.now(),
|
||||
};
|
||||
filesToSave.push([id, _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,6 +330,7 @@ export const loadFilesFromFirebase = async (
|
||||
id,
|
||||
dataURL,
|
||||
created: metadata?.created || Date.now(),
|
||||
lastRetrieved: metadata?.created || Date.now(),
|
||||
});
|
||||
} else {
|
||||
erroredFiles.set(id, true);
|
||||
|
@ -285,6 +285,7 @@ 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,6 +63,17 @@ 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;
|
||||
|
@ -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. |
|
||||
| [`langCode`](#langCode) | string | `en` | Language code string |
|
||||
| [`renderTopRightUI`](#renderTopRightUI) | Function | | Function that renders custom UI in top right corner |
|
||||
| [`hideWelcomeScreen`](#hideWelcomeScreen) | boolean | | This implies if the app should always hide the welcome sreen |
|
||||
| [`renderMenuLinks`](#renderMenuLinks) | Function | | Function that renders custom list of links (or other custom UI) in the app menu |
|
||||
| [`renderFooter `](#renderFooter) | Function | | Function that renders custom UI footer |
|
||||
| [`renderCustomStats`](#renderCustomStats) | Function | | Function that can be used to render custom stats on the stats dialog. |
|
||||
| [`renderSIdebar`](#renderSIdebar) | Function | | Render function that renders custom sidebar. |
|
||||
@ -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.
|
||||
|
||||
#### `hideWelcomeScreen`
|
||||
|
||||
<pre>
|
||||
boolean
|
||||
</pre>
|
||||
|
||||
Boolean value to override the displaying of the welcome screen elements. If set to true, the welcome screen will never be shown.
|
||||
|
||||
#### `renderMenuLinks`
|
||||
|
||||
<pre>
|
||||
((isMobile: boolean, appState: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L79">AppState</a>) => JSX | null)|null
|
||||
</pre>
|
||||
|
||||
A function returning JSX to render custom UI (intended to be a list of custom links) replacing the default list of links in the app menu. If set to null, the list of links will not be displayed. If unset, the default list of links will be displayed.
|
||||
|
||||
#### `renderFooter`
|
||||
|
||||
<pre>
|
||||
@ -703,7 +685,7 @@ This prop sets the name of the drawing which will be used when exporting the dra
|
||||
|
||||
#### `UIOptions`
|
||||
|
||||
This prop can be used to customise UI of Excalidraw. Currently we support customising [`canvasActions`](#canvasActions), [`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>
|
||||
{ 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
|
||||
|
||||

|
||||
|
||||
### `showLanguageList`
|
||||
|
||||
Boolean prop. If set to `true` the `language selector dropdown list` will be hidden in the app menu. If `false` or `undefined` the language dropdown will be rendered.
|
||||
|
||||
#### `exportOpts`
|
||||
|
||||
The below attributes can be set in `UIOptions.canvasActions.export` to customize the export dialog. If `UIOptions.canvasActions.export` is `false` the export button will not be rendered.
|
||||
|
@ -148,6 +148,7 @@ export default function App() {
|
||||
dataURL: reader.result as BinaryFileData["dataURL"],
|
||||
mimeType: MIME_TYPES.jpg,
|
||||
created: 1644915140367,
|
||||
lastRetrieved: 1644915140367,
|
||||
},
|
||||
];
|
||||
|
||||
|
@ -20,8 +20,6 @@ const ExcalidrawBase = (props: ExcalidrawProps) => {
|
||||
isCollaborating = false,
|
||||
onPointerUpdate,
|
||||
renderTopRightUI,
|
||||
hideWelcomeScreen,
|
||||
renderMenuLinks,
|
||||
renderFooter,
|
||||
renderSidebar,
|
||||
langCode = defaultLang.code,
|
||||
@ -95,8 +93,6 @@ const ExcalidrawBase = (props: ExcalidrawProps) => {
|
||||
isCollaborating={isCollaborating}
|
||||
onPointerUpdate={onPointerUpdate}
|
||||
renderTopRightUI={renderTopRightUI}
|
||||
hideWelcomeScreen={hideWelcomeScreen}
|
||||
renderMenuLinks={renderMenuLinks}
|
||||
renderFooter={renderFooter}
|
||||
langCode={langCode}
|
||||
viewModeEnabled={viewModeEnabled}
|
||||
|
@ -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 sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=
|
||||
integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==
|
||||
|
||||
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.2"
|
||||
resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.2.tgz#d6e3b4fb81870721ae4e0868ab11dd638368c129"
|
||||
integrity sha512-TM57VeHptv569d/GKh6TAYdzKblwDNiumOdkFnejjD0XwTH87K90w3O7AiJRqdQoXygvi1VQTJTLGhJl7WqA7A==
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.3.tgz#d4b15b8504c63d1fc3f2ade52d41bc8459d6ede1"
|
||||
integrity sha512-THWqIsn8QRnvLl0shHYVBN9syumU8pYWEHPTmkiVGd+7K5eFNVSY6AJhRvgGF70gg1Dz+l/k8WicvFCxdEs60A==
|
||||
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.0.4"
|
||||
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
|
||||
integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==
|
||||
version "3.1.2"
|
||||
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b"
|
||||
integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==
|
||||
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.0"
|
||||
resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.0.tgz#e4cace5b816d425a166b5f097e10cd12b36064b0"
|
||||
integrity sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==
|
||||
version "2.0.4"
|
||||
resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.4.tgz#8b5cb38b5c34a9a018ee1fc0e6a066d1dfcc528c"
|
||||
integrity sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==
|
||||
dependencies:
|
||||
big.js "^5.2.2"
|
||||
emojis-list "^3.0.0"
|
||||
|
@ -406,6 +406,8 @@ export const _renderScene = ({
|
||||
}),
|
||||
);
|
||||
|
||||
let editingLinearElement: NonDeleted<ExcalidrawLinearElement> | undefined =
|
||||
undefined;
|
||||
visibleElements.forEach((element) => {
|
||||
try {
|
||||
renderElement(element, rc, context, renderConfig);
|
||||
@ -414,15 +416,10 @@ export const _renderScene = ({
|
||||
// correct element from visible elements
|
||||
if (appState.editingLinearElement?.elementId === element.id) {
|
||||
if (element) {
|
||||
renderLinearPointHandles(
|
||||
context,
|
||||
appState,
|
||||
renderConfig,
|
||||
element as NonDeleted<ExcalidrawLinearElement>,
|
||||
);
|
||||
editingLinearElement =
|
||||
element as NonDeleted<ExcalidrawLinearElement>;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isExporting) {
|
||||
renderLinkIcon(element, context, appState);
|
||||
}
|
||||
@ -431,6 +428,15 @@ export const _renderScene = ({
|
||||
}
|
||||
});
|
||||
|
||||
if (editingLinearElement) {
|
||||
renderLinearPointHandles(
|
||||
context,
|
||||
appState,
|
||||
renderConfig,
|
||||
editingLinearElement,
|
||||
);
|
||||
}
|
||||
|
||||
// Paint selection element
|
||||
if (appState.selectionElement) {
|
||||
try {
|
||||
|
@ -17,60 +17,70 @@ 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.P, KEYS.L],
|
||||
key: KEYS.L,
|
||||
numericKey: KEYS["6"],
|
||||
fillable: true,
|
||||
},
|
||||
{
|
||||
icon: FreedrawIcon,
|
||||
value: "freedraw",
|
||||
key: [KEYS.X, KEYS.P.toUpperCase()],
|
||||
key: [KEYS.P, KEYS.X],
|
||||
numericKey: KEYS["7"],
|
||||
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;
|
||||
@ -78,7 +88,7 @@ export const SHAPES = [
|
||||
export const findShapeByKey = (key: string) => {
|
||||
const shape = SHAPES.find((shape, index) => {
|
||||
return (
|
||||
key === (shape.value === "eraser" ? 0 : index + 1).toString() ||
|
||||
(shape.numericKey != null && key === shape.numericKey.toString()) ||
|
||||
(shape.key &&
|
||||
(typeof shape.key === "string"
|
||||
? shape.key === key
|
||||
|
@ -12410,6 +12410,234 @@ exports[`regression tests key o selects ellipse tool: [end of test] number of el
|
||||
|
||||
exports[`regression tests key o selects ellipse tool: [end of test] number of renders 1`] = `9`;
|
||||
|
||||
exports[`regression tests key p selects freedraw tool: [end of test] appState 1`] = `
|
||||
Object {
|
||||
"activeTool": Object {
|
||||
"customType": null,
|
||||
"lastActiveToolBeforeEraser": null,
|
||||
"locked": false,
|
||||
"type": "freedraw",
|
||||
},
|
||||
"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 {},
|
||||
"resizingElement": null,
|
||||
"scrollX": 0,
|
||||
"scrollY": 0,
|
||||
"scrolledOutside": false,
|
||||
"selectedElementIds": Object {
|
||||
"id0": false,
|
||||
},
|
||||
"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 key p selects freedraw tool: [end of test] element 0 1`] = `
|
||||
Object {
|
||||
"angle": 0,
|
||||
"backgroundColor": "transparent",
|
||||
"boundElements": null,
|
||||
"fillStyle": "hachure",
|
||||
"groupIds": Array [],
|
||||
"height": 10,
|
||||
"id": "id0",
|
||||
"isDeleted": false,
|
||||
"lastCommittedPoint": Array [
|
||||
10,
|
||||
10,
|
||||
],
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"opacity": 100,
|
||||
"points": Array [
|
||||
Array [
|
||||
0,
|
||||
0,
|
||||
],
|
||||
Array [
|
||||
10,
|
||||
10,
|
||||
],
|
||||
Array [
|
||||
10,
|
||||
10,
|
||||
],
|
||||
],
|
||||
"pressures": Array [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
],
|
||||
"roughness": 1,
|
||||
"seed": 337897,
|
||||
"simulatePressure": false,
|
||||
"strokeColor": "#000000",
|
||||
"strokeSharpness": "round",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"type": "freedraw",
|
||||
"updated": 1,
|
||||
"version": 4,
|
||||
"versionNonce": 453191,
|
||||
"width": 10,
|
||||
"x": 10,
|
||||
"y": 10,
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`regression tests key p selects freedraw tool: [end of test] 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": false,
|
||||
},
|
||||
"selectedGroupIds": Object {},
|
||||
"viewBackgroundColor": "#ffffff",
|
||||
},
|
||||
"elements": Array [
|
||||
Object {
|
||||
"angle": 0,
|
||||
"backgroundColor": "transparent",
|
||||
"boundElements": null,
|
||||
"fillStyle": "hachure",
|
||||
"groupIds": Array [],
|
||||
"height": 10,
|
||||
"id": "id0",
|
||||
"isDeleted": false,
|
||||
"lastCommittedPoint": Array [
|
||||
10,
|
||||
10,
|
||||
],
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"opacity": 100,
|
||||
"points": Array [
|
||||
Array [
|
||||
0,
|
||||
0,
|
||||
],
|
||||
Array [
|
||||
10,
|
||||
10,
|
||||
],
|
||||
Array [
|
||||
10,
|
||||
10,
|
||||
],
|
||||
],
|
||||
"pressures": Array [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
],
|
||||
"roughness": 1,
|
||||
"seed": 337897,
|
||||
"simulatePressure": false,
|
||||
"strokeColor": "#000000",
|
||||
"strokeSharpness": "round",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"type": "freedraw",
|
||||
"updated": 1,
|
||||
"version": 4,
|
||||
"versionNonce": 453191,
|
||||
"width": 10,
|
||||
"x": 10,
|
||||
"y": 10,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`regression tests key p selects freedraw tool: [end of test] number of elements 1`] = `1`;
|
||||
|
||||
exports[`regression tests key p selects freedraw tool: [end of test] number of renders 1`] = `9`;
|
||||
|
||||
exports[`regression tests key r selects rectangle tool: [end of test] appState 1`] = `
|
||||
Object {
|
||||
"activeTool": Object {
|
||||
@ -12590,234 +12818,6 @@ exports[`regression tests key r selects rectangle tool: [end of test] number of
|
||||
|
||||
exports[`regression tests key r selects rectangle tool: [end of test] number of renders 1`] = `9`;
|
||||
|
||||
exports[`regression tests key x selects freedraw tool: [end of test] appState 1`] = `
|
||||
Object {
|
||||
"activeTool": Object {
|
||||
"customType": null,
|
||||
"lastActiveToolBeforeEraser": null,
|
||||
"locked": false,
|
||||
"type": "freedraw",
|
||||
},
|
||||
"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 {},
|
||||
"resizingElement": null,
|
||||
"scrollX": 0,
|
||||
"scrollY": 0,
|
||||
"scrolledOutside": false,
|
||||
"selectedElementIds": Object {
|
||||
"id0": false,
|
||||
},
|
||||
"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 key x selects freedraw tool: [end of test] element 0 1`] = `
|
||||
Object {
|
||||
"angle": 0,
|
||||
"backgroundColor": "transparent",
|
||||
"boundElements": null,
|
||||
"fillStyle": "hachure",
|
||||
"groupIds": Array [],
|
||||
"height": 10,
|
||||
"id": "id0",
|
||||
"isDeleted": false,
|
||||
"lastCommittedPoint": Array [
|
||||
10,
|
||||
10,
|
||||
],
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"opacity": 100,
|
||||
"points": Array [
|
||||
Array [
|
||||
0,
|
||||
0,
|
||||
],
|
||||
Array [
|
||||
10,
|
||||
10,
|
||||
],
|
||||
Array [
|
||||
10,
|
||||
10,
|
||||
],
|
||||
],
|
||||
"pressures": Array [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
],
|
||||
"roughness": 1,
|
||||
"seed": 337897,
|
||||
"simulatePressure": false,
|
||||
"strokeColor": "#000000",
|
||||
"strokeSharpness": "round",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"type": "freedraw",
|
||||
"updated": 1,
|
||||
"version": 4,
|
||||
"versionNonce": 453191,
|
||||
"width": 10,
|
||||
"x": 10,
|
||||
"y": 10,
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`regression tests key x selects freedraw tool: [end of test] 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": false,
|
||||
},
|
||||
"selectedGroupIds": Object {},
|
||||
"viewBackgroundColor": "#ffffff",
|
||||
},
|
||||
"elements": Array [
|
||||
Object {
|
||||
"angle": 0,
|
||||
"backgroundColor": "transparent",
|
||||
"boundElements": null,
|
||||
"fillStyle": "hachure",
|
||||
"groupIds": Array [],
|
||||
"height": 10,
|
||||
"id": "id0",
|
||||
"isDeleted": false,
|
||||
"lastCommittedPoint": Array [
|
||||
10,
|
||||
10,
|
||||
],
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"opacity": 100,
|
||||
"points": Array [
|
||||
Array [
|
||||
0,
|
||||
0,
|
||||
],
|
||||
Array [
|
||||
10,
|
||||
10,
|
||||
],
|
||||
Array [
|
||||
10,
|
||||
10,
|
||||
],
|
||||
],
|
||||
"pressures": Array [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
],
|
||||
"roughness": 1,
|
||||
"seed": 337897,
|
||||
"simulatePressure": false,
|
||||
"strokeColor": "#000000",
|
||||
"strokeSharpness": "round",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"type": "freedraw",
|
||||
"updated": 1,
|
||||
"version": 4,
|
||||
"versionNonce": 453191,
|
||||
"width": 10,
|
||||
"x": 10,
|
||||
"y": 10,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`regression tests key x selects freedraw tool: [end of test] number of elements 1`] = `1`;
|
||||
|
||||
exports[`regression tests key x selects freedraw tool: [end of test] number of renders 1`] = `9`;
|
||||
|
||||
exports[`regression tests make a group and duplicate it: [end of test] appState 1`] = `
|
||||
Object {
|
||||
"activeTool": Object {
|
||||
|
@ -158,6 +158,7 @@ describe("export", () => {
|
||||
dataURL: await getDataURL(await API.loadFile("./fixtures/deer.png")),
|
||||
mimeType: "image/png",
|
||||
created: Date.now(),
|
||||
lastRetrieved: Date.now(),
|
||||
},
|
||||
} as const;
|
||||
|
||||
|
@ -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,8 +89,15 @@ describe(" Test Linear Elements", () => {
|
||||
mouse.clickAt(p1[0], p1[1]);
|
||||
};
|
||||
|
||||
const enterLineEditingMode = (line: ExcalidrawLinearElement) => {
|
||||
mouse.clickAt(p1[0], p1[1]);
|
||||
const enterLineEditingMode = (
|
||||
line: ExcalidrawLinearElement,
|
||||
selectProgrammatically = false,
|
||||
) => {
|
||||
if (selectProgrammatically) {
|
||||
API.setSelectedElements([line]);
|
||||
} else {
|
||||
mouse.clickAt(p1[0], p1[1]);
|
||||
}
|
||||
Keyboard.keyPress(KEYS.ENTER);
|
||||
expect(h.state.editingLinearElement?.elementId).toEqual(line.id);
|
||||
};
|
||||
@ -604,5 +611,43 @@ 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]},
|
||||
],
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -138,7 +138,7 @@ describe("regression tests", () => {
|
||||
[`4${KEYS.O}`, "ellipse", true],
|
||||
[`5${KEYS.A}`, "arrow", true],
|
||||
[`6${KEYS.L}`, "line", true],
|
||||
[`7${KEYS.X}`, "freedraw", false],
|
||||
[`7${KEYS.P}`, "freedraw", false],
|
||||
] as [string, ExcalidrawElement["type"], boolean][]) {
|
||||
for (const key of keys) {
|
||||
it(`key ${key} selects ${shape} tool`, () => {
|
||||
|
17
src/types.ts
17
src/types.ts
@ -61,7 +61,18 @@ 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">;
|
||||
@ -284,10 +295,6 @@ export interface ExcalidrawProps {
|
||||
isMobile: boolean,
|
||||
appState: AppState,
|
||||
) => JSX.Element | null;
|
||||
renderMenuLinks?:
|
||||
| ((isMobile: boolean, appState: AppState) => JSX.Element | null)
|
||||
| null;
|
||||
hideWelcomeScreen?: boolean;
|
||||
renderFooter?: (isMobile: boolean, appState: AppState) => JSX.Element | null;
|
||||
langCode?: Language["code"];
|
||||
viewModeEnabled?: boolean;
|
||||
@ -303,7 +310,6 @@ export interface ExcalidrawProps {
|
||||
UIOptions?: {
|
||||
dockedSidebarBreakpoint?: number;
|
||||
canvasActions?: CanvasActions;
|
||||
showLanguageList?: boolean;
|
||||
};
|
||||
detectScroll?: boolean;
|
||||
handleKeyboardGlobally?: boolean;
|
||||
@ -376,7 +382,6 @@ export type AppProps = Merge<
|
||||
UIOptions: {
|
||||
canvasActions: Required<CanvasActions> & { export: ExportOpts };
|
||||
dockedSidebarBreakpoint?: number;
|
||||
showLanguageList?: boolean;
|
||||
};
|
||||
detectScroll: boolean;
|
||||
handleKeyboardGlobally: boolean;
|
||||
|
@ -11263,9 +11263,9 @@ socket.io-client@2.3.1:
|
||||
to-array "0.1.4"
|
||||
|
||||
socket.io-parser@~3.3.0:
|
||||
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==
|
||||
version "3.3.3"
|
||||
resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-3.3.3.tgz#3a8b84823eba87f3f7624e64a8aaab6d6318a72f"
|
||||
integrity sha512-qOg87q1PMWWTeO01768Yh9ogn7chB9zkKtQnya41Y355S0UmpXgpcrFwAgjYJxu9BdKug5r5e9YtVSeWhKBUZg==
|
||||
dependencies:
|
||||
component-emitter "~1.3.0"
|
||||
debug "~3.1.0"
|
||||
|
Reference in New Issue
Block a user