Compare commits

..

11 Commits

Author SHA1 Message Date
Aakansha Doshi 555bf6338f fix test 2022-11-09 22:55:27 +05:30
Daniel J. Geiger 0bef3945f6 Fix #5855. 2022-11-09 07:54:49 -06:00
Aakansha Doshi dc97dc30bf fix: disable FAST_REFRESH to fix live reload (#5852) 2022-11-09 17:13:20 +05:30
DanielJGeiger a0ecfed4cd fix: Paste clipboard contents into unbound text elements (#5849)
* Fix #5848.

* Add test.

* some tweaks

Co-authored-by: Aakansha Doshi <aakansha1216@gmail.com>
2022-11-09 11:30:22 +05:30
Aakansha Doshi e201e79cd0 fix: compute dimensions of container correctly when text pasted on container (#5845)
* fix: compute dimensions of container correctly when text pasted on container

* add test

* remove only
2022-11-08 19:50:41 +05:30
Pritam Sangani e1c5c706c6 build: stops ignoring .env files from docker context so env variables get set during react app build. (#5809)
build: stops ignoring .env.development and .env.production files from docker context so env variables get set during react app build.
* this fixes the issue where Browse Libraries button link was broken in
  docker/self-hosted versions of excalidraw
2022-11-07 16:48:38 +05:30
David Luzar bdc56090d7 feat: reintroduce x shortcut for freedraw (#5840) 2022-11-06 23:07:15 +01:00
David Luzar 58accc9310 feat: tweak toolbar shortcuts & remove library shortcut (#5832) 2022-11-06 20:14:53 +01:00
David Luzar b91158198e feat: clean unused images only after 24hrs (local-only) (#5839)
* feat: clean unused images only after 24hrs (local-only)

* fix test

* make optional for now
2022-11-06 19:41:14 +01:00
David Luzar 938ce241ff feat: refetch errored/pending images on collab room init load (#5833) 2022-11-05 15:55:14 +01:00
David Luzar 0228646507 fix: line editor points rendering below elements (#5781)
* fix: line editor points rendering below elements

* add test
2022-11-05 11:35:53 +01:00
28 changed files with 554 additions and 373 deletions
+2 -1
View File
@@ -1,5 +1,6 @@
* *
!.env !.env.development
!.env.production
!.eslintrc.json !.eslintrc.json
!.npmrc !.npmrc
!.prettierrc !.prettierrc
+2
View File
@@ -20,3 +20,5 @@ REACT_APP_DEV_ENABLE_SW=
# whether to disable live reload / HMR. Usuaully what you want to do when # whether to disable live reload / HMR. Usuaully what you want to do when
# debugging Service Workers. # debugging Service Workers.
REACT_APP_DEV_DISABLE_LIVE_RELOAD= REACT_APP_DEV_DISABLE_LIVE_RELOAD=
FAST_REFRESH=false
+4 -5
View File
@@ -218,13 +218,12 @@ export const ShapesSwitcher = ({
appState: AppState; appState: AppState;
}) => ( }) => (
<> <>
{SHAPES.map(({ value, icon, key, fillable }, index) => { {SHAPES.map(({ value, icon, key, numericKey, fillable }, index) => {
const numberKey = value === "eraser" ? 0 : index + 1;
const label = t(`toolBar.${value}`); const label = t(`toolBar.${value}`);
const letter = key && (typeof key === "string" ? key : key[0]); const letter = key && (typeof key === "string" ? key : key[0]);
const shortcut = letter const shortcut = letter
? `${capitalizeString(letter)} ${t("helpDialog.or")} ${numberKey}` ? `${capitalizeString(letter)} ${t("helpDialog.or")} ${numericKey}`
: `${numberKey}`; : `${numericKey}`;
return ( return (
<ToolButton <ToolButton
className={clsx("Shape", { fillable })} className={clsx("Shape", { fillable })}
@@ -234,7 +233,7 @@ export const ShapesSwitcher = ({
checked={activeTool.type === value} checked={activeTool.type === value}
name="editor-current-shape" name="editor-current-shape"
title={`${capitalizeString(label)}${shortcut}`} title={`${capitalizeString(label)}${shortcut}`}
keyBindingLabel={`${numberKey}`} keyBindingLabel={numericKey}
aria-label={capitalizeString(label)} aria-label={capitalizeString(label)}
aria-keyshortcuts={shortcut} aria-keyshortcuts={shortcut}
data-testid={`toolbar-${value}`} data-testid={`toolbar-${value}`}
+1 -15
View File
@@ -516,7 +516,6 @@ class App extends React.Component<AppProps, AppState> {
const { const {
onCollabButtonClick, onCollabButtonClick,
renderTopRightUI, renderTopRightUI,
renderMenuLinks,
renderFooter, renderFooter,
renderCustomStats, renderCustomStats,
} = this.props; } = this.props;
@@ -563,7 +562,6 @@ class App extends React.Component<AppProps, AppState> {
langCode={getLanguage().code} langCode={getLanguage().code}
isCollaborating={this.props.isCollaborating} isCollaborating={this.props.isCollaborating}
renderTopRightUI={renderTopRightUI} renderTopRightUI={renderTopRightUI}
renderMenuLinks={renderMenuLinks}
renderCustomFooter={renderFooter} renderCustomFooter={renderFooter}
renderCustomStats={renderCustomStats} renderCustomStats={renderCustomStats}
renderCustomSidebar={this.props.renderSidebar} renderCustomSidebar={this.props.renderSidebar}
@@ -578,7 +576,6 @@ class App extends React.Component<AppProps, AppState> {
id={this.id} id={this.id}
onImageAction={this.onImageAction} onImageAction={this.onImageAction}
renderWelcomeScreen={ renderWelcomeScreen={
this.props.hideWelcomeScreen !== true &&
this.state.showWelcomeScreen && this.state.showWelcomeScreen &&
this.state.activeTool.type === "selection" && this.state.activeTool.type === "selection" &&
!this.scene.getElementsIncludingDeleted().length !this.scene.getElementsIncludingDeleted().length
@@ -1915,18 +1912,6 @@ class App extends React.Component<AppProps, AppState> {
this.setState({ isBindingEnabled: false }); 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)) { if (isArrowKey(event.key)) {
const step = const step =
(this.state.gridSize && (this.state.gridSize &&
@@ -5246,6 +5231,7 @@ class App extends React.Component<AppProps, AppState> {
id: fileId, id: fileId,
dataURL, dataURL,
created: Date.now(), created: Date.now(),
lastRetrieved: Date.now(),
}, },
}; };
const cachedImageData = this.imageCache.get(fileId); const cachedImageData = this.imageCache.get(fileId);
+33 -13
View File
@@ -1,6 +1,6 @@
import React from "react"; import React from "react";
import { t } from "../i18n"; import { t } from "../i18n";
import { isDarwin, isWindows } from "../keys"; import { isDarwin, isWindows, KEYS } from "../keys";
import { Dialog } from "./Dialog"; import { Dialog } from "./Dialog";
import { getShortcutKey } from "../utils"; import { getShortcutKey } from "../utils";
import "./HelpDialog.scss"; import "./HelpDialog.scss";
@@ -118,22 +118,42 @@ export const HelpDialog = ({ onClose }: { onClose?: () => void }) => {
className="HelpDialog__island--tools" className="HelpDialog__island--tools"
caption={t("helpDialog.tools")} caption={t("helpDialog.tools")}
> >
<Shortcut label={t("toolBar.selection")} shortcuts={["V", "1"]} /> <Shortcut
<Shortcut label={t("toolBar.rectangle")} shortcuts={["R", "2"]} /> label={t("toolBar.selection")}
<Shortcut label={t("toolBar.diamond")} shortcuts={["D", "3"]} /> shortcuts={[KEYS.V, KEYS["1"]]}
<Shortcut label={t("toolBar.ellipse")} shortcuts={["O", "4"]} /> />
<Shortcut label={t("toolBar.arrow")} shortcuts={["A", "5"]} /> <Shortcut
<Shortcut label={t("toolBar.line")} shortcuts={["P", "6"]} /> 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 <Shortcut
label={t("toolBar.freedraw")} label={t("toolBar.freedraw")}
shortcuts={["Shift + P", "X", "7"]} shortcuts={["Shift + P", KEYS["7"]]}
/> />
<Shortcut label={t("toolBar.text")} shortcuts={["T", "8"]} /> <Shortcut
<Shortcut label={t("toolBar.image")} shortcuts={["9"]} /> label={t("toolBar.text")}
<Shortcut label={t("toolBar.library")} shortcuts={["0"]} /> shortcuts={[KEYS.T, KEYS["8"]]}
/>
<Shortcut label={t("toolBar.image")} shortcuts={[KEYS["9"]]} />
<Shortcut <Shortcut
label={t("toolBar.eraser")} label={t("toolBar.eraser")}
shortcuts={[getShortcutKey("E")]} shortcuts={[KEYS.E, KEYS["0"]]}
/> />
<Shortcut <Shortcut
label={t("helpDialog.editSelectedShape")} label={t("helpDialog.editSelectedShape")}
@@ -173,7 +193,7 @@ export const HelpDialog = ({ onClose }: { onClose?: () => void }) => {
]} ]}
isOr={false} isOr={false}
/> />
<Shortcut label={t("toolBar.lock")} shortcuts={["Q"]} /> <Shortcut label={t("toolBar.lock")} shortcuts={[KEYS.Q]} />
<Shortcut <Shortcut
label={t("helpDialog.preventBinding")} label={t("helpDialog.preventBinding")}
shortcuts={[getShortcutKey("CtrlOrCmd")]} shortcuts={[getShortcutKey("CtrlOrCmd")]}
+6 -21
View File
@@ -71,7 +71,6 @@ interface LayerUIProps {
langCode: Language["code"]; langCode: Language["code"];
isCollaborating: boolean; isCollaborating: boolean;
renderTopRightUI?: ExcalidrawProps["renderTopRightUI"]; renderTopRightUI?: ExcalidrawProps["renderTopRightUI"];
renderMenuLinks?: ExcalidrawProps["renderMenuLinks"];
renderCustomFooter?: ExcalidrawProps["renderFooter"]; renderCustomFooter?: ExcalidrawProps["renderFooter"];
renderCustomStats?: ExcalidrawProps["renderCustomStats"]; renderCustomStats?: ExcalidrawProps["renderCustomStats"];
renderCustomSidebar?: ExcalidrawProps["renderSidebar"]; renderCustomSidebar?: ExcalidrawProps["renderSidebar"];
@@ -97,7 +96,6 @@ const LayerUI = ({
showExitZenModeBtn, showExitZenModeBtn,
isCollaborating, isCollaborating,
renderTopRightUI, renderTopRightUI,
renderMenuLinks,
renderCustomFooter, renderCustomFooter,
renderCustomStats, renderCustomStats,
renderCustomSidebar, renderCustomSidebar,
@@ -220,8 +218,7 @@ const LayerUI = ({
actionManager.renderAction("loadScene")} actionManager.renderAction("loadScene")}
{/* // TODO barnabasmolnar/editor-redesign */} {/* // TODO barnabasmolnar/editor-redesign */}
{/* is this fine here? */} {/* is this fine here? */}
{UIOptions.canvasActions.saveToActiveFile && {appState.fileHandle &&
appState.fileHandle &&
actionManager.renderAction("saveToActiveFile")} actionManager.renderAction("saveToActiveFile")}
{renderJSONExportDialog()} {renderJSONExportDialog()}
{UIOptions.canvasActions.saveAsImage && ( {UIOptions.canvasActions.saveAsImage && (
@@ -243,16 +240,8 @@ const LayerUI = ({
{actionManager.renderAction("toggleShortcuts", undefined, true)} {actionManager.renderAction("toggleShortcuts", undefined, true)}
{!appState.viewModeEnabled && {!appState.viewModeEnabled &&
actionManager.renderAction("clearCanvas")} actionManager.renderAction("clearCanvas")}
{typeof renderMenuLinks === "undefined" ? ( //zsviczian <Separator />
<Separator /> <MenuLinks />
) : (
renderMenuLinks && <Separator />
)}
{typeof renderMenuLinks === "undefined" ? ( //zsviczian
<MenuLinks />
) : (
renderMenuLinks && renderMenuLinks(device.isMobile, appState)
)}
<Separator /> <Separator />
<div <div
style={{ style={{
@@ -262,11 +251,9 @@ const LayerUI = ({
}} }}
> >
<div>{actionManager.renderAction("toggleTheme")}</div> <div>{actionManager.renderAction("toggleTheme")}</div>
{UIOptions.showLanguageList !== false && ( <div style={{ padding: "0 0.625rem" }}>
<div style={{ padding: "0 0.625rem" }}> <LanguageList style={{ width: "100%" }} />
<LanguageList style={{ width: "100%" }} /> </div>
</div>
)}
{!appState.viewModeEnabled && ( {!appState.viewModeEnabled && (
<div> <div>
<div style={{ fontSize: ".75rem", marginBottom: ".5rem" }}> <div style={{ fontSize: ".75rem", marginBottom: ".5rem" }}>
@@ -497,11 +484,9 @@ const LayerUI = ({
renderCustomFooter={renderCustomFooter} renderCustomFooter={renderCustomFooter}
onImageAction={onImageAction} onImageAction={onImageAction}
renderTopRightUI={renderTopRightUI} renderTopRightUI={renderTopRightUI}
renderMenuLinks={renderMenuLinks}
renderCustomStats={renderCustomStats} renderCustomStats={renderCustomStats}
renderSidebars={renderSidebars} renderSidebars={renderSidebars}
device={device} device={device}
UIOptions={UIOptions}
/> />
)} )}
+1 -1
View File
@@ -22,7 +22,7 @@ export const LibraryButton: React.FC<{
} }
return ( return (
<label title={`${capitalizeString(t("toolBar.library"))} — 0`}> <label title={`${capitalizeString(t("toolBar.library"))}`}>
<input <input
className="ToolIcon_type_checkbox" className="ToolIcon_type_checkbox"
type="checkbox" type="checkbox"
+10 -24
View File
@@ -1,5 +1,5 @@
import React from "react"; import React from "react";
import { AppProps, AppState, Device, ExcalidrawProps } from "../types"; import { AppState, Device, ExcalidrawProps } from "../types";
import { ActionManager } from "../actions/manager"; import { ActionManager } from "../actions/manager";
import { t } from "../i18n"; import { t } from "../i18n";
import Stack from "./Stack"; import Stack from "./Stack";
@@ -45,12 +45,10 @@ type MobileMenuProps = {
isMobile: boolean, isMobile: boolean,
appState: AppState, appState: AppState,
) => JSX.Element | null; ) => JSX.Element | null;
renderMenuLinks?: ExcalidrawProps["renderMenuLinks"];
renderCustomStats?: ExcalidrawProps["renderCustomStats"]; renderCustomStats?: ExcalidrawProps["renderCustomStats"];
renderSidebars: () => JSX.Element | null; renderSidebars: () => JSX.Element | null;
device: Device; device: Device;
renderWelcomeScreen?: boolean; renderWelcomeScreen?: boolean;
UIOptions: AppProps["UIOptions"];
}; };
export const MobileMenu = ({ export const MobileMenu = ({
@@ -68,12 +66,10 @@ export const MobileMenu = ({
renderCustomFooter, renderCustomFooter,
onImageAction, onImageAction,
renderTopRightUI, renderTopRightUI,
renderMenuLinks,
renderCustomStats, renderCustomStats,
renderSidebars, renderSidebars,
device, device,
renderWelcomeScreen, renderWelcomeScreen,
UIOptions,
}: MobileMenuProps) => { }: MobileMenuProps) => {
const renderToolbar = () => { const renderToolbar = () => {
return ( return (
@@ -115,8 +111,8 @@ export const MobileMenu = ({
/> />
</Stack.Row> </Stack.Row>
</Island> </Island>
{renderTopRightUI && renderTopRightUI(true, appState)}
<div className="mobile-misc-tools-container"> <div className="mobile-misc-tools-container">
{renderTopRightUI && renderTopRightUI(true, appState)}
<PenModeButton <PenModeButton
checked={appState.penMode} checked={appState.penMode}
onChange={onPenModeToggle} onChange={onPenModeToggle}
@@ -196,14 +192,12 @@ export const MobileMenu = ({
{!appState.viewModeEnabled && actionManager.renderAction("loadScene")} {!appState.viewModeEnabled && actionManager.renderAction("loadScene")}
{renderJSONExportDialog()} {renderJSONExportDialog()}
{renderImageExportDialog()} {renderImageExportDialog()}
{UIOptions.canvasActions.saveAsImage && ( <MenuItem
<MenuItem label={t("buttons.exportImage")}
label={t("buttons.exportImage")} icon={ExportImageIcon}
icon={ExportImageIcon} dataTestId="image-export-button"
dataTestId="image-export-button" onClick={() => setAppState({ openDialog: "imageExport" })}
onClick={() => setAppState({ openDialog: "imageExport" })} />
/>
)}
{onCollabButtonClick && ( {onCollabButtonClick && (
<CollabButton <CollabButton
isCollaborating={isCollaborating} isCollaborating={isCollaborating}
@@ -213,16 +207,8 @@ export const MobileMenu = ({
)} )}
{actionManager.renderAction("toggleShortcuts", undefined, true)} {actionManager.renderAction("toggleShortcuts", undefined, true)}
{!appState.viewModeEnabled && actionManager.renderAction("clearCanvas")} {!appState.viewModeEnabled && actionManager.renderAction("clearCanvas")}
{typeof renderMenuLinks === "undefined" ? ( //zsviczian <Separator />
<Separator /> <MenuLinks />
) : (
renderMenuLinks && <Separator />
)}
{typeof renderMenuLinks === "undefined" ? ( //zsviczian
<MenuLinks />
) : (
renderMenuLinks && renderMenuLinks(device.isMobile, appState)
)}
<Separator /> <Separator />
{!appState.viewModeEnabled && ( {!appState.viewModeEnabled && (
<div style={{ marginBottom: ".5rem" }}> <div style={{ marginBottom: ".5rem" }}>
+1 -3
View File
@@ -169,8 +169,7 @@ const getAdjustedDimensions = (
let maxWidth = null; let maxWidth = null;
const container = getContainerElement(element); const container = getContainerElement(element);
if (container) { if (container) {
const containerDims = getContainerDims(container); maxWidth = getMaxContainerWidth(container);
maxWidth = containerDims.width - BOUND_TEXT_PADDING * 2;
} }
const { const {
width: nextWidth, width: nextWidth,
@@ -258,7 +257,6 @@ export const refreshTextDimensions = (
) => { ) => {
const container = getContainerElement(textElement); const container = getContainerElement(textElement);
if (container) { if (container) {
// text = wrapText(text, getFontString(textElement), container.width);
text = wrapText( text = wrapText(
text, text,
getFontString(textElement), getFontString(textElement),
+2 -4
View File
@@ -19,13 +19,12 @@ export const redrawTextBoundingBox = (
) => { ) => {
let maxWidth = undefined; let maxWidth = undefined;
let text = textElement.text; let text = textElement.text;
if (container) { if (container) {
maxWidth = getMaxContainerWidth(container); maxWidth = getMaxContainerWidth(container);
text = wrapText( text = wrapText(
textElement.originalText, textElement.originalText,
getFontString(textElement), getFontString(textElement),
getMaxContainerWidth(container), maxWidth,
); );
} }
const metrics = measureText( const metrics = measureText(
@@ -230,10 +229,9 @@ export const measureText = (
const baseline = span.offsetTop + span.offsetHeight; const baseline = span.offsetTop + span.offsetHeight;
// Since span adds 1px extra width to the container // Since span adds 1px extra width to the container
const width = container.offsetWidth + 1; const width = container.offsetWidth + 1;
const height = container.offsetHeight; const height = container.offsetHeight;
document.body.removeChild(container);
document.body.removeChild(container);
return { width, height, baseline }; return { width, height, baseline };
}; };
+84
View File
@@ -10,11 +10,13 @@ import { BOUND_TEXT_PADDING, FONT_FAMILY } from "../constants";
import { import {
ExcalidrawTextElement, ExcalidrawTextElement,
ExcalidrawTextElementWithContainer, ExcalidrawTextElementWithContainer,
FontString,
} from "./types"; } from "./types";
import * as textElementUtils from "./textElement"; import * as textElementUtils from "./textElement";
import { API } from "../tests/helpers/api"; import { API } from "../tests/helpers/api";
import { mutateElement } from "./mutateElement"; import { mutateElement } from "./mutateElement";
import { resize } from "../tests/utils"; import { resize } from "../tests/utils";
import { getMaxContainerWidth } from "./newElement";
// Unmount ReactDOM from root // Unmount ReactDOM from root
ReactDOM.unmountComponentAtNode(document.getElementById("root")!); ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
@@ -433,6 +435,25 @@ describe("textWysiwyg", () => {
); );
expect(h.state.zoom.value).toBe(1); expect(h.state.zoom.value).toBe(1);
}); });
it("should paste text correctly", async () => {
Keyboard.keyPress(KEYS.ENTER);
await new Promise((r) => setTimeout(r, 0));
const 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);
});
}); });
describe("Test container-bound text", () => { describe("Test container-bound text", () => {
@@ -876,5 +897,68 @@ 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;
const wrappedText = textElementUtils.wrapText(
"Wikipedia is hosted by the Wikimedia Foundation, a non-profit organization that also hosts a range of other projects.",
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: () =>
"Wikipedia is hosted by the Wikimedia Foundation, a non-profit organization that also hosts a range of other projects.",
},
});
await new Promise((cb) => setTimeout(cb, 0));
editor.blur();
expect(rectangle.width).toBe(100);
expect(rectangle.height).toBe(210);
const textElement = h.elements[1] as ExcalidrawTextElement;
expect(textElement.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
."
`,
);
});
}); });
}); });
+31
View File
@@ -20,6 +20,7 @@ import {
getBoundTextElementId, getBoundTextElementId,
getContainerDims, getContainerDims,
getContainerElement, getContainerElement,
measureText,
wrapText, wrapText,
} from "./textElement"; } from "./textElement";
import { import {
@@ -29,6 +30,7 @@ import {
import { actionZoomIn, actionZoomOut } from "../actions/actionCanvas"; import { actionZoomIn, actionZoomOut } from "../actions/actionCanvas";
import App from "../components/App"; import App from "../components/App";
import { getMaxContainerWidth } from "./newElement"; import { getMaxContainerWidth } from "./newElement";
import { parseClipboard } from "../clipboard";
const normalizeText = (text: string) => { const normalizeText = (text: string) => {
return ( return (
@@ -275,6 +277,35 @@ export const textWysiwyg = ({
updateWysiwygStyle(); updateWysiwygStyle();
if (onChange) { if (onChange) {
editable.onpaste = async (event) => {
event.preventDefault();
const clipboardData = await parseClipboard(event);
if (!clipboardData.text) {
return;
}
const data = normalizeText(clipboardData.text);
const container = getContainerElement(element);
const font = getFontString({
fontSize: app.state.currentItemFontSize,
fontFamily: app.state.currentItemFontFamily,
});
if (data) {
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 wrappedText = container
? wrapText(newText, font, getMaxContainerWidth(container!))
: newText;
const dimensions = measureText(wrappedText, font);
editable.style.height = `${dimensions.height}px`;
onChange(newText);
}
};
editable.oninput = () => { editable.oninput = () => {
const updatedTextElement = Scene.getScene(element)?.getElement( const updatedTextElement = Scene.getScene(element)?.getElement(
id, id,
+14 -3
View File
@@ -310,16 +310,27 @@ class Collab extends PureComponent<Props, CollabState> {
} }
}; };
private fetchImageFilesFromFirebase = async (scene: { private fetchImageFilesFromFirebase = async (opts: {
elements: readonly ExcalidrawElement[]; 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) => { .filter((element) => {
return ( return (
isInitializedImageElement(element) && isInitializedImageElement(element) &&
!this.fileManager.isFileHandled(element.fileId) && !this.fileManager.isFileHandled(element.fileId) &&
!element.isDeleted && !element.isDeleted &&
element.status === "saved" (opts.forceFetchFiles
? element.status !== "pending" ||
Date.now() - element.updated > 10000
: element.status === "saved")
); );
}) })
.map((element) => (element as InitializedExcalidrawImageElement).fileId); .map((element) => (element as InitializedExcalidrawImageElement).fileId);
+1
View File
@@ -195,6 +195,7 @@ export const encodeFilesForUpload = async ({
id, id,
mimeType: fileData.mimeType, mimeType: fileData.mimeType,
created: Date.now(), created: Date.now(),
lastRetrieved: Date.now(),
}, },
}); });
+32 -8
View File
@@ -10,7 +10,7 @@
* (localStorage, indexedDB). * (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 { clearAppStateForLocalStorage } from "../../appState";
import { clearElementsForLocalStorage } from "../../element"; import { clearElementsForLocalStorage } from "../../element";
import { ExcalidrawElement, FileId } from "../../element/types"; import { ExcalidrawElement, FileId } from "../../element/types";
@@ -25,12 +25,21 @@ const filesStore = createStore("files-db", "files-store");
class LocalFileManager extends FileManager { class LocalFileManager extends FileManager {
clearObsoleteFiles = async (opts: { currentFileIds: FileId[] }) => { clearObsoleteFiles = async (opts: { currentFileIds: FileId[] }) => {
const allIds = await keys(filesStore); await entries(filesStore).then((entries) => {
for (const id of allIds) { for (const [id, imageData] of entries as [FileId, BinaryFileData][]) {
if (!opts.currentFileIds.includes(id as FileId)) { // if image is unused (not on canvas) & is older than 1 day, delete it
del(id, filesStore); // 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({ static fileStorage = new LocalFileManager({
getFiles(ids) { getFiles(ids) {
return getMany(ids, filesStore).then( return getMany(ids, filesStore).then(
(filesData: (BinaryFileData | undefined)[]) => { async (filesData: (BinaryFileData | undefined)[]) => {
const loadedFiles: BinaryFileData[] = []; const loadedFiles: BinaryFileData[] = [];
const erroredFiles = new Map<FileId, true>(); const erroredFiles = new Map<FileId, true>();
const filesToSave: [FileId, BinaryFileData][] = [];
filesData.forEach((data, index) => { filesData.forEach((data, index) => {
const id = ids[index]; const id = ids[index];
if (data) { if (data) {
loadedFiles.push(data); const _data: BinaryFileData = {
...data,
lastRetrieved: Date.now(),
};
filesToSave.push([id, _data]);
loadedFiles.push(_data);
} else { } else {
erroredFiles.set(id, true); 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 }; return { loadedFiles, erroredFiles };
}, },
); );
+1
View File
@@ -330,6 +330,7 @@ export const loadFilesFromFirebase = async (
id, id,
dataURL, dataURL,
created: metadata?.created || Date.now(), created: metadata?.created || Date.now(),
lastRetrieved: metadata?.created || Date.now(),
}); });
} else { } else {
erroredFiles.set(id, true); erroredFiles.set(id, true);
+1
View File
@@ -285,6 +285,7 @@ const ExcalidrawWrapper = () => {
collabAPI collabAPI
.fetchImageFilesFromFirebase({ .fetchImageFilesFromFirebase({
elements: data.scene.elements, elements: data.scene.elements,
forceFetchFiles: true,
}) })
.then(({ loadedFiles, erroredFiles }) => { .then(({ loadedFiles, erroredFiles }) => {
excalidrawAPI.addFiles(loadedFiles); excalidrawAPI.addFiles(loadedFiles);
+11
View File
@@ -63,6 +63,17 @@ export const KEYS = {
Y: "y", Y: "y",
Z: "z", Z: "z",
K: "k", K: "k",
0: "0",
1: "1",
2: "2",
3: "3",
4: "4",
5: "5",
6: "6",
7: "7",
8: "8",
9: "9",
} as const; } as const;
export type Key = keyof typeof KEYS; export type Key = keyof typeof KEYS;
+1 -23
View File
@@ -392,8 +392,6 @@ No, Excalidraw package doesn't come with collaboration built in, since the imple
| [`onPointerUpdate`](#onPointerUpdate) | Function | | Callback triggered when mouse pointer is updated. | | [`onPointerUpdate`](#onPointerUpdate) | Function | | Callback triggered when mouse pointer is updated. |
| [`langCode`](#langCode) | string | `en` | Language code string | | [`langCode`](#langCode) | string | `en` | Language code string |
| [`renderTopRightUI`](#renderTopRightUI) | Function | | Function that renders custom UI in top right corner | | [`renderTopRightUI`](#renderTopRightUI) | Function | | Function that renders custom UI in top right corner |
| [`hideWelcomeScreen`](#hideWelcomeScreen) | boolean | | This implies if the app should always hide the welcome sreen |
| [`renderMenuLinks`](#renderMenuLinks) | Function | | Function that renders custom list of links (or other custom UI) in the app menu |
| [`renderFooter `](#renderFooter) | Function | | Function that renders custom UI footer | | [`renderFooter `](#renderFooter) | Function | | Function that renders custom UI footer |
| [`renderCustomStats`](#renderCustomStats) | Function | | Function that can be used to render custom stats on the stats dialog. | | [`renderCustomStats`](#renderCustomStats) | Function | | Function that can be used to render custom stats on the stats dialog. |
| [`renderSIdebar`](#renderSIdebar) | Function | | Render function that renders custom sidebar. | | [`renderSIdebar`](#renderSIdebar) | Function | | Render function that renders custom sidebar. |
@@ -615,22 +613,6 @@ import { defaultLang, languages } from "@excalidraw/excalidraw";
A function returning JSX to render custom UI in the top right corner of the app. A function returning JSX to render custom UI in the top right corner of the app.
#### `hideWelcomeScreen`
<pre>
boolean
</pre>
Boolean value to override the displaying of the welcome screen elements. If set to true, the welcome screen will never be shown.
#### `renderMenuLinks`
<pre>
((isMobile: boolean, appState: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L79">AppState</a>) => JSX | null)|null
</pre>
A function returning JSX to render custom UI (intended to be a list of custom links) replacing the default list of links in the app menu. If set to null, the list of links will not be displayed. If unset, the default list of links will be displayed.
#### `renderFooter` #### `renderFooter`
<pre> <pre>
@@ -703,7 +685,7 @@ This prop sets the name of the drawing which will be used when exporting the dra
#### `UIOptions` #### `UIOptions`
This prop can be used to customise UI of Excalidraw. Currently we support customising [`canvasActions`](#canvasActions), [`dockedSidebarBreakpoint`](dockedSidebarBreakpoint), and ['showLanguageList`](showLanguageList). It accepts the below parameters This prop can be used to customise UI of Excalidraw. Currently we support customising [`canvasActions`](#canvasActions) and [`dockedSidebarBreakpoint`](dockedSidebarBreakpoint). It accepts the below parameters
<pre> <pre>
{ canvasActions: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L208"> CanvasActions<a/> } { canvasActions: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L208"> CanvasActions<a/> }
@@ -727,10 +709,6 @@ This prop indicates at what point should we break to a docked, permanent sidebar
![image](https://user-images.githubusercontent.com/11256141/174664866-c698c3fa-197b-43ff-956c-d79852c7b326.png) ![image](https://user-images.githubusercontent.com/11256141/174664866-c698c3fa-197b-43ff-956c-d79852c7b326.png)
### `showLanguageList`
Boolean prop. If set to `true` the `language selector dropdown list` will be hidden in the app menu. If `false` or `undefined` the language dropdown will be rendered.
#### `exportOpts` #### `exportOpts`
The below attributes can be set in `UIOptions.canvasActions.export` to customize the export dialog. If `UIOptions.canvasActions.export` is `false` the export button will not be rendered. The below attributes can be set in `UIOptions.canvasActions.export` to customize the export dialog. If `UIOptions.canvasActions.export` is `false` the export button will not be rendered.
+1
View File
@@ -148,6 +148,7 @@ export default function App() {
dataURL: reader.result as BinaryFileData["dataURL"], dataURL: reader.result as BinaryFileData["dataURL"],
mimeType: MIME_TYPES.jpg, mimeType: MIME_TYPES.jpg,
created: 1644915140367, created: 1644915140367,
lastRetrieved: 1644915140367,
}, },
]; ];
-4
View File
@@ -20,8 +20,6 @@ const ExcalidrawBase = (props: ExcalidrawProps) => {
isCollaborating = false, isCollaborating = false,
onPointerUpdate, onPointerUpdate,
renderTopRightUI, renderTopRightUI,
hideWelcomeScreen,
renderMenuLinks,
renderFooter, renderFooter,
renderSidebar, renderSidebar,
langCode = defaultLang.code, langCode = defaultLang.code,
@@ -95,8 +93,6 @@ const ExcalidrawBase = (props: ExcalidrawProps) => {
isCollaborating={isCollaborating} isCollaborating={isCollaborating}
onPointerUpdate={onPointerUpdate} onPointerUpdate={onPointerUpdate}
renderTopRightUI={renderTopRightUI} renderTopRightUI={renderTopRightUI}
hideWelcomeScreen={hideWelcomeScreen}
renderMenuLinks={renderMenuLinks}
renderFooter={renderFooter} renderFooter={renderFooter}
langCode={langCode} langCode={langCode}
viewModeEnabled={viewModeEnabled} viewModeEnabled={viewModeEnabled}
+13 -7
View File
@@ -406,6 +406,8 @@ export const _renderScene = ({
}), }),
); );
let editingLinearElement: NonDeleted<ExcalidrawLinearElement> | undefined =
undefined;
visibleElements.forEach((element) => { visibleElements.forEach((element) => {
try { try {
renderElement(element, rc, context, renderConfig); renderElement(element, rc, context, renderConfig);
@@ -414,15 +416,10 @@ export const _renderScene = ({
// correct element from visible elements // correct element from visible elements
if (appState.editingLinearElement?.elementId === element.id) { if (appState.editingLinearElement?.elementId === element.id) {
if (element) { if (element) {
renderLinearPointHandles( editingLinearElement =
context, element as NonDeleted<ExcalidrawLinearElement>;
appState,
renderConfig,
element as NonDeleted<ExcalidrawLinearElement>,
);
} }
} }
if (!isExporting) { if (!isExporting) {
renderLinkIcon(element, context, appState); renderLinkIcon(element, context, appState);
} }
@@ -431,6 +428,15 @@ export const _renderScene = ({
} }
}); });
if (editingLinearElement) {
renderLinearPointHandles(
context,
appState,
renderConfig,
editingLinearElement,
);
}
// Paint selection element // Paint selection element
if (appState.selectionElement) { if (appState.selectionElement) {
try { try {
+13 -3
View File
@@ -17,60 +17,70 @@ export const SHAPES = [
icon: SelectionIcon, icon: SelectionIcon,
value: "selection", value: "selection",
key: KEYS.V, key: KEYS.V,
numericKey: KEYS["1"],
fillable: true, fillable: true,
}, },
{ {
icon: RectangleIcon, icon: RectangleIcon,
value: "rectangle", value: "rectangle",
key: KEYS.R, key: KEYS.R,
numericKey: KEYS["2"],
fillable: true, fillable: true,
}, },
{ {
icon: DiamondIcon, icon: DiamondIcon,
value: "diamond", value: "diamond",
key: KEYS.D, key: KEYS.D,
numericKey: KEYS["3"],
fillable: true, fillable: true,
}, },
{ {
icon: EllipseIcon, icon: EllipseIcon,
value: "ellipse", value: "ellipse",
key: KEYS.O, key: KEYS.O,
numericKey: KEYS["4"],
fillable: true, fillable: true,
}, },
{ {
icon: ArrowIcon, icon: ArrowIcon,
value: "arrow", value: "arrow",
key: KEYS.A, key: KEYS.A,
numericKey: KEYS["5"],
fillable: true, fillable: true,
}, },
{ {
icon: LineIcon, icon: LineIcon,
value: "line", value: "line",
key: [KEYS.P, KEYS.L], key: KEYS.L,
numericKey: KEYS["6"],
fillable: true, fillable: true,
}, },
{ {
icon: FreedrawIcon, icon: FreedrawIcon,
value: "freedraw", value: "freedraw",
key: [KEYS.X, KEYS.P.toUpperCase()], key: [KEYS.P, KEYS.X],
numericKey: KEYS["7"],
fillable: false, fillable: false,
}, },
{ {
icon: TextIcon, icon: TextIcon,
value: "text", value: "text",
key: KEYS.T, key: KEYS.T,
numericKey: KEYS["8"],
fillable: false, fillable: false,
}, },
{ {
icon: ImageIcon, icon: ImageIcon,
value: "image", value: "image",
key: null, key: null,
numericKey: KEYS["9"],
fillable: false, fillable: false,
}, },
{ {
icon: EraserIcon, icon: EraserIcon,
value: "eraser", value: "eraser",
key: KEYS.E, key: KEYS.E,
numericKey: KEYS["0"],
fillable: false, fillable: false,
}, },
] as const; ] as const;
@@ -78,7 +88,7 @@ export const SHAPES = [
export const findShapeByKey = (key: string) => { export const findShapeByKey = (key: string) => {
const shape = SHAPES.find((shape, index) => { const shape = SHAPES.find((shape, index) => {
return ( return (
key === (shape.value === "eraser" ? 0 : index + 1).toString() || (shape.numericKey != null && key === shape.numericKey.toString()) ||
(shape.key && (shape.key &&
(typeof shape.key === "string" (typeof shape.key === "string"
? shape.key === key ? 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 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`] = ` exports[`regression tests key r selects rectangle tool: [end of test] appState 1`] = `
Object { Object {
"activeTool": 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 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`] = ` exports[`regression tests make a group and duplicate it: [end of test] appState 1`] = `
Object { Object {
"activeTool": Object { "activeTool": Object {
+1
View File
@@ -158,6 +158,7 @@ describe("export", () => {
dataURL: await getDataURL(await API.loadFile("./fixtures/deer.png")), dataURL: await getDataURL(await API.loadFile("./fixtures/deer.png")),
mimeType: "image/png", mimeType: "image/png",
created: Date.now(), created: Date.now(),
lastRetrieved: Date.now(),
}, },
} as const; } as const;
+48 -3
View File
@@ -16,7 +16,7 @@ const renderScene = jest.spyOn(Renderer, "renderScene");
const { h } = window; const { h } = window;
describe(" Test Linear Elements", () => { describe("Test Linear Elements", () => {
let container: HTMLElement; let container: HTMLElement;
let canvas: HTMLCanvasElement; let canvas: HTMLCanvasElement;
@@ -89,8 +89,15 @@ describe(" Test Linear Elements", () => {
mouse.clickAt(p1[0], p1[1]); mouse.clickAt(p1[0], p1[1]);
}; };
const enterLineEditingMode = (line: ExcalidrawLinearElement) => { const enterLineEditingMode = (
mouse.clickAt(p1[0], p1[1]); line: ExcalidrawLinearElement,
selectProgrammatically = false,
) => {
if (selectProgrammatically) {
API.setSelectedElements([line]);
} else {
mouse.clickAt(p1[0], p1[1]);
}
Keyboard.keyPress(KEYS.ENTER); Keyboard.keyPress(KEYS.ENTER);
expect(h.state.editingLinearElement?.elementId).toEqual(line.id); 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]},
],
]
`);
});
}); });
}); });
+1 -1
View File
@@ -138,7 +138,7 @@ describe("regression tests", () => {
[`4${KEYS.O}`, "ellipse", true], [`4${KEYS.O}`, "ellipse", true],
[`5${KEYS.A}`, "arrow", true], [`5${KEYS.A}`, "arrow", true],
[`6${KEYS.L}`, "line", true], [`6${KEYS.L}`, "line", true],
[`7${KEYS.X}`, "freedraw", false], [`7${KEYS.P}`, "freedraw", false],
] as [string, ExcalidrawElement["type"], boolean][]) { ] as [string, ExcalidrawElement["type"], boolean][]) {
for (const key of keys) { for (const key of keys) {
it(`key ${key} selects ${shape} tool`, () => { it(`key ${key} selects ${shape} tool`, () => {
+11 -6
View File
@@ -61,7 +61,18 @@ export type BinaryFileData = {
| typeof MIME_TYPES.binary; | typeof MIME_TYPES.binary;
id: FileId; id: FileId;
dataURL: DataURL; dataURL: DataURL;
/**
* Epoch timestamp in milliseconds
*/
created: number; 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">; export type BinaryFileMetadata = Omit<BinaryFileData, "dataURL">;
@@ -284,10 +295,6 @@ export interface ExcalidrawProps {
isMobile: boolean, isMobile: boolean,
appState: AppState, appState: AppState,
) => JSX.Element | null; ) => JSX.Element | null;
renderMenuLinks?:
| ((isMobile: boolean, appState: AppState) => JSX.Element | null)
| null;
hideWelcomeScreen?: boolean;
renderFooter?: (isMobile: boolean, appState: AppState) => JSX.Element | null; renderFooter?: (isMobile: boolean, appState: AppState) => JSX.Element | null;
langCode?: Language["code"]; langCode?: Language["code"];
viewModeEnabled?: boolean; viewModeEnabled?: boolean;
@@ -303,7 +310,6 @@ export interface ExcalidrawProps {
UIOptions?: { UIOptions?: {
dockedSidebarBreakpoint?: number; dockedSidebarBreakpoint?: number;
canvasActions?: CanvasActions; canvasActions?: CanvasActions;
showLanguageList?: boolean;
}; };
detectScroll?: boolean; detectScroll?: boolean;
handleKeyboardGlobally?: boolean; handleKeyboardGlobally?: boolean;
@@ -376,7 +382,6 @@ export type AppProps = Merge<
UIOptions: { UIOptions: {
canvasActions: Required<CanvasActions> & { export: ExportOpts }; canvasActions: Required<CanvasActions> & { export: ExportOpts };
dockedSidebarBreakpoint?: number; dockedSidebarBreakpoint?: number;
showLanguageList?: boolean;
}; };
detectScroll: boolean; detectScroll: boolean;
handleKeyboardGlobally: boolean; handleKeyboardGlobally: boolean;