Merge branch 'master' into kb/auto-save-support

This commit is contained in:
dwelle 2021-04-04 14:47:38 +02:00
commit 6442a45bd4
117 changed files with 4189 additions and 1026 deletions

1
.gitignore vendored
View File

@ -20,3 +20,4 @@ package-lock.json
static static
yarn-debug.log* yarn-debug.log*
yarn-error.log* yarn-error.log*
src/packages/excalidraw/types

View File

@ -23,16 +23,16 @@
"@sentry/integrations": "6.2.1", "@sentry/integrations": "6.2.1",
"@testing-library/jest-dom": "5.11.9", "@testing-library/jest-dom": "5.11.9",
"@testing-library/react": "11.2.5", "@testing-library/react": "11.2.5",
"@types/jest": "26.0.20", "@types/jest": "26.0.22",
"@types/react": "17.0.3", "@types/react": "17.0.3",
"@types/react-dom": "17.0.2", "@types/react-dom": "17.0.2",
"@types/socket.io-client": "1.4.36", "@types/socket.io-client": "1.4.36",
"browser-fs-access": "0.15.3", "browser-fs-access": "0.16.2",
"clsx": "1.1.1", "clsx": "1.1.1",
"firebase": "8.2.10", "firebase": "8.2.10",
"i18next-browser-languagedetector": "6.0.1", "i18next-browser-languagedetector": "6.0.1",
"lodash.throttle": "4.1.1", "lodash.throttle": "4.1.1",
"nanoid": "3.1.21", "nanoid": "3.1.22",
"open-color": "1.8.0", "open-color": "1.8.0",
"pako": "1.0.11", "pako": "1.0.11",
"png-chunk-text": "1.0.0", "png-chunk-text": "1.0.0",
@ -40,8 +40,8 @@
"png-chunks-extract": "1.0.0", "png-chunks-extract": "1.0.0",
"points-on-curve": "0.2.0", "points-on-curve": "0.2.0",
"pwacompat": "2.0.17", "pwacompat": "2.0.17",
"react": "17.0.1", "react": "17.0.2",
"react-dom": "17.0.1", "react-dom": "17.0.2",
"react-scripts": "4.0.3", "react-scripts": "4.0.3",
"roughjs": "4.3.1", "roughjs": "4.3.1",
"sass": "1.32.8", "sass": "1.32.8",

View File

@ -51,8 +51,7 @@
name="twitter:description" name="twitter:description"
content="Excalidraw is a whiteboard tool that lets you easily sketch diagrams that have a hand-drawn feel to them." content="Excalidraw is a whiteboard tool that lets you easily sketch diagrams that have a hand-drawn feel to them."
/> />
<!-- OG tags require absolute url for images -->
<meta name="twitter:image" content="https://excalidraw.com/og-image.png" />
<link rel="shortcut icon" href="favicon.ico" type="image/x-icon" /> <link rel="shortcut icon" href="favicon.ico" type="image/x-icon" />
<!-- Excalidraw version --> <!-- Excalidraw version -->
@ -148,6 +147,9 @@
color: var(--popup-text-color); color: var(--popup-text-color);
font-size: 1.3em; font-size: 1.3em;
} }
#root {
height: 100%;
}
</style> </style>
</head> </head>

View File

@ -8,7 +8,7 @@ import { getCommonBounds, getNonDeletedElements } from "../element";
import { newElementWith } from "../element/mutateElement"; import { newElementWith } from "../element/mutateElement";
import { ExcalidrawElement } from "../element/types"; import { ExcalidrawElement } from "../element/types";
import { t } from "../i18n"; import { t } from "../i18n";
import useIsMobile from "../is-mobile"; import { useIsMobile } from "../is-mobile";
import { CODES, KEYS } from "../keys"; import { CODES, KEYS } from "../keys";
import { getNormalizedZoom, getSelectedElements } from "../scene"; import { getNormalizedZoom, getSelectedElements } from "../scene";
import { centerScrollOn } from "../scene/scroll"; import { centerScrollOn } from "../scene/scroll";
@ -33,6 +33,7 @@ export const actionChangeViewBackgroundColor = register({
type="canvasBackground" type="canvasBackground"
color={appState.viewBackgroundColor} color={appState.viewBackgroundColor}
onChange={(color) => updateData(color)} onChange={(color) => updateData(color)}
data-testid="canvas-background-picker"
/> />
</div> </div>
); );
@ -72,6 +73,7 @@ export const actionClearCanvas = register({
updateData(null); updateData(null);
} }
}} }}
data-testid="clear-canvas-button"
/> />
), ),
}); });

View File

@ -8,7 +8,7 @@ import { Tooltip } from "../components/Tooltip";
import { DarkModeToggle, Appearence } from "../components/DarkModeToggle"; import { DarkModeToggle, Appearence } from "../components/DarkModeToggle";
import { loadFromJSON, saveAsJSON } from "../data"; import { loadFromJSON, saveAsJSON } from "../data";
import { t } from "../i18n"; import { t } from "../i18n";
import useIsMobile from "../is-mobile"; import { useIsMobile } from "../is-mobile";
import { KEYS } from "../keys"; import { KEYS } from "../keys";
import { register } from "./register"; import { register } from "./register";
import { supported } from "browser-fs-access"; import { supported } from "browser-fs-access";
@ -136,6 +136,7 @@ export const actionSaveScene = register({
aria-label={t("buttons.save")} aria-label={t("buttons.save")}
showAriaLabel={useIsMobile()} showAriaLabel={useIsMobile()}
onClick={() => updateData(null)} onClick={() => updateData(null)}
data-testid="save-button"
/> />
), ),
}); });
@ -167,6 +168,7 @@ export const actionSaveAsScene = register({
showAriaLabel={useIsMobile()} showAriaLabel={useIsMobile()}
hidden={!supported} hidden={!supported}
onClick={() => updateData(null)} onClick={() => updateData(null)}
data-testid="save-as-button"
/> />
), ),
}); });
@ -204,6 +206,7 @@ export const actionLoadScene = register({
aria-label={t("buttons.load")} aria-label={t("buttons.load")}
showAriaLabel={useIsMobile()} showAriaLabel={useIsMobile()}
onClick={updateData} onClick={updateData}
data-testid="load-button"
/> />
), ),
}); });

207
src/actions/actionFlip.ts Normal file
View File

@ -0,0 +1,207 @@
import { register } from "./register";
import { getSelectedElements } from "../scene";
import { getElementMap, getNonDeletedElements } from "../element";
import { mutateElement } from "../element/mutateElement";
import { ExcalidrawElement, NonDeleted } from "../element/types";
import { normalizeAngle, resizeSingleElement } from "../element/resizeElements";
import { AppState } from "../types";
import { getTransformHandles } from "../element/transformHandles";
import { isLinearElement } from "../element/typeChecks";
import { updateBoundElements } from "../element/binding";
import { LinearElementEditor } from "../element/linearElementEditor";
const enableActionFlipHorizontal = (
elements: readonly ExcalidrawElement[],
appState: AppState,
) => {
const eligibleElements = getSelectedElements(
getNonDeletedElements(elements),
appState,
);
return eligibleElements.length === 1 && eligibleElements[0].type !== "text";
};
const enableActionFlipVertical = (
elements: readonly ExcalidrawElement[],
appState: AppState,
) => {
const eligibleElements = getSelectedElements(
getNonDeletedElements(elements),
appState,
);
return eligibleElements.length === 1;
};
export const actionFlipHorizontal = register({
name: "flipHorizontal",
perform: (elements, appState) => {
return {
elements: flipSelectedElements(elements, appState, "horizontal"),
appState,
commitToHistory: true,
};
},
keyTest: (event) => event.shiftKey && event.code === "KeyH",
contextItemLabel: "labels.flipHorizontal",
contextItemPredicate: (elements, appState) =>
enableActionFlipHorizontal(elements, appState),
});
export const actionFlipVertical = register({
name: "flipVertical",
perform: (elements, appState) => {
return {
elements: flipSelectedElements(elements, appState, "vertical"),
appState,
commitToHistory: true,
};
},
keyTest: (event) => event.shiftKey && event.code === "KeyV",
contextItemLabel: "labels.flipVertical",
contextItemPredicate: (elements, appState) =>
enableActionFlipVertical(elements, appState),
});
const flipSelectedElements = (
elements: readonly ExcalidrawElement[],
appState: Readonly<AppState>,
flipDirection: "horizontal" | "vertical",
) => {
const selectedElements = getSelectedElements(
getNonDeletedElements(elements),
appState,
);
// remove once we allow for groups of elements to be flipped
if (selectedElements.length > 1) {
return elements;
}
const updatedElements = flipElements(
selectedElements,
appState,
flipDirection,
);
const updatedElementsMap = getElementMap(updatedElements);
return elements.map((element) => updatedElementsMap[element.id] || element);
};
const flipElements = (
elements: NonDeleted<ExcalidrawElement>[],
appState: AppState,
flipDirection: "horizontal" | "vertical",
): ExcalidrawElement[] => {
for (let i = 0; i < elements.length; i++) {
flipElement(elements[i], appState);
// If vertical flip, rotate an extra 180
if (flipDirection === "vertical") {
rotateElement(elements[i], Math.PI);
}
}
return elements;
};
const flipElement = (
element: NonDeleted<ExcalidrawElement>,
appState: AppState,
) => {
const originalX = element.x;
const originalY = element.y;
const width = element.width;
const height = element.height;
const originalAngle = normalizeAngle(element.angle);
let finalOffsetX = 0;
if (isLinearElement(element)) {
finalOffsetX =
element.points.reduce((max, point) => Math.max(max, point[0]), 0) * 2 -
element.width;
}
// Rotate back to zero, if necessary
mutateElement(element, {
angle: normalizeAngle(0),
});
// Flip unrotated by pulling TransformHandle to opposite side
const transformHandles = getTransformHandles(element, appState.zoom);
let usingNWHandle = true;
let newNCoordsX = 0;
let nHandle = transformHandles.nw;
if (!nHandle) {
// Use ne handle instead
usingNWHandle = false;
nHandle = transformHandles.ne;
if (!nHandle) {
mutateElement(element, {
angle: originalAngle,
});
return;
}
}
if (isLinearElement(element)) {
for (let i = 1; i < element.points.length; i++) {
LinearElementEditor.movePoint(element, i, [
-element.points[i][0],
element.points[i][1],
]);
}
LinearElementEditor.normalizePoints(element);
} else {
// calculate new x-coord for transformation
newNCoordsX = usingNWHandle ? element.x + 2 * width : element.x - 2 * width;
resizeSingleElement(
element,
true,
element,
usingNWHandle ? "nw" : "ne",
false,
newNCoordsX,
nHandle[1],
);
// fix the size to account for handle sizes
mutateElement(element, {
width,
height,
});
}
// Rotate by (360 degrees - original angle)
let angle = normalizeAngle(2 * Math.PI - originalAngle);
if (angle < 0) {
// check, probably unnecessary
angle = normalizeAngle(angle + 2 * Math.PI);
}
mutateElement(element, {
angle,
});
// Move back to original spot to appear "flipped in place"
mutateElement(element, {
x: originalX + finalOffsetX,
y: originalY,
});
updateBoundElements(element);
};
const rotateElement = (element: ExcalidrawElement, rotationAngle: number) => {
const originalX = element.x;
const originalY = element.y;
let angle = normalizeAngle(element.angle + rotationAngle);
if (angle < 0) {
// check, probably unnecessary
angle = normalizeAngle(2 * Math.PI + angle);
}
mutateElement(element, {
angle,
});
// Move back to original spot
mutateElement(element, {
x: originalX,
y: originalY,
});
};

View File

@ -67,6 +67,8 @@ export {
distributeVertically, distributeVertically,
} from "./actionDistribute"; } from "./actionDistribute";
export { actionFlipHorizontal, actionFlipVertical } from "./actionFlip";
export { export {
actionCopy, actionCopy,
actionCut, actionCut,

View File

@ -7,12 +7,12 @@ import {
ActionResult, ActionResult,
} from "./types"; } from "./types";
import { ExcalidrawElement } from "../element/types"; import { ExcalidrawElement } from "../element/types";
import { AppState, ExcalidrawProps } from "../types"; import { AppProps, AppState } from "../types";
import { MODES } from "../constants"; import { MODES } from "../constants";
// This is the <App> component, but for now we don't care about anything but its // This is the <App> component, but for now we don't care about anything but its
// `canvas` state. // `canvas` state.
type App = { canvas: HTMLCanvasElement | null; props: ExcalidrawProps }; type App = { canvas: HTMLCanvasElement | null; props: AppProps };
export class ActionManager implements ActionsManagerInterface { export class ActionManager implements ActionsManagerInterface {
actions = {} as ActionsManagerInterface["actions"]; actions = {} as ActionsManagerInterface["actions"];
@ -52,10 +52,14 @@ export class ActionManager implements ActionsManagerInterface {
} }
handleKeyDown(event: KeyboardEvent) { handleKeyDown(event: KeyboardEvent) {
const canvasActions = this.app.props.UIOptions.canvasActions;
const data = Object.values(this.actions) const data = Object.values(this.actions)
.sort((a, b) => (b.keyPriority || 0) - (a.keyPriority || 0)) .sort((a, b) => (b.keyPriority || 0) - (a.keyPriority || 0))
.filter( .filter(
(action) => (action) =>
(action.name in canvasActions
? canvasActions[action.name as keyof typeof canvasActions]
: true) &&
action.keyTest && action.keyTest &&
action.keyTest( action.keyTest(
event, event,
@ -102,7 +106,15 @@ export class ActionManager implements ActionsManagerInterface {
// like the user list. We can use this key to extract more // like the user list. We can use this key to extract more
// data from app state. This is an alternative to generic prop hell! // data from app state. This is an alternative to generic prop hell!
renderAction = (name: ActionName, id?: string) => { renderAction = (name: ActionName, id?: string) => {
if (this.actions[name] && "PanelComponent" in this.actions[name]) { const canvasActions = this.app.props.UIOptions.canvasActions;
if (
this.actions[name] &&
"PanelComponent" in this.actions[name] &&
(name in canvasActions
? canvasActions[name as keyof typeof canvasActions]
: true)
) {
const action = this.actions[name]; const action = this.actions[name];
const PanelComponent = action.PanelComponent!; const PanelComponent = action.PanelComponent!;
const updateData = (formState?: any) => { const updateData = (formState?: any) => {

View File

@ -23,7 +23,9 @@ export type ShortcutName =
| "zenMode" | "zenMode"
| "stats" | "stats"
| "addToLibrary" | "addToLibrary"
| "viewMode"; | "viewMode"
| "flipHorizontal"
| "flipVertical";
const shortcutMap: Record<ShortcutName, string[]> = { const shortcutMap: Record<ShortcutName, string[]> = {
cut: [getShortcutKey("CtrlOrCmd+X")], cut: [getShortcutKey("CtrlOrCmd+X")],
@ -57,6 +59,8 @@ const shortcutMap: Record<ShortcutName, string[]> = {
zenMode: [getShortcutKey("Alt+Z")], zenMode: [getShortcutKey("Alt+Z")],
stats: [], stats: [],
addToLibrary: [], addToLibrary: [],
flipHorizontal: [getShortcutKey("Shift+H")],
flipVertical: [getShortcutKey("Shift+V")],
viewMode: [getShortcutKey("Alt+R")], viewMode: [getShortcutKey("Alt+R")],
}; };

View File

@ -6,7 +6,10 @@ import { AppState, ExcalidrawProps } from "../types";
export type ActionResult = export type ActionResult =
| { | {
elements?: readonly ExcalidrawElement[] | null; elements?: readonly ExcalidrawElement[] | null;
appState?: MarkOptional<AppState, "offsetTop" | "offsetLeft"> | null; appState?: MarkOptional<
AppState,
"offsetTop" | "offsetLeft" | "width" | "height"
> | null;
commitToHistory: boolean; commitToHistory: boolean;
syncHistory?: boolean; syncHistory?: boolean;
} }
@ -86,6 +89,8 @@ export type ActionName =
| "alignHorizontallyCentered" | "alignHorizontallyCentered"
| "distributeHorizontally" | "distributeHorizontally"
| "distributeVertically" | "distributeVertically"
| "flipHorizontal"
| "flipVertical"
| "viewMode" | "viewMode"
| "exportWithDarkMode"; | "exportWithDarkMode";

View File

@ -10,7 +10,7 @@ import { getDateTime } from "./utils";
export const getDefaultAppState = (): Omit< export const getDefaultAppState = (): Omit<
AppState, AppState,
"offsetTop" | "offsetLeft" "offsetTop" | "offsetLeft" | "width" | "height"
> => { > => {
return { return {
autosave: false, autosave: false,
@ -44,7 +44,6 @@ export const getDefaultAppState = (): Omit<
exportWithDarkMode: false, exportWithDarkMode: false,
fileHandle: null, fileHandle: null,
gridSize: null, gridSize: null,
height: window.innerHeight,
isBindingEnabled: true, isBindingEnabled: true,
isLibraryOpen: false, isLibraryOpen: false,
isLoading: false, isLoading: false,
@ -71,7 +70,6 @@ export const getDefaultAppState = (): Omit<
suggestedBindings: [], suggestedBindings: [],
toastMessage: null, toastMessage: null,
viewBackgroundColor: oc.white, viewBackgroundColor: oc.white,
width: window.innerWidth,
zenModeEnabled: false, zenModeEnabled: false,
zoom: { value: 1 as NormalizedZoomValue, translation: { x: 0, y: 0 } }, zoom: { value: 1 as NormalizedZoomValue, translation: { x: 0, y: 0 } },
viewModeEnabled: false, viewModeEnabled: false,

View File

@ -3,7 +3,7 @@ import { ActionManager } from "../actions/manager";
import { getNonDeletedElements } from "../element"; import { getNonDeletedElements } from "../element";
import { ExcalidrawElement } from "../element/types"; import { ExcalidrawElement } from "../element/types";
import { t } from "../i18n"; import { t } from "../i18n";
import useIsMobile from "../is-mobile"; import { useIsMobile } from "../is-mobile";
import { import {
canChangeSharpness, canChangeSharpness,
canHaveArrowheads, canHaveArrowheads,

View File

@ -17,6 +17,8 @@ import {
actionDeleteSelected, actionDeleteSelected,
actionDuplicateSelection, actionDuplicateSelection,
actionFinalize, actionFinalize,
actionFlipHorizontal,
actionFlipVertical,
actionGroup, actionGroup,
actionPasteStyles, actionPasteStyles,
actionSelectAll, actionSelectAll,
@ -42,6 +44,7 @@ import {
import { import {
APP_NAME, APP_NAME,
CURSOR_TYPE, CURSOR_TYPE,
DEFAULT_UI_OPTIONS,
DEFAULT_VERTICAL_ALIGN, DEFAULT_VERTICAL_ALIGN,
DRAGGING_THRESHOLD, DRAGGING_THRESHOLD,
ELEMENT_SHIFT_TRANSLATE_AMOUNT, ELEMENT_SHIFT_TRANSLATE_AMOUNT,
@ -57,6 +60,8 @@ import {
TAP_TWICE_TIMEOUT, TAP_TWICE_TIMEOUT,
TEXT_TO_CENTER_SNAP_THRESHOLD, TEXT_TO_CENTER_SNAP_THRESHOLD,
TOUCH_CTX_MENU_TIMEOUT, TOUCH_CTX_MENU_TIMEOUT,
URL_HASH_KEYS,
URL_QUERY_KEYS,
ZOOM_STEP, ZOOM_STEP,
} from "../constants"; } from "../constants";
import { loadFromBlob } from "../data"; import { loadFromBlob } from "../data";
@ -157,13 +162,7 @@ import Scene from "../scene/Scene";
import { SceneState, ScrollBars } from "../scene/types"; import { SceneState, ScrollBars } from "../scene/types";
import { getNewZoom } from "../scene/zoom"; import { getNewZoom } from "../scene/zoom";
import { findShapeByKey } from "../shapes"; import { findShapeByKey } from "../shapes";
import { import { AppProps, AppState, Gesture, GestureEvent, SceneData } from "../types";
AppState,
ExcalidrawProps,
Gesture,
GestureEvent,
SceneData,
} from "../types";
import { import {
debounce, debounce,
distance, distance,
@ -277,29 +276,30 @@ export type ExcalidrawImperativeAPI = {
getSceneElements: InstanceType<typeof App>["getSceneElements"]; getSceneElements: InstanceType<typeof App>["getSceneElements"];
getAppState: () => InstanceType<typeof App>["state"]; getAppState: () => InstanceType<typeof App>["state"];
setCanvasOffsets: InstanceType<typeof App>["setCanvasOffsets"]; setCanvasOffsets: InstanceType<typeof App>["setCanvasOffsets"];
importLibrary: InstanceType<typeof App>["importLibraryFromUrl"];
setToastMessage: InstanceType<typeof App>["setToastMessage"];
readyPromise: ResolvablePromise<ExcalidrawImperativeAPI>; readyPromise: ResolvablePromise<ExcalidrawImperativeAPI>;
ready: true; ready: true;
}; };
class App extends React.Component<ExcalidrawProps, AppState> { class App extends React.Component<AppProps, AppState> {
canvas: HTMLCanvasElement | null = null; canvas: HTMLCanvasElement | null = null;
rc: RoughCanvas | null = null; rc: RoughCanvas | null = null;
unmounted: boolean = false; unmounted: boolean = false;
actionManager: ActionManager; actionManager: ActionManager;
private excalidrawContainerRef = React.createRef<HTMLDivElement>(); private excalidrawContainerRef = React.createRef<HTMLDivElement>();
public static defaultProps: Partial<ExcalidrawProps> = { public static defaultProps: Partial<AppProps> = {
width: window.innerWidth, // needed for tests to pass since we directly render App in many tests
height: window.innerHeight, UIOptions: DEFAULT_UI_OPTIONS,
}; };
private scene: Scene; private scene: Scene;
constructor(props: ExcalidrawProps) { private resizeObserver: ResizeObserver | undefined;
constructor(props: AppProps) {
super(props); super(props);
const defaultAppState = getDefaultAppState(); const defaultAppState = getDefaultAppState();
const { const {
width = window.innerWidth,
height = window.innerHeight,
excalidrawRef, excalidrawRef,
viewModeEnabled = false, viewModeEnabled = false,
zenModeEnabled = false, zenModeEnabled = false,
@ -311,13 +311,13 @@ class App extends React.Component<ExcalidrawProps, AppState> {
...defaultAppState, ...defaultAppState,
theme, theme,
isLoading: true, isLoading: true,
width,
height,
...this.getCanvasOffsets(), ...this.getCanvasOffsets(),
viewModeEnabled, viewModeEnabled,
zenModeEnabled, zenModeEnabled,
gridSize: gridModeEnabled ? GRID_SIZE : null, gridSize: gridModeEnabled ? GRID_SIZE : null,
name, name,
width: window.innerWidth,
height: window.innerHeight,
}; };
if (excalidrawRef) { if (excalidrawRef) {
const readyPromise = const readyPromise =
@ -337,6 +337,8 @@ class App extends React.Component<ExcalidrawProps, AppState> {
getSceneElements: this.getSceneElements, getSceneElements: this.getSceneElements,
getAppState: () => this.state, getAppState: () => this.state,
setCanvasOffsets: this.setCanvasOffsets, setCanvasOffsets: this.setCanvasOffsets,
importLibrary: this.importLibraryFromUrl,
setToastMessage: this.setToastMessage,
} as const; } as const;
if (typeof excalidrawRef === "function") { if (typeof excalidrawRef === "function") {
excalidrawRef(api); excalidrawRef(api);
@ -423,7 +425,12 @@ class App extends React.Component<ExcalidrawProps, AppState> {
viewModeEnabled, viewModeEnabled,
} = this.state; } = this.state;
const { onCollabButtonClick, onExportToBackend, renderFooter } = this.props; const {
onCollabButtonClick,
onExportToBackend,
renderFooter,
renderCustomStats,
} = this.props;
const DEFAULT_PASTE_X = canvasDOMWidth / 2; const DEFAULT_PASTE_X = canvasDOMWidth / 2;
const DEFAULT_PASTE_Y = canvasDOMHeight / 2; const DEFAULT_PASTE_Y = canvasDOMHeight / 2;
@ -434,10 +441,6 @@ class App extends React.Component<ExcalidrawProps, AppState> {
"excalidraw--view-mode": viewModeEnabled, "excalidraw--view-mode": viewModeEnabled,
})} })}
ref={this.excalidrawContainerRef} ref={this.excalidrawContainerRef}
style={{
width: canvasDOMWidth,
height: canvasDOMHeight,
}}
> >
<LayerUI <LayerUI
canvas={this.canvas} canvas={this.canvas}
@ -464,8 +467,12 @@ class App extends React.Component<ExcalidrawProps, AppState> {
showExitZenModeBtn={ showExitZenModeBtn={
typeof this.props?.zenModeEnabled === "undefined" && zenModeEnabled typeof this.props?.zenModeEnabled === "undefined" && zenModeEnabled
} }
showThemeBtn={typeof this.props?.theme === "undefined"} showThemeBtn={
typeof this.props?.theme === "undefined" &&
this.props.UIOptions.canvasActions.theme
}
libraryReturnUrl={this.props.libraryReturnUrl} libraryReturnUrl={this.props.libraryReturnUrl}
UIOptions={this.props.UIOptions}
/> />
<div className="excalidraw-textEditorContainer" /> <div className="excalidraw-textEditorContainer" />
{this.state.showStats && ( {this.state.showStats && (
@ -474,6 +481,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
setAppState={this.setAppState} setAppState={this.setAppState}
elements={this.scene.getElements()} elements={this.scene.getElements()}
onClose={this.toggleStats} onClose={this.toggleStats}
renderCustomStats={renderCustomStats}
/> />
)} )}
{this.state.toastMessage !== null && ( {this.state.toastMessage !== null && (
@ -547,7 +555,6 @@ class App extends React.Component<ExcalidrawProps, AppState> {
if (typeof this.props.name !== "undefined") { if (typeof this.props.name !== "undefined") {
name = this.props.name; name = this.props.name;
} }
this.setState( this.setState(
(state) => { (state) => {
// using Object.assign instead of spread to fool TS 4.2.2+ into // using Object.assign instead of spread to fool TS 4.2.2+ into
@ -556,10 +563,6 @@ class App extends React.Component<ExcalidrawProps, AppState> {
return Object.assign(actionResult.appState || {}, { return Object.assign(actionResult.appState || {}, {
editingElement: editingElement:
editingElement || actionResult.appState?.editingElement || null, editingElement || actionResult.appState?.editingElement || null,
width: state.width,
height: state.height,
offsetTop: state.offsetTop,
offsetLeft: state.offsetLeft,
viewModeEnabled, viewModeEnabled,
zenModeEnabled, zenModeEnabled,
gridSize, gridSize,
@ -604,8 +607,17 @@ class App extends React.Component<ExcalidrawProps, AppState> {
this.onSceneUpdated(); this.onSceneUpdated();
}; };
private importLibraryFromUrl = async (url: string) => { private importLibraryFromUrl = async (url: string, token?: string | null) => {
window.history.replaceState({}, APP_NAME, window.location.origin); if (window.location.hash.includes(URL_HASH_KEYS.addLibrary)) {
const hash = new URLSearchParams(window.location.hash.slice(1));
hash.delete(URL_HASH_KEYS.addLibrary);
window.history.replaceState({}, APP_NAME, `#${hash.toString()}`);
} else if (window.location.search.includes(URL_QUERY_KEYS.addLibrary)) {
const query = new URLSearchParams(window.location.search);
query.delete(URL_QUERY_KEYS.addLibrary);
window.history.replaceState({}, APP_NAME, `?${query.toString()}`);
}
try { try {
const request = await fetch(decodeURIComponent(url)); const request = await fetch(decodeURIComponent(url));
const blob = await request.blob(); const blob = await request.blob();
@ -614,14 +626,17 @@ class App extends React.Component<ExcalidrawProps, AppState> {
throw new Error(); throw new Error();
} }
if ( if (
token === Library.csrfToken ||
window.confirm( window.confirm(
t("alerts.confirmAddLibrary", { numShapes: json.library.length }), t("alerts.confirmAddLibrary", { numShapes: json.library.length }),
) )
) { ) {
await Library.importLibrary(blob); await Library.importLibrary(blob);
this.setState({ // hack to rerender the library items after import
isLibraryOpen: true, if (this.state.isLibraryOpen) {
}); this.setState({ isLibraryOpen: false });
}
this.setState({ isLibraryOpen: true });
} }
} catch (error) { } catch (error) {
window.alert(t("alerts.errorLoadingLibrary")); window.alert(t("alerts.errorLoadingLibrary"));
@ -680,7 +695,6 @@ class App extends React.Component<ExcalidrawProps, AppState> {
if (!this.state.isLoading) { if (!this.state.isLoading) {
this.setState({ isLoading: true }); this.setState({ isLoading: true });
} }
let initialData = null; let initialData = null;
try { try {
initialData = (await this.props.initialData) || null; initialData = (await this.props.initialData) || null;
@ -689,7 +703,6 @@ class App extends React.Component<ExcalidrawProps, AppState> {
} }
const scene = restore(initialData, null); const scene = restore(initialData, null);
scene.appState = { scene.appState = {
...scene.appState, ...scene.appState,
isLoading: false, isLoading: false,
@ -717,12 +730,18 @@ class App extends React.Component<ExcalidrawProps, AppState> {
commitToHistory: true, commitToHistory: true,
}); });
const addToLibraryUrl = new URLSearchParams(window.location.search).get( const libraryUrl =
"addLibrary", // current
); new URLSearchParams(window.location.hash.slice(1)).get(
URL_HASH_KEYS.addLibrary,
) ||
// legacy, kept for compat reasons
new URLSearchParams(window.location.search).get(
URL_QUERY_KEYS.addLibrary,
);
if (addToLibraryUrl) { if (libraryUrl) {
await this.importLibraryFromUrl(addToLibraryUrl); await this.importLibraryFromUrl(libraryUrl);
} }
}; };
@ -755,19 +774,24 @@ class App extends React.Component<ExcalidrawProps, AppState> {
this.scene.addCallback(this.onSceneUpdated); this.scene.addCallback(this.onSceneUpdated);
this.addEventListeners(); this.addEventListeners();
if ("ResizeObserver" in window && this.excalidrawContainerRef?.current) {
this.resizeObserver = new ResizeObserver(() => {
this.updateDOMRect();
});
this.resizeObserver?.observe(this.excalidrawContainerRef.current);
}
const searchParams = new URLSearchParams(window.location.search.slice(1)); const searchParams = new URLSearchParams(window.location.search.slice(1));
if (searchParams.has("web-share-target")) { if (searchParams.has("web-share-target")) {
// Obtain a file that was shared via the Web Share Target API. // Obtain a file that was shared via the Web Share Target API.
this.restoreFileFromShare(); this.restoreFileFromShare();
} else { } else {
this.setState(this.getCanvasOffsets(), () => { this.updateDOMRect(this.initializeScene);
this.initializeScene();
});
} }
} }
public componentWillUnmount() { public componentWillUnmount() {
this.resizeObserver?.disconnect();
this.unmounted = true; this.unmounted = true;
this.removeEventListeners(); this.removeEventListeners();
this.scene.destroy(); this.scene.destroy();
@ -859,22 +883,11 @@ class App extends React.Component<ExcalidrawProps, AppState> {
window.addEventListener(EVENT.DROP, this.disableEvent, false); window.addEventListener(EVENT.DROP, this.disableEvent, false);
} }
componentDidUpdate(prevProps: ExcalidrawProps, prevState: AppState) { componentDidUpdate(prevProps: AppProps, prevState: AppState) {
if (prevProps.langCode !== this.props.langCode) { if (prevProps.langCode !== this.props.langCode) {
this.updateLanguage(); this.updateLanguage();
} }
if (
prevProps.width !== this.props.width ||
prevProps.height !== this.props.height
) {
this.setState({
width: this.props.width ?? window.innerWidth,
height: this.props.height ?? window.innerHeight,
...this.getCanvasOffsets(),
});
}
if (prevProps.viewModeEnabled !== this.props.viewModeEnabled) { if (prevProps.viewModeEnabled !== this.props.viewModeEnabled) {
this.setState( this.setState(
{ viewModeEnabled: !!this.props.viewModeEnabled }, { viewModeEnabled: !!this.props.viewModeEnabled },
@ -1334,6 +1347,10 @@ class App extends React.Component<ExcalidrawProps, AppState> {
this.setState({ toastMessage: null }); this.setState({ toastMessage: null });
}; };
setToastMessage = (toastMessage: string) => {
this.setState({ toastMessage });
};
restoreFileFromShare = async () => { restoreFileFromShare = async () => {
try { try {
const webShareTargetCache = await caches.open("web-share-target"); const webShareTargetCache = await caches.open("web-share-target");
@ -1907,8 +1924,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
} }
resetCursor(this.canvas); resetCursor(this.canvas);
if (!event[KEYS.CTRL_OR_CMD] && !this.state.viewModeEnabled) {
if (!event[KEYS.CTRL_OR_CMD]) {
this.startTextEditing({ this.startTextEditing({
sceneX, sceneX,
sceneY, sceneY,
@ -2282,10 +2298,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
touchTimeout = window.setTimeout(() => { touchTimeout = window.setTimeout(() => {
touchTimeout = 0; touchTimeout = 0;
if (!invalidateContextMenu) { if (!invalidateContextMenu) {
this.openContextMenu({ this.handleCanvasContextMenu(event);
clientX: event.clientX,
clientY: event.clientY,
});
} }
}, TOUCH_CTX_MENU_TIMEOUT); }, TOUCH_CTX_MENU_TIMEOUT);
} }
@ -3632,9 +3645,20 @@ class App extends React.Component<ExcalidrawProps, AppState> {
const file = event.dataTransfer?.files[0]; const file = event.dataTransfer?.files[0];
if ( if (
file?.type === "application/json" || file?.type === MIME_TYPES.excalidrawlib ||
file?.name.endsWith(".excalidraw") file?.name?.endsWith(".excalidrawlib")
) { ) {
Library.importLibrary(file)
.then(() => {
// Close and then open to get the libraries updated
this.setState({ isLibraryOpen: false });
this.setState({ isLibraryOpen: true });
})
.catch((error) =>
this.setState({ isLoading: false, errorMessage: error.message }),
);
// default: assume an Excalidraw file regardless of extension/MimeType
} else {
this.setState({ isLoading: true }); this.setState({ isLoading: true });
if (supported) { if (supported) {
try { try {
@ -3646,23 +3670,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
console.warn(error.name, error.message); console.warn(error.name, error.message);
} }
} }
this.loadFileToCanvas(file); await this.loadFileToCanvas(file);
} else if (
file?.type === MIME_TYPES.excalidrawlib ||
file?.name.endsWith(".excalidrawlib")
) {
Library.importLibrary(file)
.then(() => {
this.setState({ isLibraryOpen: false });
})
.catch((error) =>
this.setState({ isLoading: false, errorMessage: error.message }),
);
} else {
this.setState({
isLoading: false,
errorMessage: t("alerts.couldNotLoadInvalidFile"),
});
} }
}; };
@ -3687,7 +3695,19 @@ class App extends React.Component<ExcalidrawProps, AppState> {
event: React.PointerEvent<HTMLCanvasElement>, event: React.PointerEvent<HTMLCanvasElement>,
) => { ) => {
event.preventDefault(); event.preventDefault();
this.openContextMenu(event);
const { x, y } = viewportCoordsToSceneCoords(event, this.state);
const element = this.getElementAtPosition(x, y);
const type = element ? "element" : "canvas";
if (element && !this.state.selectedElementIds[element.id]) {
this.setState({ selectedElementIds: { [element.id]: true } }, () => {
this._openContextMenu(event, type);
});
} else {
this._openContextMenu(event, type);
}
}; };
private maybeDragNewGenericElement = ( private maybeDragNewGenericElement = (
@ -3777,18 +3797,17 @@ class App extends React.Component<ExcalidrawProps, AppState> {
return false; return false;
}; };
private openContextMenu = ({ /** @private use this.handleCanvasContextMenu */
clientX, private _openContextMenu = (
clientY, {
}: { clientX,
clientX: number; clientY,
clientY: number; }: {
}) => { clientX: number;
const { x, y } = viewportCoordsToSceneCoords( clientY: number;
{ clientX, clientY }, },
this.state, type: "canvas" | "element",
); ) => {
const maybeGroupAction = actionGroup.contextItemPredicate!( const maybeGroupAction = actionGroup.contextItemPredicate!(
this.actionManager.getElementsIncludingDeleted(), this.actionManager.getElementsIncludingDeleted(),
this.actionManager.getAppState(), this.actionManager.getAppState(),
@ -3799,12 +3818,22 @@ class App extends React.Component<ExcalidrawProps, AppState> {
this.actionManager.getAppState(), this.actionManager.getAppState(),
); );
const maybeFlipHorizontal = actionFlipHorizontal.contextItemPredicate!(
this.actionManager.getElementsIncludingDeleted(),
this.actionManager.getAppState(),
);
const maybeFlipVertical = actionFlipVertical.contextItemPredicate!(
this.actionManager.getElementsIncludingDeleted(),
this.actionManager.getAppState(),
);
const separator = "separator"; const separator = "separator";
const _isMobile = isMobile(); const _isMobile = isMobile();
const elements = this.scene.getElements(); const elements = this.scene.getElements();
const element = this.getElementAtPosition(x, y);
const options: ContextMenuOption[] = []; const options: ContextMenuOption[] = [];
if (probablySupportsClipboardBlob && elements.length > 0) { if (probablySupportsClipboardBlob && elements.length > 0) {
options.push(actionCopyAsPng); options.push(actionCopyAsPng);
@ -3813,7 +3842,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
if (probablySupportsClipboardWriteText && elements.length > 0) { if (probablySupportsClipboardWriteText && elements.length > 0) {
options.push(actionCopyAsSvg); options.push(actionCopyAsSvg);
} }
if (!element) { if (type === "canvas") {
const viewModeOptions = [ const viewModeOptions = [
...options, ...options,
typeof this.props.gridModeEnabled === "undefined" && typeof this.props.gridModeEnabled === "undefined" &&
@ -3877,10 +3906,6 @@ class App extends React.Component<ExcalidrawProps, AppState> {
return; return;
} }
if (!this.state.selectedElementIds[element.id]) {
this.setState({ selectedElementIds: { [element.id]: true } });
}
if (this.state.viewModeEnabled) { if (this.state.viewModeEnabled) {
ContextMenu.push({ ContextMenu.push({
options: [navigator.clipboard && actionCopy, ...options], options: [navigator.clipboard && actionCopy, ...options],
@ -3923,6 +3948,9 @@ class App extends React.Component<ExcalidrawProps, AppState> {
actionSendToBack, actionSendToBack,
actionBringToFront, actionBringToFront,
separator, separator,
maybeFlipHorizontal && actionFlipHorizontal,
maybeFlipVertical && actionFlipVertical,
(maybeFlipHorizontal || maybeFlipVertical) && separator,
actionDuplicateSelection, actionDuplicateSelection,
actionDeleteSelected, actionDeleteSelected,
], ],
@ -4063,14 +4091,56 @@ class App extends React.Component<ExcalidrawProps, AppState> {
} }
}, 300); }, 300);
private updateDOMRect = (cb?: () => void) => {
if (this.excalidrawContainerRef?.current) {
const excalidrawContainer = this.excalidrawContainerRef.current;
const {
width,
height,
left: offsetLeft,
top: offsetTop,
} = excalidrawContainer.getBoundingClientRect();
const {
width: currentWidth,
height: currentHeight,
offsetTop: currentOffsetTop,
offsetLeft: currentOffsetLeft,
} = this.state;
if (
width === currentWidth &&
height === currentHeight &&
offsetLeft === currentOffsetLeft &&
offsetTop === currentOffsetTop
) {
if (cb) {
cb();
}
return;
}
this.setState(
{
width,
height,
offsetLeft,
offsetTop,
},
() => {
cb && cb();
},
);
}
};
public setCanvasOffsets = () => { public setCanvasOffsets = () => {
this.setState({ ...this.getCanvasOffsets() }); this.setState({ ...this.getCanvasOffsets() });
}; };
private getCanvasOffsets(): Pick<AppState, "offsetTop" | "offsetLeft"> { private getCanvasOffsets(): Pick<AppState, "offsetTop" | "offsetLeft"> {
if (this.excalidrawContainerRef?.current?.parentElement) { if (this.excalidrawContainerRef?.current) {
const parentElement = this.excalidrawContainerRef.current.parentElement; const excalidrawContainer = this.excalidrawContainerRef.current;
const { left, top } = parentElement.getBoundingClientRect(); const { left, top } = excalidrawContainer.getBoundingClientRect();
return { return {
offsetLeft: left, offsetLeft: left,
offsetTop: top, offsetTop: top,
@ -4104,9 +4174,6 @@ declare global {
history: SceneHistory; history: SceneHistory;
app: InstanceType<typeof App>; app: InstanceType<typeof App>;
library: typeof Library; library: typeof Library;
collab: InstanceType<
typeof import("../excalidraw-app/collab/CollabWrapper").default
>;
}; };
} }
} }

View File

@ -1,4 +1,3 @@
import React from "react";
import clsx from "clsx"; import clsx from "clsx";
export const ButtonIconCycle = <T extends any>({ export const ButtonIconCycle = <T extends any>({
@ -14,11 +13,11 @@ export const ButtonIconCycle = <T extends any>({
}) => { }) => {
const current = options.find((op) => op.value === value); const current = options.find((op) => op.value === value);
function cycle() { const cycle = () => {
const index = options.indexOf(current!); const index = options.indexOf(current!);
const next = (index + 1) % options.length; const next = (index + 1) % options.length;
onChange(options[next].value); onChange(options[next].value);
} };
return ( return (
<label key={group} className={clsx({ active: current!.value !== null })}> <label key={group} className={clsx({ active: current!.value !== null })}>

View File

@ -2,7 +2,7 @@ import React from "react";
import clsx from "clsx"; import clsx from "clsx";
import { ToolButton } from "./ToolButton"; import { ToolButton } from "./ToolButton";
import { t } from "../i18n"; import { t } from "../i18n";
import useIsMobile from "../is-mobile"; import { useIsMobile } from "../is-mobile";
import { users } from "./icons"; import { users } from "./icons";
import "./CollabButton.scss"; import "./CollabButton.scss";

View File

@ -2,7 +2,7 @@ import clsx from "clsx";
import React, { useEffect } from "react"; import React, { useEffect } from "react";
import { useCallbackRefState } from "../hooks/useCallbackRefState"; import { useCallbackRefState } from "../hooks/useCallbackRefState";
import { t } from "../i18n"; import { t } from "../i18n";
import useIsMobile from "../is-mobile"; import { useIsMobile } from "../is-mobile";
import { KEYS } from "../keys"; import { KEYS } from "../keys";
import "./Dialog.scss"; import "./Dialog.scss";
import { back, close } from "./icons"; import { back, close } from "./icons";

View File

@ -6,7 +6,7 @@ import { canvasToBlob } from "../data/blob";
import { NonDeletedExcalidrawElement } from "../element/types"; import { NonDeletedExcalidrawElement } from "../element/types";
import { CanvasError } from "../errors"; import { CanvasError } from "../errors";
import { t } from "../i18n"; import { t } from "../i18n";
import useIsMobile from "../is-mobile"; import { useIsMobile } from "../is-mobile";
import { getSelectedElements, isSomeElementSelected } from "../scene"; import { getSelectedElements, isSomeElementSelected } from "../scene";
import { exportToCanvas, getExportSize } from "../scene/export"; import { exportToCanvas, getExportSize } from "../scene/export";
import { AppState } from "../types"; import { AppState } from "../types";

View File

@ -349,6 +349,14 @@ export const HelpDialog = ({ onClose }: { onClose?: () => void }) => {
label={t("labels.ungroup")} label={t("labels.ungroup")}
shortcuts={[getShortcutKey("CtrlOrCmd+Shift+G")]} shortcuts={[getShortcutKey("CtrlOrCmd+Shift+G")]}
/> />
<Shortcut
label={t("labels.flipHorizontal")}
shortcuts={[getShortcutKey("Shift+H")]}
/>
<Shortcut
label={t("labels.flipVertical")}
shortcuts={[getShortcutKey("Shift+V")]}
/>
</ShortcutIsland> </ShortcutIsland>
</Column> </Column>
</Columns> </Columns>

View File

@ -9,7 +9,13 @@ type HelpIconProps = {
}; };
export const HelpIcon = (props: HelpIconProps) => ( export const HelpIcon = (props: HelpIconProps) => (
<label title={`${props.title} — ?`} className="help-icon"> <button
<div onClick={props.onClick}>{questionCircle}</div> className="help-icon"
</label> onClick={props.onClick}
type="button"
title={`${props.title} — ?`}
aria-label={props.title}
>
{questionCircle}
</button>
); );

View File

@ -14,10 +14,16 @@ import { Library } from "../data/library";
import { isTextElement, showSelectedShapeActions } from "../element"; import { isTextElement, showSelectedShapeActions } from "../element";
import { NonDeletedExcalidrawElement } from "../element/types"; import { NonDeletedExcalidrawElement } from "../element/types";
import { Language, t } from "../i18n"; import { Language, t } from "../i18n";
import useIsMobile from "../is-mobile"; import { useIsMobile } from "../is-mobile";
import { calculateScrollCenter, getSelectedElements } from "../scene"; import { calculateScrollCenter, getSelectedElements } from "../scene";
import { ExportType } from "../scene/types"; import { ExportType } from "../scene/types";
import { AppState, ExcalidrawProps, LibraryItem, LibraryItems } from "../types"; import {
AppProps,
AppState,
ExcalidrawProps,
LibraryItem,
LibraryItems,
} from "../types";
import { muteFSAbortError } from "../utils"; import { muteFSAbortError } from "../utils";
import { SelectedShapeActions, ShapesSwitcher, ZoomActions } from "./Actions"; import { SelectedShapeActions, ShapesSwitcher, ZoomActions } from "./Actions";
import { BackgroundPickerAndDarkModeToggle } from "./BackgroundPickerAndDarkModeToggle"; import { BackgroundPickerAndDarkModeToggle } from "./BackgroundPickerAndDarkModeToggle";
@ -65,6 +71,7 @@ interface LayerUIProps {
renderCustomFooter?: (isMobile: boolean) => JSX.Element; renderCustomFooter?: (isMobile: boolean) => JSX.Element;
viewModeEnabled: boolean; viewModeEnabled: boolean;
libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"]; libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"];
UIOptions: AppProps["UIOptions"];
} }
const useOnClickOutside = ( const useOnClickOutside = (
@ -121,10 +128,11 @@ const LibraryMenuItems = ({
const rows = []; const rows = [];
let addedPendingElements = false; let addedPendingElements = false;
const referrer = libraryReturnUrl || window.location.origin; const referrer =
libraryReturnUrl || window.location.origin + window.location.pathname;
rows.push( rows.push(
<div className="layer-ui__library-header"> <div className="layer-ui__library-header" key="library-header">
<ToolButton <ToolButton
key="import" key="import"
type="button" type="button"
@ -134,9 +142,9 @@ const LibraryMenuItems = ({
onClick={() => { onClick={() => {
importLibraryFromJSON() importLibraryFromJSON()
.then(() => { .then(() => {
// Maybe we should close and open the menu so that the items get updated. // Close and then open to get the libraries updated
// But for now we just close the menu.
setAppState({ isLibraryOpen: false }); setAppState({ isLibraryOpen: false });
setAppState({ isLibraryOpen: true });
}) })
.catch(muteFSAbortError) .catch(muteFSAbortError)
.catch((error) => { .catch((error) => {
@ -178,7 +186,7 @@ const LibraryMenuItems = ({
<a <a
href={`https://libraries.excalidraw.com?target=${ href={`https://libraries.excalidraw.com?target=${
window.name || "_blank" window.name || "_blank"
}&referrer=${referrer}`} }&referrer=${referrer}&useHash=true&token=${Library.csrfToken}`}
target="_excalidraw_libraries" target="_excalidraw_libraries"
> >
{t("labels.libraries")} {t("labels.libraries")}
@ -338,6 +346,7 @@ const LayerUI = ({
renderCustomFooter, renderCustomFooter,
viewModeEnabled, viewModeEnabled,
libraryReturnUrl, libraryReturnUrl,
UIOptions,
}: LayerUIProps) => { }: LayerUIProps) => {
const isMobile = useIsMobile(); const isMobile = useIsMobile();
@ -349,6 +358,7 @@ const LayerUI = ({
href="https://blog.excalidraw.com/end-to-end-encryption/" href="https://blog.excalidraw.com/end-to-end-encryption/"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
aria-label={t("encrypted.link")}
> >
<Tooltip label={t("encrypted.tooltip")} position="above" long={true}> <Tooltip label={t("encrypted.tooltip")} position="above" long={true}>
{shield} {shield}
@ -357,6 +367,10 @@ const LayerUI = ({
); );
const renderExportDialog = () => { const renderExportDialog = () => {
if (!UIOptions.canvasActions.export) {
return null;
}
const createExporter = (type: ExportType): ExportCB => async ( const createExporter = (type: ExportType): ExportCB => async (
exportedElements, exportedElements,
scale, scale,

View File

@ -4,7 +4,7 @@ import React, { useEffect, useRef, useState } from "react";
import { close } from "../components/icons"; import { close } from "../components/icons";
import { MIME_TYPES } from "../constants"; import { MIME_TYPES } from "../constants";
import { t } from "../i18n"; import { t } from "../i18n";
import useIsMobile from "../is-mobile"; import { useIsMobile } from "../is-mobile";
import { exportToSvg } from "../scene/export"; import { exportToSvg } from "../scene/export";
import { LibraryItem } from "../types"; import { LibraryItem } from "../types";
import "./LibraryUnit.scss"; import "./LibraryUnit.scss";

View File

@ -1,49 +1,22 @@
import React, { useEffect, useState } from "react"; import React from "react";
import { copyTextToSystemClipboard } from "../clipboard";
import { DEFAULT_VERSION } from "../constants";
import { getCommonBounds } from "../element/bounds"; import { getCommonBounds } from "../element/bounds";
import { NonDeletedExcalidrawElement } from "../element/types"; import { NonDeletedExcalidrawElement } from "../element/types";
import {
getElementsStorageSize,
getTotalStorageSize,
} from "../excalidraw-app/data/localStorage";
import { t } from "../i18n"; import { t } from "../i18n";
import useIsMobile from "../is-mobile"; import { useIsMobile } from "../is-mobile";
import { getTargetElements } from "../scene"; import { getTargetElements } from "../scene";
import { AppState } from "../types"; import { AppState, ExcalidrawProps } from "../types";
import { debounce, getVersion, nFormatter } from "../utils";
import { close } from "./icons"; import { close } from "./icons";
import { Island } from "./Island"; import { Island } from "./Island";
import "./Stats.scss"; import "./Stats.scss";
type StorageSizes = { scene: number; total: number };
const getStorageSizes = debounce((cb: (sizes: StorageSizes) => void) => {
cb({
scene: getElementsStorageSize(),
total: getTotalStorageSize(),
});
}, 500);
export const Stats = (props: { export const Stats = (props: {
appState: AppState; appState: AppState;
setAppState: React.Component<any, AppState>["setState"]; setAppState: React.Component<any, AppState>["setState"];
elements: readonly NonDeletedExcalidrawElement[]; elements: readonly NonDeletedExcalidrawElement[];
onClose: () => void; onClose: () => void;
renderCustomStats: ExcalidrawProps["renderCustomStats"];
}) => { }) => {
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const [storageSizes, setStorageSizes] = useState<StorageSizes>({
scene: 0,
total: 0,
});
useEffect(() => {
getStorageSizes((sizes) => {
setStorageSizes(sizes);
});
});
useEffect(() => () => getStorageSizes.cancel(), []);
const boundingBox = getCommonBounds(props.elements); const boundingBox = getCommonBounds(props.elements);
const selectedElements = getTargetElements(props.elements, props.appState); const selectedElements = getTargetElements(props.elements, props.appState);
@ -53,17 +26,6 @@ export const Stats = (props: {
return null; return null;
} }
const version = getVersion();
let hash;
let timestamp;
if (version !== DEFAULT_VERSION) {
timestamp = version.slice(0, 16).replace("T", " ");
hash = version.slice(21);
} else {
timestamp = t("stats.versionNotAvailable");
}
return ( return (
<div className="Stats"> <div className="Stats">
<Island padding={2}> <Island padding={2}>
@ -88,17 +50,7 @@ export const Stats = (props: {
<td>{t("stats.height")}</td> <td>{t("stats.height")}</td>
<td>{Math.round(boundingBox[3]) - Math.round(boundingBox[1])}</td> <td>{Math.round(boundingBox[3]) - Math.round(boundingBox[1])}</td>
</tr> </tr>
<tr>
<th colSpan={2}>{t("stats.storage")}</th>
</tr>
<tr>
<td>{t("stats.scene")}</td>
<td>{nFormatter(storageSizes.scene, 1)}</td>
</tr>
<tr>
<td>{t("stats.total")}</td>
<td>{nFormatter(storageSizes.total, 1)}</td>
</tr>
{selectedElements.length === 1 && ( {selectedElements.length === 1 && (
<tr> <tr>
<th colSpan={2}>{t("stats.element")}</th> <th colSpan={2}>{t("stats.element")}</th>
@ -120,31 +72,17 @@ export const Stats = (props: {
<> <>
<tr> <tr>
<td>{"x"}</td> <td>{"x"}</td>
<td> <td>{Math.round(selectedBoundingBox[0])}</td>
{Math.round(
selectedElements.length === 1
? selectedElements[0].x
: selectedBoundingBox[0],
)}
</td>
</tr> </tr>
<tr> <tr>
<td>{"y"}</td> <td>{"y"}</td>
<td> <td>{Math.round(selectedBoundingBox[1])}</td>
{Math.round(
selectedElements.length === 1
? selectedElements[0].y
: selectedBoundingBox[1],
)}
</td>
</tr> </tr>
<tr> <tr>
<td>{t("stats.width")}</td> <td>{t("stats.width")}</td>
<td> <td>
{Math.round( {Math.round(
selectedElements.length === 1 selectedBoundingBox[2] - selectedBoundingBox[0],
? selectedElements[0].width
: selectedBoundingBox[2] - selectedBoundingBox[0],
)} )}
</td> </td>
</tr> </tr>
@ -152,9 +90,7 @@ export const Stats = (props: {
<td>{t("stats.height")}</td> <td>{t("stats.height")}</td>
<td> <td>
{Math.round( {Math.round(
selectedElements.length === 1 selectedBoundingBox[3] - selectedBoundingBox[1],
? selectedElements[0].height
: selectedBoundingBox[3] - selectedBoundingBox[1],
)} )}
</td> </td>
</tr> </tr>
@ -170,28 +106,7 @@ export const Stats = (props: {
</td> </td>
</tr> </tr>
)} )}
<tr> {props.renderCustomStats?.(props.elements, props.appState)}
<th colSpan={2}>{t("stats.version")}</th>
</tr>
<tr>
<td
colSpan={2}
style={{ textAlign: "center", cursor: "pointer" }}
onClick={async () => {
try {
await copyTextToSystemClipboard(getVersion());
props.setAppState({
toastMessage: t("toast.copyToClipboard"),
});
} catch {}
}}
title={t("stats.versionCopy")}
>
{timestamp}
<br />
{hash}
</td>
</tr>
</tbody> </tbody>
</table> </table>
</Island> </Island>

View File

@ -1,5 +1,6 @@
import { FontFamily } from "./element/types"; import { FontFamily } from "./element/types";
import cssVariables from "./css/variables.module.scss"; import cssVariables from "./css/variables.module.scss";
import { AppProps } from "./types";
export const APP_NAME = "Excalidraw"; export const APP_NAME = "Excalidraw";
@ -117,3 +118,23 @@ export const MODES = {
}; };
export const THEME_FILTER = cssVariables.themeFilter; export const THEME_FILTER = cssVariables.themeFilter;
export const URL_QUERY_KEYS = {
addLibrary: "addLibrary",
} as const;
export const URL_HASH_KEYS = {
addLibrary: "addLibrary",
} as const;
export const DEFAULT_UI_OPTIONS: AppProps["UIOptions"] = {
canvasActions: {
changeViewBackgroundColor: true,
clearCanvas: true,
export: true,
loadScene: true,
saveAsScene: true,
saveScene: true,
theme: true,
},
};

View File

@ -16,6 +16,8 @@
bottom: 0; bottom: 0;
left: 0; left: 0;
right: 0; right: 0;
height: 100%;
width: 100%;
// serves 2 purposes: // serves 2 purposes:
// 1. prevent selecting text outside the component when double-clicking or // 1. prevent selecting text outside the component when double-clicking or
@ -47,6 +49,12 @@
z-index: var(--zIndex-canvas); z-index: var(--zIndex-canvas);
} }
#canvas {
// Remove the main canvas from document flow to avoid resizeObserver
// feedback loop (see https://github.com/excalidraw/excalidraw/pull/3379)
position: absolute;
}
&.theme--dark { &.theme--dark {
// The percentage is inspired by // The percentage is inspired by
// https://material.io/design/color/dark-theme.html#properties, which // https://material.io/design/color/dark-theme.html#properties, which
@ -454,6 +462,14 @@
fill: $oc-gray-6; fill: $oc-gray-6;
bottom: 14px; bottom: 14px;
width: 1.5rem; width: 1.5rem;
padding: 0;
margin: 0;
background: none;
color: var(--icon-fill-color);
&:hover {
background: none;
}
:root[dir="ltr"] & { :root[dir="ltr"] & {
right: 14px; right: 14px;

View File

@ -1,5 +1,5 @@
import { cleanAppStateForExport } from "../appState"; import { cleanAppStateForExport } from "../appState";
import { EXPORT_DATA_TYPES, MIME_TYPES } from "../constants"; import { EXPORT_DATA_TYPES } from "../constants";
import { clearElementsForExport } from "../element"; import { clearElementsForExport } from "../element";
import { CanvasError } from "../errors"; import { CanvasError } from "../errors";
import { t } from "../i18n"; import { t } from "../i18n";
@ -95,13 +95,7 @@ export const loadFromBlob = async (
elements: clearElementsForExport(data.elements || []), elements: clearElementsForExport(data.elements || []),
appState: { appState: {
theme: localAppState?.theme, theme: localAppState?.theme,
fileHandle: fileHandle: blob.handle ?? null,
blob.handle &&
["application/json", MIME_TYPES.excalidraw].includes(
getMimeType(blob),
)
? blob.handle
: null,
...cleanAppStateForExport(data.appState || {}), ...cleanAppStateForExport(data.appState || {}),
...(localAppState ...(localAppState
? calculateScrollCenter(data.elements || [], localAppState, null) ? calculateScrollCenter(data.elements || [], localAppState, null)

View File

@ -116,5 +116,5 @@ export const importLibraryFromJSON = async () => {
extensions: [".json", ".excalidrawlib"], extensions: [".json", ".excalidrawlib"],
*/ */
}); });
Library.importLibrary(blob); await Library.importLibrary(blob);
}; };

View File

@ -4,9 +4,11 @@ import { restoreElements } from "./restore";
import { STORAGE_KEYS } from "../constants"; import { STORAGE_KEYS } from "../constants";
import { getNonDeletedElements } from "../element"; import { getNonDeletedElements } from "../element";
import { NonDeleted, ExcalidrawElement } from "../element/types"; import { NonDeleted, ExcalidrawElement } from "../element/types";
import { nanoid } from "nanoid";
export class Library { export class Library {
private static libraryCache: LibraryItems | null = null; private static libraryCache: LibraryItems | null = null;
public static csrfToken = nanoid();
static resetLibrary = () => { static resetLibrary = () => {
Library.libraryCache = null; Library.libraryCache = null;

View File

@ -144,7 +144,7 @@ export const restoreElements = (
export const restoreAppState = ( export const restoreAppState = (
appState: ImportedDataState["appState"], appState: ImportedDataState["appState"],
localAppState: Partial<AppState> | null, localAppState: Partial<AppState> | null,
): AppState => { ): DataState["appState"] => {
appState = appState || {}; appState = appState || {};
const defaultAppState = getDefaultAppState(); const defaultAppState = getDefaultAppState();
@ -166,8 +166,6 @@ export const restoreAppState = (
return { return {
...nextAppState, ...nextAppState,
offsetLeft: appState.offsetLeft || 0,
offsetTop: appState.offsetTop || 0,
// Migrates from previous version where appState.zoom was a number // Migrates from previous version where appState.zoom was a number
zoom: zoom:
typeof appState.zoom === "number" typeof appState.zoom === "number"

View File

@ -6,7 +6,7 @@ export interface DataState {
version?: string; version?: string;
source?: string; source?: string;
elements: readonly ExcalidrawElement[]; elements: readonly ExcalidrawElement[];
appState: MarkOptional<AppState, "offsetTop" | "offsetLeft">; appState: Omit<AppState, "offsetTop" | "offsetLeft" | "width" | "height">;
} }
export interface ImportedDataState { export interface ImportedDataState {

View File

@ -31,7 +31,7 @@ import {
import { PointerDownState } from "../components/App"; import { PointerDownState } from "../components/App";
import { Point } from "../types"; import { Point } from "../types";
const normalizeAngle = (angle: number): number => { export const normalizeAngle = (angle: number): number => {
if (angle >= 2 * Math.PI) { if (angle >= 2 * Math.PI) {
return angle - 2 * Math.PI; return angle - 2 * Math.PI;
} }
@ -181,7 +181,7 @@ const getPerfectElementSizeWithRotation = (
return rotate(size.width, size.height, 0, 0, -angle); return rotate(size.width, size.height, 0, 0, -angle);
}; };
const reshapeSingleTwoPointElement = ( export const reshapeSingleTwoPointElement = (
element: NonDeleted<ExcalidrawLinearElement>, element: NonDeleted<ExcalidrawLinearElement>,
resizeArrowDirection: "origin" | "end", resizeArrowDirection: "origin" | "end",
isRotateWithDiscreteAngle: boolean, isRotateWithDiscreteAngle: boolean,
@ -378,7 +378,7 @@ const resizeSingleTextElement = (
} }
}; };
const resizeSingleElement = ( export const resizeSingleElement = (
stateAtResizeStart: NonDeletedExcalidrawElement, stateAtResizeStart: NonDeletedExcalidrawElement,
shouldKeepSidesRatio: boolean, shouldKeepSidesRatio: boolean,
element: NonDeletedExcalidrawElement, element: NonDeletedExcalidrawElement,

View File

@ -0,0 +1,85 @@
import React, { useEffect, useState } from "react";
import { debounce, getVersion, nFormatter } from "../utils";
import {
getElementsStorageSize,
getTotalStorageSize,
} from "./data/localStorage";
import { DEFAULT_VERSION } from "../constants";
import { t } from "../i18n";
import { copyTextToSystemClipboard } from "../clipboard";
type StorageSizes = { scene: number; total: number };
const STORAGE_SIZE_TIMEOUT = 500;
const getStorageSizes = debounce((cb: (sizes: StorageSizes) => void) => {
cb({
scene: getElementsStorageSize(),
total: getTotalStorageSize(),
});
}, STORAGE_SIZE_TIMEOUT);
type Props = {
setToastMessage: (message: string) => void;
};
const CustomStats = (props: Props) => {
const [storageSizes, setStorageSizes] = useState<StorageSizes>({
scene: 0,
total: 0,
});
useEffect(() => {
getStorageSizes((sizes) => {
setStorageSizes(sizes);
});
});
useEffect(() => () => getStorageSizes.cancel(), []);
const version = getVersion();
let hash;
let timestamp;
if (version !== DEFAULT_VERSION) {
timestamp = version.slice(0, 16).replace("T", " ");
hash = version.slice(21);
} else {
timestamp = t("stats.versionNotAvailable");
}
return (
<>
<tr>
<th colSpan={2}>{t("stats.storage")}</th>
</tr>
<tr>
<td>{t("stats.scene")}</td>
<td>{nFormatter(storageSizes.scene, 1)}</td>
</tr>
<tr>
<td>{t("stats.total")}</td>
<td>{nFormatter(storageSizes.total, 1)}</td>
</tr>
<tr>
<th colSpan={2}>{t("stats.version")}</th>
</tr>
<tr>
<td
colSpan={2}
style={{ textAlign: "center", cursor: "pointer" }}
onClick={async () => {
try {
await copyTextToSystemClipboard(getVersion());
props.setToastMessage(t("toast.copyToClipboard"));
} catch {}
}}
title={t("stats.versionCopy")}
>
{timestamp}
<br />
{hash}
</td>
</tr>
</>
);
};
export default CustomStats;

View File

@ -38,7 +38,7 @@ import Portal from "./Portal";
import RoomDialog from "./RoomDialog"; import RoomDialog from "./RoomDialog";
import { createInverseContext } from "../../createInverseContext"; import { createInverseContext } from "../../createInverseContext";
import { t } from "../../i18n"; import { t } from "../../i18n";
import { UserIdleState } from "./types"; import { UserIdleState } from "../../types";
import { IDLE_THRESHOLD, ACTIVE_THRESHOLD } from "../../constants"; import { IDLE_THRESHOLD, ACTIVE_THRESHOLD } from "../../constants";
import { trackEvent } from "../../analytics"; import { trackEvent } from "../../analytics";
@ -113,8 +113,8 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
process.env.NODE_ENV === ENV.TEST || process.env.NODE_ENV === ENV.TEST ||
process.env.NODE_ENV === ENV.DEVELOPMENT process.env.NODE_ENV === ENV.DEVELOPMENT
) { ) {
window.h = window.h || ({} as Window["h"]); window.collab = window.collab || ({} as Window["collab"]);
Object.defineProperties(window.h, { Object.defineProperties(window, {
collab: { collab: {
configurable: true, configurable: true,
value: this, value: this,
@ -658,4 +658,17 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
} }
} }
declare global {
interface Window {
collab: InstanceType<typeof CollabWrapper>;
}
}
if (
process.env.NODE_ENV === ENV.TEST ||
process.env.NODE_ENV === ENV.DEVELOPMENT
) {
window.collab = window.collab || ({} as Window["collab"]);
}
export default CollabWrapper; export default CollabWrapper;

View File

@ -9,7 +9,7 @@ import CollabWrapper from "./CollabWrapper";
import { getSyncableElements } from "../../packages/excalidraw/index"; import { getSyncableElements } from "../../packages/excalidraw/index";
import { ExcalidrawElement } from "../../element/types"; import { ExcalidrawElement } from "../../element/types";
import { BROADCAST, SCENE } from "../app_constants"; import { BROADCAST, SCENE } from "../app_constants";
import { UserIdleState } from "./types"; import { UserIdleState } from "../../types";
import { trackEvent } from "../../analytics"; import { trackEvent } from "../../analytics";
class Portal { class Portal {

View File

@ -1,5 +0,0 @@
export enum UserIdleState {
ACTIVE = "active",
AWAY = "away",
IDLE = "idle",
}

View File

@ -3,8 +3,7 @@ import { restore } from "../../data/restore";
import { ImportedDataState } from "../../data/types"; import { ImportedDataState } from "../../data/types";
import { ExcalidrawElement } from "../../element/types"; import { ExcalidrawElement } from "../../element/types";
import { t } from "../../i18n"; import { t } from "../../i18n";
import { AppState } from "../../types"; import { AppState, UserIdleState } from "../../types";
import { UserIdleState } from "../collab/types";
const byteToHex = (byte: number): string => `0${byte.toString(16)}`.slice(-2); const byteToHex = (byte: number): string => `0${byte.toString(16)}`.slice(-2);

View File

@ -101,7 +101,7 @@ export const importFromLocalStorage = () => {
export const getElementsStorageSize = () => { export const getElementsStorageSize = () => {
try { try {
const elements = localStorage.getItem(STORAGE_KEYS.LOCAL_STORAGE_ELEMENTS); const elements = localStorage.getItem(STORAGE_KEYS.LOCAL_STORAGE_ELEMENTS);
const elementsSize = elements ? JSON.stringify(elements).length : 0; const elementsSize = elements?.length || 0;
return elementsSize; return elementsSize;
} catch (error) { } catch (error) {
console.error(error); console.error(error);
@ -117,9 +117,9 @@ export const getTotalStorageSize = () => {
APP_STORAGE_KEYS.LOCAL_STORAGE_LIBRARY, APP_STORAGE_KEYS.LOCAL_STORAGE_LIBRARY,
); );
const appStateSize = appState ? JSON.stringify(appState).length : 0; const appStateSize = appState?.length || 0;
const collabSize = collab ? JSON.stringify(collab).length : 0; const collabSize = collab?.length || 0;
const librarySize = library ? JSON.stringify(library).length : 0; const librarySize = library?.length || 0;
return appStateSize + collabSize + librarySize + getElementsStorageSize(); return appStateSize + collabSize + librarySize + getElementsStorageSize();
} catch (error) { } catch (error) {

View File

@ -3,7 +3,6 @@ import React, {
useCallback, useCallback,
useContext, useContext,
useEffect, useEffect,
useLayoutEffect,
useRef, useRef,
useState, useState,
} from "react"; } from "react";
@ -12,7 +11,13 @@ import { getDefaultAppState } from "../appState";
import { ExcalidrawImperativeAPI } from "../components/App"; import { ExcalidrawImperativeAPI } from "../components/App";
import { ErrorDialog } from "../components/ErrorDialog"; import { ErrorDialog } from "../components/ErrorDialog";
import { TopErrorBoundary } from "../components/TopErrorBoundary"; import { TopErrorBoundary } from "../components/TopErrorBoundary";
import { APP_NAME, EVENT, TITLE_TIMEOUT, VERSION_TIMEOUT } from "../constants"; import {
APP_NAME,
EVENT,
TITLE_TIMEOUT,
URL_HASH_KEYS,
VERSION_TIMEOUT,
} from "../constants";
import { loadFromBlob } from "../data/blob"; import { loadFromBlob } from "../data/blob";
import { DataState, ImportedDataState } from "../data/types"; import { DataState, ImportedDataState } from "../data/types";
import { import {
@ -44,6 +49,7 @@ import {
importFromLocalStorage, importFromLocalStorage,
saveToLocalStorage, saveToLocalStorage,
} from "./data/localStorage"; } from "./data/localStorage";
import CustomStats from "./CustomStats";
const languageDetector = new LanguageDetector(); const languageDetector = new LanguageDetector();
languageDetector.init({ languageDetector.init({
@ -155,31 +161,11 @@ const initializeScene = async (opts: {
return null; return null;
}; };
function ExcalidrawWrapper() { const ExcalidrawWrapper = () => {
// dimensions
// ---------------------------------------------------------------------------
const [dimensions, setDimensions] = useState({
width: window.innerWidth,
height: window.innerHeight,
});
const [errorMessage, setErrorMessage] = useState(""); const [errorMessage, setErrorMessage] = useState("");
const currentLangCode = languageDetector.detect() || defaultLang.code; const currentLangCode = languageDetector.detect() || defaultLang.code;
const [langCode, setLangCode] = useState(currentLangCode); const [langCode, setLangCode] = useState(currentLangCode);
useLayoutEffect(() => {
const onResize = () => {
setDimensions({
width: window.innerWidth,
height: window.innerHeight,
});
};
window.addEventListener("resize", onResize);
return () => window.removeEventListener("resize", onResize);
}, []);
// initial state // initial state
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@ -213,12 +199,24 @@ function ExcalidrawWrapper() {
initialStatePromiseRef.current.promise.resolve(scene); initialStatePromiseRef.current.promise.resolve(scene);
}); });
const onHashChange = (_: HashChangeEvent) => { const onHashChange = (event: HashChangeEvent) => {
initializeScene({ collabAPI }).then((scene) => { event.preventDefault();
if (scene) { const hash = new URLSearchParams(window.location.hash.slice(1));
excalidrawAPI.updateScene(scene); const libraryUrl = hash.get(URL_HASH_KEYS.addLibrary);
} if (libraryUrl) {
}); // If hash changed and it contains library url, import it and replace
// the url to its previous state (important in case of collaboration
// and similar).
// Using history API won't trigger another hashchange.
window.history.replaceState({}, "", event.oldURL);
excalidrawAPI.importLibrary(libraryUrl, hash.get("token"));
} else {
initializeScene({ collabAPI }).then((scene) => {
if (scene) {
excalidrawAPI.updateScene(scene);
}
});
}
}; };
const titleTimeout = setTimeout( const titleTimeout = setTimeout(
@ -305,13 +303,19 @@ function ExcalidrawWrapper() {
[langCode], [langCode],
); );
const renderCustomStats = () => {
return (
<CustomStats
setToastMessage={(message) => excalidrawAPI!.setToastMessage(message)}
/>
);
};
return ( return (
<> <>
<Excalidraw <Excalidraw
ref={excalidrawRefCallback} ref={excalidrawRefCallback}
onChange={onChange} onChange={onChange}
width={dimensions.width}
height={dimensions.height}
initialData={initialStatePromiseRef.current.promise} initialData={initialStatePromiseRef.current.promise}
onCollabButtonClick={collabAPI?.onCollabButtonClick} onCollabButtonClick={collabAPI?.onCollabButtonClick}
isCollaborating={collabAPI?.isCollaborating()} isCollaborating={collabAPI?.isCollaborating()}
@ -319,6 +323,7 @@ function ExcalidrawWrapper() {
onExportToBackend={onExportToBackend} onExportToBackend={onExportToBackend}
renderFooter={renderFooter} renderFooter={renderFooter}
langCode={langCode} langCode={langCode}
renderCustomStats={renderCustomStats}
/> />
{excalidrawAPI && <CollabWrapper excalidrawAPI={excalidrawAPI} />} {excalidrawAPI && <CollabWrapper excalidrawAPI={excalidrawAPI} />}
{errorMessage && ( {errorMessage && (
@ -329,9 +334,9 @@ function ExcalidrawWrapper() {
)} )}
</> </>
); );
} };
export default function ExcalidrawApp() { const ExcalidrawApp = () => {
return ( return (
<TopErrorBoundary> <TopErrorBoundary>
<CollabContextConsumer> <CollabContextConsumer>
@ -339,4 +344,6 @@ export default function ExcalidrawApp() {
</CollabContextConsumer> </CollabContextConsumer>
</TopErrorBoundary> </TopErrorBoundary>
); );
} };
export default ExcalidrawApp;

2
src/global.d.ts vendored
View File

@ -89,3 +89,5 @@ interface Blob {
handle?: import("browser-fs-acces").FileSystemHandle; handle?: import("browser-fs-acces").FileSystemHandle;
name?: string; name?: string;
} }
declare module "*.scss";

View File

@ -63,6 +63,8 @@ const canvas = exportToCanvas(
...getDefaultAppState(), ...getDefaultAppState(),
offsetTop: 0, offsetTop: 0,
offsetLeft: 0, offsetLeft: 0,
width: 0,
height: 0,
}, },
{ {
exportBackground: true, exportBackground: true,

View File

@ -34,7 +34,4 @@ export const IsMobileProvider = ({
}; };
export const isMobile = () => getIsMobileMatcher().matches; export const isMobile = () => getIsMobileMatcher().matches;
export const useIsMobile = () => useContext(context);
export default function useIsMobile() {
return useContext(context);
}

View File

@ -61,7 +61,7 @@
"architect": "معماري", "architect": "معماري",
"artist": "رسام", "artist": "رسام",
"cartoonist": "كرتوني", "cartoonist": "كرتوني",
"fileTitle": "عنوان الملف", "fileTitle": "",
"colorPicker": "اختيار الألوان", "colorPicker": "اختيار الألوان",
"canvasBackground": "خلفية اللوحة", "canvasBackground": "خلفية اللوحة",
"drawingCanvas": "لوحة الرسم", "drawingCanvas": "لوحة الرسم",
@ -77,7 +77,7 @@
"group": "تحديد مجموعة", "group": "تحديد مجموعة",
"ungroup": "إلغاء تحديد مجموعة", "ungroup": "إلغاء تحديد مجموعة",
"collaborators": "المتعاونون", "collaborators": "المتعاونون",
"showGrid": "", "showGrid": "إظهار الشبكة",
"addToLibrary": "أضف إلى المكتبة", "addToLibrary": "أضف إلى المكتبة",
"removeFromLibrary": "حذف من المكتبة", "removeFromLibrary": "حذف من المكتبة",
"libraryLoadingMessage": "جارٍ تحميل المكتبة…", "libraryLoadingMessage": "جارٍ تحميل المكتبة…",
@ -92,9 +92,11 @@
"centerHorizontally": "توسيط أفقي", "centerHorizontally": "توسيط أفقي",
"distributeHorizontally": "التوزيع الأفقي", "distributeHorizontally": "التوزيع الأفقي",
"distributeVertically": "التوزيع عمودياً", "distributeVertically": "التوزيع عمودياً",
"viewMode": "", "flipHorizontal": "",
"flipVertical": "",
"viewMode": "نمط العرض",
"toggleExportColorScheme": "", "toggleExportColorScheme": "",
"share": "" "share": "مشاركة"
}, },
"buttons": { "buttons": {
"clearReset": "إعادة تعيين اللوحة", "clearReset": "إعادة تعيين اللوحة",
@ -119,7 +121,7 @@
"edit": "تعديل", "edit": "تعديل",
"undo": "تراجع", "undo": "تراجع",
"redo": "إعادة تنفيذ", "redo": "إعادة تنفيذ",
"resetLibrary": "", "resetLibrary": "إعادة ضبط المكتبة",
"createNewRoom": "إنشاء غرفة جديدة", "createNewRoom": "إنشاء غرفة جديدة",
"fullScreen": "شاشة كاملة", "fullScreen": "شاشة كاملة",
"darkMode": "الوضع المظلم", "darkMode": "الوضع المظلم",
@ -138,7 +140,7 @@
"decryptFailed": "تعذر فك تشفير البيانات.", "decryptFailed": "تعذر فك تشفير البيانات.",
"uploadedSecurly": "تم تأمين التحميل بتشفير النهاية إلى النهاية، مما يعني أن خادوم Excalidraw والأطراف الثالثة لا يمكنها قراءة المحتوى.", "uploadedSecurly": "تم تأمين التحميل بتشفير النهاية إلى النهاية، مما يعني أن خادوم Excalidraw والأطراف الثالثة لا يمكنها قراءة المحتوى.",
"loadSceneOverridePrompt": "تحميل الرسم الخارجي سيحل محل المحتوى الموجود لديك. هل ترغب في المتابعة؟", "loadSceneOverridePrompt": "تحميل الرسم الخارجي سيحل محل المحتوى الموجود لديك. هل ترغب في المتابعة؟",
"collabStopOverridePrompt": "", "collabStopOverridePrompt": "إيقاف الجلسة سيؤدي إلى الكتابة فوق رسومك السابقة المخزنة داخليا. هل أنت متأكد؟\n\n(إذا كنت ترغب في الاحتفاظ برسمك المخزن داخليا، ببساطة أغلق علامة تبويب المتصفح بدلاً من ذلك.)",
"errorLoadingLibrary": "حصل خطأ أثناء تحميل مكتبة الطرف الثالث.", "errorLoadingLibrary": "حصل خطأ أثناء تحميل مكتبة الطرف الثالث.",
"confirmAddLibrary": "هذا سيضيف {{numShapes}} شكل إلى مكتبتك. هل أنت متأكد؟", "confirmAddLibrary": "هذا سيضيف {{numShapes}} شكل إلى مكتبتك. هل أنت متأكد؟",
"imageDoesNotContainScene": "استيراد الصور غير مدعوم في الوقت الراهن.\n\nهل تريد استيراد مشهد؟ لا يبدو أن هذه الصورة تحتوي على أي بيانات مشهد. هل قمت بسماح هذا أثناء التصدير؟", "imageDoesNotContainScene": "استيراد الصور غير مدعوم في الوقت الراهن.\n\nهل تريد استيراد مشهد؟ لا يبدو أن هذه الصورة تحتوي على أي بيانات مشهد. هل قمت بسماح هذا أثناء التصدير؟",
@ -212,9 +214,9 @@
"curvedArrow": "", "curvedArrow": "",
"curvedLine": "", "curvedLine": "",
"documentation": "", "documentation": "",
"drag": "", "drag": "اسحب",
"editor": "", "editor": "المحرر",
"github": "", "github": "عثرت على مشكلة؟ إرسال",
"howto": "", "howto": "",
"or": "", "or": "",
"preventBinding": "", "preventBinding": "",
@ -228,7 +230,8 @@
"zoomToSelection": "" "zoomToSelection": ""
}, },
"encrypted": { "encrypted": {
"tooltip": "رسوماتك مشفرة من النهاية إلى النهاية حتى أن خوادم Excalidraw لن تراها أبدا." "tooltip": "رسوماتك مشفرة من النهاية إلى النهاية حتى أن خوادم Excalidraw لن تراها أبدا.",
"link": ""
}, },
"stats": { "stats": {
"angle": "الزاوية", "angle": "الزاوية",

View File

@ -61,7 +61,7 @@
"architect": "Архитект", "architect": "Архитект",
"artist": "Художник", "artist": "Художник",
"cartoonist": "Карикатурист", "cartoonist": "Карикатурист",
"fileTitle": "Заглавие на файл", "fileTitle": "",
"colorPicker": "Избор на цвят", "colorPicker": "Избор на цвят",
"canvasBackground": "Фон на платно", "canvasBackground": "Фон на платно",
"drawingCanvas": "Платно за рисуване", "drawingCanvas": "Платно за рисуване",
@ -92,6 +92,8 @@
"centerHorizontally": "Центрирай хоризонтално", "centerHorizontally": "Центрирай хоризонтално",
"distributeHorizontally": "Разпредели хоризонтално", "distributeHorizontally": "Разпредели хоризонтално",
"distributeVertically": "Разпредели вертикално", "distributeVertically": "Разпредели вертикално",
"flipHorizontal": "",
"flipVertical": "",
"viewMode": "Изглед", "viewMode": "Изглед",
"toggleExportColorScheme": "", "toggleExportColorScheme": "",
"share": "" "share": ""
@ -228,7 +230,8 @@
"zoomToSelection": "Приближи селекцията" "zoomToSelection": "Приближи селекцията"
}, },
"encrypted": { "encrypted": {
"tooltip": "Вашите рисунки са криптирани от край до край, така че сървърите на Excalidraw няма да могат да ги виждат." "tooltip": "Вашите рисунки са криптирани от край до край, така че сървърите на Excalidraw няма да могат да ги виждат.",
"link": ""
}, },
"stats": { "stats": {
"angle": "Ъгъл", "angle": "Ъгъл",

View File

@ -61,7 +61,7 @@
"architect": "Arquitecte", "architect": "Arquitecte",
"artist": "Artista", "artist": "Artista",
"cartoonist": "Dibuixant", "cartoonist": "Dibuixant",
"fileTitle": "Títol del fitxer", "fileTitle": "",
"colorPicker": "Selector de colors", "colorPicker": "Selector de colors",
"canvasBackground": "Fons del llenç", "canvasBackground": "Fons del llenç",
"drawingCanvas": "Llenç de dibuix", "drawingCanvas": "Llenç de dibuix",
@ -92,6 +92,8 @@
"centerHorizontally": "Centrar horitzontalment", "centerHorizontally": "Centrar horitzontalment",
"distributeHorizontally": "Distribuir horitzontalment", "distributeHorizontally": "Distribuir horitzontalment",
"distributeVertically": "Distribuir verticalment", "distributeVertically": "Distribuir verticalment",
"flipHorizontal": "",
"flipVertical": "",
"viewMode": "Mode de visualització", "viewMode": "Mode de visualització",
"toggleExportColorScheme": "Canvia l'esquema de colors de l'exportació", "toggleExportColorScheme": "Canvia l'esquema de colors de l'exportació",
"share": "Compartir" "share": "Compartir"
@ -228,7 +230,8 @@
"zoomToSelection": "Zoom per veure la selecció" "zoomToSelection": "Zoom per veure la selecció"
}, },
"encrypted": { "encrypted": {
"tooltip": "Els vostres dibuixos estan xifrats de punta a punta de manera que els servidors dExcalidraw no els veuran mai." "tooltip": "Els vostres dibuixos estan xifrats de punta a punta de manera que els servidors dExcalidraw no els veuran mai.",
"link": ""
}, },
"stats": { "stats": {
"angle": "Angle", "angle": "Angle",

View File

@ -92,6 +92,8 @@
"centerHorizontally": "Horizontal zentrieren", "centerHorizontally": "Horizontal zentrieren",
"distributeHorizontally": "Horizontal verteilen", "distributeHorizontally": "Horizontal verteilen",
"distributeVertically": "Vertikal verteilen", "distributeVertically": "Vertikal verteilen",
"flipHorizontal": "Horizontal spiegeln",
"flipVertical": "Vertikal spiegeln",
"viewMode": "Ansichtsmodus", "viewMode": "Ansichtsmodus",
"toggleExportColorScheme": "Farbschema für Export umschalten", "toggleExportColorScheme": "Farbschema für Export umschalten",
"share": "Teilen" "share": "Teilen"
@ -228,7 +230,8 @@
"zoomToSelection": "Auf Auswahl zoomen" "zoomToSelection": "Auf Auswahl zoomen"
}, },
"encrypted": { "encrypted": {
"tooltip": "Da deine Zeichnungen Ende-zu-Ende verschlüsselt werden, sehen auch unsere Excalidraw-Server sie niemals." "tooltip": "Da deine Zeichnungen Ende-zu-Ende verschlüsselt werden, sehen auch unsere Excalidraw-Server sie niemals.",
"link": "Blogbeitrag über Ende-zu-Ende-Verschlüsselung in Excalidraw"
}, },
"stats": { "stats": {
"angle": "Winkel", "angle": "Winkel",

View File

@ -61,7 +61,7 @@
"architect": "Αρχιτέκτονας", "architect": "Αρχιτέκτονας",
"artist": "Καλλιτέχνης", "artist": "Καλλιτέχνης",
"cartoonist": "Σκιτσογράφος", "cartoonist": "Σκιτσογράφος",
"fileTitle": "Τίτλος αρχείου", "fileTitle": "Όνομα αρχείου",
"colorPicker": "Επιλογή Χρώματος", "colorPicker": "Επιλογή Χρώματος",
"canvasBackground": "Φόντο καμβά", "canvasBackground": "Φόντο καμβά",
"drawingCanvas": "Σχεδίαση καμβά", "drawingCanvas": "Σχεδίαση καμβά",
@ -92,9 +92,11 @@
"centerHorizontally": "Κέντρο οριζόντια", "centerHorizontally": "Κέντρο οριζόντια",
"distributeHorizontally": "Οριζόντια κατανομή", "distributeHorizontally": "Οριζόντια κατανομή",
"distributeVertically": "Κατακόρυφη κατανομή", "distributeVertically": "Κατακόρυφη κατανομή",
"flipHorizontal": "Οριζόντια αναστροφή",
"flipVertical": "Κατακόρυφη αναστροφή",
"viewMode": "Λειτουργία προβολής", "viewMode": "Λειτουργία προβολής",
"toggleExportColorScheme": "Εναλλαγή εξαγωγής θέματος χρωμάτων", "toggleExportColorScheme": "Εναλλαγή εξαγωγής θέματος χρωμάτων",
"share": "" "share": "Κοινοποίηση"
}, },
"buttons": { "buttons": {
"clearReset": "Επαναφορά του καμβά", "clearReset": "Επαναφορά του καμβά",
@ -228,7 +230,8 @@
"zoomToSelection": "Ζουμ στην επιλογή" "zoomToSelection": "Ζουμ στην επιλογή"
}, },
"encrypted": { "encrypted": {
"tooltip": "Τα σχέδιά σου είναι κρυπτογραφημένα από άκρο σε άκρο, έτσι δεν θα είναι ποτέ ορατά μέσα από τους διακομιστές του Excalidraw." "tooltip": "Τα σχέδιά σου είναι κρυπτογραφημένα από άκρο σε άκρο, έτσι δεν θα είναι ποτέ ορατά μέσα από τους διακομιστές του Excalidraw.",
"link": ""
}, },
"stats": { "stats": {
"angle": "Γωνία", "angle": "Γωνία",

View File

@ -94,6 +94,8 @@
"centerHorizontally": "Center horizontally", "centerHorizontally": "Center horizontally",
"distributeHorizontally": "Distribute horizontally", "distributeHorizontally": "Distribute horizontally",
"distributeVertically": "Distribute vertically", "distributeVertically": "Distribute vertically",
"flipHorizontal": "Flip horizontal",
"flipVertical": "Flip vertical",
"viewMode": "View mode", "viewMode": "View mode",
"toggleExportColorScheme": "Toggle export color scheme", "toggleExportColorScheme": "Toggle export color scheme",
"share": "Share" "share": "Share"
@ -230,7 +232,8 @@
"zoomToSelection": "Zoom to selection" "zoomToSelection": "Zoom to selection"
}, },
"encrypted": { "encrypted": {
"tooltip": "Your drawings are end-to-end encrypted so Excalidraw's servers will never see them." "tooltip": "Your drawings are end-to-end encrypted so Excalidraw's servers will never see them.",
"link": "Blog post on end-to-end encryption in Excalidraw"
}, },
"stats": { "stats": {
"angle": "Angle", "angle": "Angle",

View File

@ -61,7 +61,7 @@
"architect": "Arquitecto", "architect": "Arquitecto",
"artist": "Artista", "artist": "Artista",
"cartoonist": "Caricatura", "cartoonist": "Caricatura",
"fileTitle": "Título del archivo", "fileTitle": "Nombre del archivo",
"colorPicker": "Selector de color", "colorPicker": "Selector de color",
"canvasBackground": "Fondo del lienzo", "canvasBackground": "Fondo del lienzo",
"drawingCanvas": "Lienzo de dibujo", "drawingCanvas": "Lienzo de dibujo",
@ -92,6 +92,8 @@
"centerHorizontally": "Centrar horizontalmente", "centerHorizontally": "Centrar horizontalmente",
"distributeHorizontally": "Distribuir horizontalmente", "distributeHorizontally": "Distribuir horizontalmente",
"distributeVertically": "Distribuir verticalmente", "distributeVertically": "Distribuir verticalmente",
"flipHorizontal": "Girar horizontalmente",
"flipVertical": "Girar verticalmente",
"viewMode": "Modo presentación", "viewMode": "Modo presentación",
"toggleExportColorScheme": "Cambiar el esquema de colores de exportación", "toggleExportColorScheme": "Cambiar el esquema de colores de exportación",
"share": "Compartir" "share": "Compartir"
@ -228,7 +230,8 @@
"zoomToSelection": "Hacer zoom a la selección" "zoomToSelection": "Hacer zoom a la selección"
}, },
"encrypted": { "encrypted": {
"tooltip": "Tus dibujos están cifrados de punto a punto, por lo que los servidores de Excalidraw nunca los verán." "tooltip": "Tus dibujos están cifrados de punto a punto, por lo que los servidores de Excalidraw nunca los verán.",
"link": ""
}, },
"stats": { "stats": {
"angle": "Ángulo", "angle": "Ángulo",

View File

@ -61,7 +61,7 @@
"architect": "معمار", "architect": "معمار",
"artist": "هنرمند", "artist": "هنرمند",
"cartoonist": "کارتونیست", "cartoonist": "کارتونیست",
"fileTitle": "عنوان فایل", "fileTitle": "",
"colorPicker": "انتخابگر رنگ", "colorPicker": "انتخابگر رنگ",
"canvasBackground": "بوم", "canvasBackground": "بوم",
"drawingCanvas": "بوم نقاشی", "drawingCanvas": "بوم نقاشی",
@ -92,6 +92,8 @@
"centerHorizontally": "وسط قرار دادن به صورت افقی", "centerHorizontally": "وسط قرار دادن به صورت افقی",
"distributeHorizontally": "توزیع کردن به صورت افقی", "distributeHorizontally": "توزیع کردن به صورت افقی",
"distributeVertically": "توزیع کردن به صورت عمودی", "distributeVertically": "توزیع کردن به صورت عمودی",
"flipHorizontal": "",
"flipVertical": "",
"viewMode": "", "viewMode": "",
"toggleExportColorScheme": "", "toggleExportColorScheme": "",
"share": "" "share": ""
@ -228,7 +230,8 @@
"zoomToSelection": "بزرگنمایی قسمت انتخاب شده" "zoomToSelection": "بزرگنمایی قسمت انتخاب شده"
}, },
"encrypted": { "encrypted": {
"tooltip": "شما در یک محیط رمزگزاری شده دو طرفه در حال طراحی هستید پس Excalidraw هرگز طرح های شما را نمیبند." "tooltip": "شما در یک محیط رمزگزاری شده دو طرفه در حال طراحی هستید پس Excalidraw هرگز طرح های شما را نمیبند.",
"link": ""
}, },
"stats": { "stats": {
"angle": "زاویه", "angle": "زاویه",

View File

@ -61,7 +61,7 @@
"architect": "Arkkitehti", "architect": "Arkkitehti",
"artist": "Taiteilija", "artist": "Taiteilija",
"cartoonist": "Sarjakuva", "cartoonist": "Sarjakuva",
"fileTitle": "Tiedoston otsikko", "fileTitle": "Tiedostonimi",
"colorPicker": "Värin valinta", "colorPicker": "Värin valinta",
"canvasBackground": "Piirtoalueen tausta", "canvasBackground": "Piirtoalueen tausta",
"drawingCanvas": "Piirtoalue", "drawingCanvas": "Piirtoalue",
@ -92,6 +92,8 @@
"centerHorizontally": "Keskitä vaakasuunnassa", "centerHorizontally": "Keskitä vaakasuunnassa",
"distributeHorizontally": "Jaa vaakasuunnassa", "distributeHorizontally": "Jaa vaakasuunnassa",
"distributeVertically": "Jaa pystysuunnassa", "distributeVertically": "Jaa pystysuunnassa",
"flipHorizontal": "Käännä vaakasuunnassa",
"flipVertical": "Käännä pystysuunnassa",
"viewMode": "Katselutila", "viewMode": "Katselutila",
"toggleExportColorScheme": "Vaihda viennin väriteema", "toggleExportColorScheme": "Vaihda viennin väriteema",
"share": "Jaa" "share": "Jaa"
@ -228,7 +230,8 @@
"zoomToSelection": "Näytä valinta" "zoomToSelection": "Näytä valinta"
}, },
"encrypted": { "encrypted": {
"tooltip": "Piirroksesi ovat päästä-päähän-salattuja, joten Excalidrawin palvelimet eivät koskaan näe niitä." "tooltip": "Piirroksesi ovat päästä-päähän-salattuja, joten Excalidrawin palvelimet eivät koskaan näe niitä.",
"link": ""
}, },
"stats": { "stats": {
"angle": "Kulma", "angle": "Kulma",

View File

@ -61,7 +61,7 @@
"architect": "Architecte", "architect": "Architecte",
"artist": "Artiste", "artist": "Artiste",
"cartoonist": "Caricaturiste", "cartoonist": "Caricaturiste",
"fileTitle": "Titre du fichier", "fileTitle": "Nom du fichier",
"colorPicker": "Sélecteur de couleur", "colorPicker": "Sélecteur de couleur",
"canvasBackground": "Arrière-plan du canevas", "canvasBackground": "Arrière-plan du canevas",
"drawingCanvas": "Zone de dessin", "drawingCanvas": "Zone de dessin",
@ -92,6 +92,8 @@
"centerHorizontally": "Centrer horizontalement", "centerHorizontally": "Centrer horizontalement",
"distributeHorizontally": "Distribuer horizontalement", "distributeHorizontally": "Distribuer horizontalement",
"distributeVertically": "Distribuer verticalement", "distributeVertically": "Distribuer verticalement",
"flipHorizontal": "Retourner horizontalement",
"flipVertical": "Retourner verticalement",
"viewMode": "Mode présentation", "viewMode": "Mode présentation",
"toggleExportColorScheme": "Activer/Désactiver l'export du thème de couleur", "toggleExportColorScheme": "Activer/Désactiver l'export du thème de couleur",
"share": "Partager" "share": "Partager"
@ -228,7 +230,8 @@
"zoomToSelection": "Zoomer sur la sélection" "zoomToSelection": "Zoomer sur la sélection"
}, },
"encrypted": { "encrypted": {
"tooltip": "Vos dessins sont chiffrés de bout en bout, les serveurs d'Excalidraw ne les verront jamais." "tooltip": "Vos dessins sont chiffrés de bout en bout, les serveurs d'Excalidraw ne les verront jamais.",
"link": "Article de blog sur le chiffrement de bout en bout dans Excalidraw"
}, },
"stats": { "stats": {
"angle": "Angle", "angle": "Angle",

View File

@ -61,7 +61,7 @@
"architect": "ארכיטקט", "architect": "ארכיטקט",
"artist": "אמן", "artist": "אמן",
"cartoonist": "קריקטוריסט", "cartoonist": "קריקטוריסט",
"fileTitle": "כותרת הקובץ", "fileTitle": "",
"colorPicker": "בחירת צבע", "colorPicker": "בחירת צבע",
"canvasBackground": "רקע הלוח", "canvasBackground": "רקע הלוח",
"drawingCanvas": "לוח ציור", "drawingCanvas": "לוח ציור",
@ -92,6 +92,8 @@
"centerHorizontally": "מרכז אופקית", "centerHorizontally": "מרכז אופקית",
"distributeHorizontally": "חלוקה אופקית", "distributeHorizontally": "חלוקה אופקית",
"distributeVertically": "חלוקה אנכית", "distributeVertically": "חלוקה אנכית",
"flipHorizontal": "",
"flipVertical": "",
"viewMode": "מצב תצוגה", "viewMode": "מצב תצוגה",
"toggleExportColorScheme": "", "toggleExportColorScheme": "",
"share": "" "share": ""
@ -228,7 +230,8 @@
"zoomToSelection": "התמקד בבחירה" "zoomToSelection": "התמקד בבחירה"
}, },
"encrypted": { "encrypted": {
"tooltip": "הרישומים שלך מוצפנים מקצה לקצה כך שהשרתים של Excalidraw לא יראו אותם לעולם." "tooltip": "הרישומים שלך מוצפנים מקצה לקצה כך שהשרתים של Excalidraw לא יראו אותם לעולם.",
"link": ""
}, },
"stats": { "stats": {
"angle": "זווית", "angle": "זווית",

View File

@ -61,7 +61,7 @@
"architect": "वास्तुकार", "architect": "वास्तुकार",
"artist": "कलाकार", "artist": "कलाकार",
"cartoonist": "व्यंग्य चित्रकार", "cartoonist": "व्यंग्य चित्रकार",
"fileTitle": "फ़ाइल का शीर्षक", "fileTitle": "फ़ाइल का नाम",
"colorPicker": "रंग चयन", "colorPicker": "रंग चयन",
"canvasBackground": "कैनवास बैकग्राउंड", "canvasBackground": "कैनवास बैकग्राउंड",
"drawingCanvas": "कैनवास बना रहे हैं", "drawingCanvas": "कैनवास बना रहे हैं",
@ -92,6 +92,8 @@
"centerHorizontally": "क्षैतिज केन्द्रित", "centerHorizontally": "क्षैतिज केन्द्रित",
"distributeHorizontally": "क्षैतिज रूप से वितरित करें", "distributeHorizontally": "क्षैतिज रूप से वितरित करें",
"distributeVertically": "खड़ी रूप से वितरित करें", "distributeVertically": "खड़ी रूप से वितरित करें",
"flipHorizontal": "",
"flipVertical": "",
"viewMode": "", "viewMode": "",
"toggleExportColorScheme": "", "toggleExportColorScheme": "",
"share": "" "share": ""
@ -228,7 +230,8 @@
"zoomToSelection": "चयन तक ज़ूम करे" "zoomToSelection": "चयन तक ज़ूम करे"
}, },
"encrypted": { "encrypted": {
"tooltip": "आपके चित्र अंत-से-अंत एन्क्रिप्टेड हैं, इसलिए एक्सक्लूसिव्रॉव के सर्वर उन्हें कभी नहीं देखेंगे।" "tooltip": "आपके चित्र अंत-से-अंत एन्क्रिप्टेड हैं, इसलिए एक्सक्लूसिव्रॉव के सर्वर उन्हें कभी नहीं देखेंगे।",
"link": ""
}, },
"stats": { "stats": {
"angle": "कोण", "angle": "कोण",

View File

@ -61,7 +61,7 @@
"architect": "Tervezői", "architect": "Tervezői",
"artist": "Művészi", "artist": "Művészi",
"cartoonist": "Karikatúrás", "cartoonist": "Karikatúrás",
"fileTitle": "Fájl címe", "fileTitle": "",
"colorPicker": "Színválasztó", "colorPicker": "Színválasztó",
"canvasBackground": "Vászon háttérszíne", "canvasBackground": "Vászon háttérszíne",
"drawingCanvas": "Rajzvászon", "drawingCanvas": "Rajzvászon",
@ -92,6 +92,8 @@
"centerHorizontally": "Vízszintesen középre igazított", "centerHorizontally": "Vízszintesen középre igazított",
"distributeHorizontally": "Vízszintes elosztás", "distributeHorizontally": "Vízszintes elosztás",
"distributeVertically": "Függőleges elosztás", "distributeVertically": "Függőleges elosztás",
"flipHorizontal": "",
"flipVertical": "",
"viewMode": "", "viewMode": "",
"toggleExportColorScheme": "", "toggleExportColorScheme": "",
"share": "" "share": ""
@ -228,7 +230,8 @@
"zoomToSelection": "" "zoomToSelection": ""
}, },
"encrypted": { "encrypted": {
"tooltip": "A rajzaidat végpontok közötti titkosítással tároljuk, tehát az Excalidraw szervereiről se tud más belenézni." "tooltip": "A rajzaidat végpontok közötti titkosítással tároljuk, tehát az Excalidraw szervereiről se tud más belenézni.",
"link": ""
}, },
"stats": { "stats": {
"angle": "Szög", "angle": "Szög",

View File

@ -61,7 +61,7 @@
"architect": "Arsitek", "architect": "Arsitek",
"artist": "Artis", "artist": "Artis",
"cartoonist": "Kartunis", "cartoonist": "Kartunis",
"fileTitle": "Judul file", "fileTitle": "Nama file",
"colorPicker": "Pilihan Warna", "colorPicker": "Pilihan Warna",
"canvasBackground": "Latar Kanvas", "canvasBackground": "Latar Kanvas",
"drawingCanvas": "Kanvas", "drawingCanvas": "Kanvas",
@ -92,6 +92,8 @@
"centerHorizontally": "Pusatkan secara horizontal", "centerHorizontally": "Pusatkan secara horizontal",
"distributeHorizontally": "Distribusikan horizontal", "distributeHorizontally": "Distribusikan horizontal",
"distributeVertically": "Distribusikan vertikal", "distributeVertically": "Distribusikan vertikal",
"flipHorizontal": "Balikkan horizontal",
"flipVertical": "Balikkan vertikal",
"viewMode": "Mode tampilan", "viewMode": "Mode tampilan",
"toggleExportColorScheme": "Ubah skema warna ekspor", "toggleExportColorScheme": "Ubah skema warna ekspor",
"share": "Bagikan" "share": "Bagikan"
@ -143,7 +145,7 @@
"confirmAddLibrary": "Ini akan menambahkan {{numShapes}} bentuk ke pustaka Anda. Anda yakin?", "confirmAddLibrary": "Ini akan menambahkan {{numShapes}} bentuk ke pustaka Anda. Anda yakin?",
"imageDoesNotContainScene": "Mengimpor gambar tidak didukung saat ini.\n\nApakah Anda ingin impor pemandangan? Gambar ini tidak berisi data pemandangan. Sudah ka Anda aktifkan ini ketika ekspor?", "imageDoesNotContainScene": "Mengimpor gambar tidak didukung saat ini.\n\nApakah Anda ingin impor pemandangan? Gambar ini tidak berisi data pemandangan. Sudah ka Anda aktifkan ini ketika ekspor?",
"cannotRestoreFromImage": "Pemandangan tidak dapat dipulihkan dari file gambar ini", "cannotRestoreFromImage": "Pemandangan tidak dapat dipulihkan dari file gambar ini",
"invalidSceneUrl": "", "invalidSceneUrl": "Tidak dapat impor pemandangan dari URL. Kemungkinan URL itu rusak atau tidak berisi data JSON Excalidraw yang valid.",
"resetLibrary": "Ini akan menghapus pustaka Anda. Anda yakin?" "resetLibrary": "Ini akan menghapus pustaka Anda. Anda yakin?"
}, },
"toolBar": { "toolBar": {
@ -228,7 +230,8 @@
"zoomToSelection": "Perbesar ke seleksi" "zoomToSelection": "Perbesar ke seleksi"
}, },
"encrypted": { "encrypted": {
"tooltip": "Gambar anda terenkripsi end-to-end sehingga server Excalidraw tidak akan pernah dapat melihatnya." "tooltip": "Gambar anda terenkripsi end-to-end sehingga server Excalidraw tidak akan pernah dapat melihatnya.",
"link": "Pos blog tentang enkripsi ujung ke ujung di Excalidraw"
}, },
"stats": { "stats": {
"angle": "Sudut", "angle": "Sudut",

View File

@ -61,7 +61,7 @@
"architect": "Architetto", "architect": "Architetto",
"artist": "Artista", "artist": "Artista",
"cartoonist": "Fumettista", "cartoonist": "Fumettista",
"fileTitle": "Titolo del file", "fileTitle": "Nome del file",
"colorPicker": "Selettore colore", "colorPicker": "Selettore colore",
"canvasBackground": "Sfondo tela", "canvasBackground": "Sfondo tela",
"drawingCanvas": "Area di disegno", "drawingCanvas": "Area di disegno",
@ -92,6 +92,8 @@
"centerHorizontally": "Centra orizzontalmente", "centerHorizontally": "Centra orizzontalmente",
"distributeHorizontally": "Distribuisci orizzontalmente", "distributeHorizontally": "Distribuisci orizzontalmente",
"distributeVertically": "Distribuisci verticalmente", "distributeVertically": "Distribuisci verticalmente",
"flipHorizontal": "Capovolgi orizzontalmente",
"flipVertical": "Capovolgi verticalmente",
"viewMode": "Modalità visualizzazione", "viewMode": "Modalità visualizzazione",
"toggleExportColorScheme": "Cambia lo schema di colori in esportazione", "toggleExportColorScheme": "Cambia lo schema di colori in esportazione",
"share": "Condividi" "share": "Condividi"
@ -228,7 +230,8 @@
"zoomToSelection": "Zoom alla selezione" "zoomToSelection": "Zoom alla selezione"
}, },
"encrypted": { "encrypted": {
"tooltip": "I tuoi disegni sono crittografati end-to-end in modo che i server di Excalidraw non li possano mai vedere." "tooltip": "I tuoi disegni sono crittografati end-to-end in modo che i server di Excalidraw non li possano mai vedere.",
"link": "Articolo del blog sulla crittografia end-to-end di Excalidraw"
}, },
"stats": { "stats": {
"angle": "Angolo", "angle": "Angolo",

View File

@ -92,6 +92,8 @@
"centerHorizontally": "横方向に中央揃え", "centerHorizontally": "横方向に中央揃え",
"distributeHorizontally": "水平方向に分散配置", "distributeHorizontally": "水平方向に分散配置",
"distributeVertically": "垂直方向に分散配置", "distributeVertically": "垂直方向に分散配置",
"flipHorizontal": "水平方向に反転",
"flipVertical": "垂直方向に反転",
"viewMode": "閲覧モード", "viewMode": "閲覧モード",
"toggleExportColorScheme": "", "toggleExportColorScheme": "",
"share": "" "share": ""
@ -201,7 +203,7 @@
"desc_inProgressIntro": "共同編集セッションが有効になっています。", "desc_inProgressIntro": "共同編集セッションが有効になっています。",
"desc_shareLink": "下記URLを共同編集したい人に共有してください", "desc_shareLink": "下記URLを共同編集したい人に共有してください",
"desc_exitSession": "セッションを終了すると部屋から切断されますが、手元の環境で編集を続けることができます。変更内容は他の人には反映されません。", "desc_exitSession": "セッションを終了すると部屋から切断されますが、手元の環境で編集を続けることができます。変更内容は他の人には反映されません。",
"shareTitle": "" "shareTitle": "Excalidrawのライブコラボレーションセッションに参加する"
}, },
"errorDialog": { "errorDialog": {
"title": "エラー" "title": "エラー"
@ -228,7 +230,8 @@
"zoomToSelection": "選択要素にズーム" "zoomToSelection": "選択要素にズーム"
}, },
"encrypted": { "encrypted": {
"tooltip": "描画内容はエンドツーエンド暗号化が施されており、Excalidrawサーバーが内容を見ることはできません。" "tooltip": "描画内容はエンドツーエンド暗号化が施されており、Excalidrawサーバーが内容を見ることはできません。",
"link": "Excalidrawのエンドツーエンド暗号化に関するブログ記事"
}, },
"stats": { "stats": {
"angle": "角度", "angle": "角度",
@ -251,7 +254,7 @@
"copyToClipboardAsPng": "", "copyToClipboardAsPng": "",
"fileSaved": "ファイルを保存しました", "fileSaved": "ファイルを保存しました",
"fileSavedToFilename": "{filename} に保存しました", "fileSavedToFilename": "{filename} に保存しました",
"canvas": "", "canvas": "キャンバス",
"selection": "" "selection": ""
} }
} }

View File

@ -61,7 +61,7 @@
"architect": "Amasdag", "architect": "Amasdag",
"artist": "Anaẓur", "artist": "Anaẓur",
"cartoonist": "", "cartoonist": "",
"fileTitle": "Azwel n ufaylu", "fileTitle": "Isem n ufaylu",
"colorPicker": "Amafran n yini", "colorPicker": "Amafran n yini",
"canvasBackground": "Agilal n teɣzut n usuneɣ", "canvasBackground": "Agilal n teɣzut n usuneɣ",
"drawingCanvas": "Taɣzut n usuneɣ", "drawingCanvas": "Taɣzut n usuneɣ",
@ -92,6 +92,8 @@
"centerHorizontally": "Di tlemmast s uglawi", "centerHorizontally": "Di tlemmast s uglawi",
"distributeHorizontally": "Freq s uglawi", "distributeHorizontally": "Freq s uglawi",
"distributeVertically": "Freq s yibeddi", "distributeVertically": "Freq s yibeddi",
"flipHorizontal": "Tuttya taglawant",
"flipVertical": "Tuttya tubdidt",
"viewMode": "Askar n tmuɣli", "viewMode": "Askar n tmuɣli",
"toggleExportColorScheme": "Sermed/sens asifeḍ usentel n yini", "toggleExportColorScheme": "Sermed/sens asifeḍ usentel n yini",
"share": "Bḍu" "share": "Bḍu"
@ -228,7 +230,8 @@
"zoomToSelection": "Simɣur ɣer tefrayt" "zoomToSelection": "Simɣur ɣer tefrayt"
}, },
"encrypted": { "encrypted": {
"tooltip": "Unuɣen-inek (m) ttuwgelhnen seg yixef s ixef dɣa iqeddacen n Excalidraw werǧin ad ten-walin. " "tooltip": "Unuɣen-inek (m) ttuwgelhnen seg yixef s ixef dɣa iqeddacen n Excalidraw werǧin ad ten-walin. ",
"link": "Amagrad ɣef uwgelhen ixef s ixef di Excalidraw"
}, },
"stats": { "stats": {
"angle": "Tiɣmeṛt", "angle": "Tiɣmeṛt",

View File

@ -61,7 +61,7 @@
"architect": "건축가", "architect": "건축가",
"artist": "예술가", "artist": "예술가",
"cartoonist": "만화가", "cartoonist": "만화가",
"fileTitle": "파일명", "fileTitle": "",
"colorPicker": "색상 선택기", "colorPicker": "색상 선택기",
"canvasBackground": "캔버스 배경", "canvasBackground": "캔버스 배경",
"drawingCanvas": "캔버스 그리기", "drawingCanvas": "캔버스 그리기",
@ -92,6 +92,8 @@
"centerHorizontally": "수평으로 중앙 정렬", "centerHorizontally": "수평으로 중앙 정렬",
"distributeHorizontally": "수평으로 분배", "distributeHorizontally": "수평으로 분배",
"distributeVertically": "수직으로 분배", "distributeVertically": "수직으로 분배",
"flipHorizontal": "",
"flipVertical": "",
"viewMode": "보기 모드", "viewMode": "보기 모드",
"toggleExportColorScheme": "", "toggleExportColorScheme": "",
"share": "" "share": ""
@ -228,7 +230,8 @@
"zoomToSelection": "선택 영역으로 확대/축소" "zoomToSelection": "선택 영역으로 확대/축소"
}, },
"encrypted": { "encrypted": {
"tooltip": "그림은 종단 간 암호화되므로 Excalidraw의 서버는 절대로 내용을 알 수 없습니다." "tooltip": "그림은 종단 간 암호화되므로 Excalidraw의 서버는 절대로 내용을 알 수 없습니다.",
"link": ""
}, },
"stats": { "stats": {
"angle": "각도", "angle": "각도",

View File

@ -61,7 +61,7 @@
"architect": "ဗိသုကာ", "architect": "ဗိသုကာ",
"artist": "ပန်းချီ", "artist": "ပန်းချီ",
"cartoonist": "ကာတွန်း", "cartoonist": "ကာတွန်း",
"fileTitle": "ခေါင်းစဉ်", "fileTitle": "",
"colorPicker": "အရောင်ရွေး", "colorPicker": "အရောင်ရွေး",
"canvasBackground": "ကားချပ်နောက်ခံ", "canvasBackground": "ကားချပ်နောက်ခံ",
"drawingCanvas": "ပုံဆွဲကားချပ်", "drawingCanvas": "ပုံဆွဲကားချပ်",
@ -92,6 +92,8 @@
"centerHorizontally": "အလျားလိုက်အလယ်ညှိ", "centerHorizontally": "အလျားလိုက်အလယ်ညှိ",
"distributeHorizontally": "အလျားလိုက်", "distributeHorizontally": "အလျားလိုက်",
"distributeVertically": "ထောင်လိုက်", "distributeVertically": "ထောင်လိုက်",
"flipHorizontal": "",
"flipVertical": "",
"viewMode": "", "viewMode": "",
"toggleExportColorScheme": "", "toggleExportColorScheme": "",
"share": "" "share": ""
@ -228,7 +230,8 @@
"zoomToSelection": "" "zoomToSelection": ""
}, },
"encrypted": { "encrypted": {
"tooltip": "ရေးဆွဲထားသောပုံများအား နှစ်ဘက်စွန်းတိုင်လျှို့ဝှက်ထားသဖြင့် Excalidraw ၏ဆာဗာများပင်လျှင်မြင်တွေ့ရမည်မဟုတ်ပါ။" "tooltip": "ရေးဆွဲထားသောပုံများအား နှစ်ဘက်စွန်းတိုင်လျှို့ဝှက်ထားသဖြင့် Excalidraw ၏ဆာဗာများပင်လျှင်မြင်တွေ့ရမည်မဟုတ်ပါ။",
"link": ""
}, },
"stats": { "stats": {
"angle": "ထောင့်", "angle": "ထောင့်",

View File

@ -92,6 +92,8 @@
"centerHorizontally": "Midtstill horisontalt", "centerHorizontally": "Midtstill horisontalt",
"distributeHorizontally": "Distribuer horisontalt", "distributeHorizontally": "Distribuer horisontalt",
"distributeVertically": "Distribuer vertikalt", "distributeVertically": "Distribuer vertikalt",
"flipHorizontal": "Snu horisontalt",
"flipVertical": "Snu vertikalt",
"viewMode": "Visningsmodus", "viewMode": "Visningsmodus",
"toggleExportColorScheme": "Veksle eksport av fargepalett", "toggleExportColorScheme": "Veksle eksport av fargepalett",
"share": "Del" "share": "Del"
@ -228,7 +230,8 @@
"zoomToSelection": "Zoom til utvalg" "zoomToSelection": "Zoom til utvalg"
}, },
"encrypted": { "encrypted": {
"tooltip": "Dine tegninger er ende-til-ende-krypterte slik at Excalidraw sine servere aldri vil se dem." "tooltip": "Dine tegninger er ende-til-ende-krypterte slik at Excalidraw sine servere aldri vil se dem.",
"link": "Blogginnlegg om ende-til-ende-kryptering i Excalidraw"
}, },
"stats": { "stats": {
"angle": "Vinkel", "angle": "Vinkel",

View File

@ -92,6 +92,8 @@
"centerHorizontally": "Horizontaal Centreren", "centerHorizontally": "Horizontaal Centreren",
"distributeHorizontally": "Horizontaal verspreiden", "distributeHorizontally": "Horizontaal verspreiden",
"distributeVertically": "Verticaal distribueren", "distributeVertically": "Verticaal distribueren",
"flipHorizontal": "Horizontaal spiegelen",
"flipVertical": "Verticaal spiegelen",
"viewMode": "Weergavemodus", "viewMode": "Weergavemodus",
"toggleExportColorScheme": "Kleurenschema exporteren aan/uit", "toggleExportColorScheme": "Kleurenschema exporteren aan/uit",
"share": "Deel" "share": "Deel"
@ -138,7 +140,7 @@
"decryptFailed": "Kan gegevens niet decoderen.", "decryptFailed": "Kan gegevens niet decoderen.",
"uploadedSecurly": "De upload is beveiligd met end-to-end encryptie, wat betekent dat de Excalidraw server en derden de inhoud niet kunnen lezen.", "uploadedSecurly": "De upload is beveiligd met end-to-end encryptie, wat betekent dat de Excalidraw server en derden de inhoud niet kunnen lezen.",
"loadSceneOverridePrompt": "Het laden van externe tekening zal uw bestaande inhoud vervangen. Wil je doorgaan?", "loadSceneOverridePrompt": "Het laden van externe tekening zal uw bestaande inhoud vervangen. Wil je doorgaan?",
"collabStopOverridePrompt": "", "collabStopOverridePrompt": "Wanneer de sessie wordt gestopt, overschrijft u de eerdere, lokaal opgeslagen tekening. Weet je het zeker?\n\n(Als je de lokale tekening wilt behouden, sluit je in plaats daarvan het browsertabblad)",
"errorLoadingLibrary": "Bij het laden van de externe bibliotheek is een fout opgetreden.", "errorLoadingLibrary": "Bij het laden van de externe bibliotheek is een fout opgetreden.",
"confirmAddLibrary": "Hiermee worden {{numShapes}} vorm(n) aan uw bibliotheek toegevoegd. Ben je het zeker?", "confirmAddLibrary": "Hiermee worden {{numShapes}} vorm(n) aan uw bibliotheek toegevoegd. Ben je het zeker?",
"imageDoesNotContainScene": "Afbeeldingen importeren wordt op dit moment niet ondersteund.\n\nWil je een scène importeren? Deze afbeelding lijkt geen scène gegevens te bevatten. Heb je dit geactiveerd tijdens het exporteren?", "imageDoesNotContainScene": "Afbeeldingen importeren wordt op dit moment niet ondersteund.\n\nWil je een scène importeren? Deze afbeelding lijkt geen scène gegevens te bevatten. Heb je dit geactiveerd tijdens het exporteren?",
@ -228,7 +230,8 @@
"zoomToSelection": "Inzoomen op selectie" "zoomToSelection": "Inzoomen op selectie"
}, },
"encrypted": { "encrypted": {
"tooltip": "Je tekeningen zijn beveiligd met end-to-end encryptie, dus Excalidraw's servers zullen nooit zien wat je tekent." "tooltip": "Je tekeningen zijn beveiligd met end-to-end encryptie, dus Excalidraw's servers zullen nooit zien wat je tekent.",
"link": "Blog post over end-to-end versleuteling in Excalidraw"
}, },
"stats": { "stats": {
"angle": "Hoek", "angle": "Hoek",

View File

@ -61,7 +61,7 @@
"architect": "Arkitekt", "architect": "Arkitekt",
"artist": "Kunstnar", "artist": "Kunstnar",
"cartoonist": "Teiknar", "cartoonist": "Teiknar",
"fileTitle": "Filnamn", "fileTitle": "",
"colorPicker": "Fargeveljar", "colorPicker": "Fargeveljar",
"canvasBackground": "Lerretsbakgrunn", "canvasBackground": "Lerretsbakgrunn",
"drawingCanvas": "Lerret", "drawingCanvas": "Lerret",
@ -92,6 +92,8 @@
"centerHorizontally": "Midtstill horisontalt", "centerHorizontally": "Midtstill horisontalt",
"distributeHorizontally": "Sprei horisontalt", "distributeHorizontally": "Sprei horisontalt",
"distributeVertically": "Sprei vertikalt", "distributeVertically": "Sprei vertikalt",
"flipHorizontal": "",
"flipVertical": "",
"viewMode": "", "viewMode": "",
"toggleExportColorScheme": "", "toggleExportColorScheme": "",
"share": "" "share": ""
@ -228,7 +230,8 @@
"zoomToSelection": "" "zoomToSelection": ""
}, },
"encrypted": { "encrypted": {
"tooltip": "Teikningane dine er ende-til-ende-krypterte slik at Excalidraw sine serverar aldri får sjå dei." "tooltip": "Teikningane dine er ende-til-ende-krypterte slik at Excalidraw sine serverar aldri får sjå dei.",
"link": ""
}, },
"stats": { "stats": {
"angle": "Vinkel", "angle": "Vinkel",

View File

@ -38,7 +38,7 @@
"fontSize": "Talha poliça", "fontSize": "Talha poliça",
"fontFamily": "Familha de poliça", "fontFamily": "Familha de poliça",
"onlySelected": "Seleccion sonque", "onlySelected": "Seleccion sonque",
"withBackground": "Inclure rèireplan", "withBackground": "Inclure lo rèireplan",
"exportEmbedScene": "Integrar la scèna al fichièr dexpo", "exportEmbedScene": "Integrar la scèna al fichièr dexpo",
"exportEmbedScene_details": "Las donadas de scèna seràn enregistradas dins lo fichièr PNG/SVG exportat, per que la scèna pòsca èsser restaurada a partir daqueste fichièr.\nAumentarà la talha del fichièr exportat.", "exportEmbedScene_details": "Las donadas de scèna seràn enregistradas dins lo fichièr PNG/SVG exportat, per que la scèna pòsca èsser restaurada a partir daqueste fichièr.\nAumentarà la talha del fichièr exportat.",
"addWatermark": "Apondre «Fabricat amb Excalidraw»", "addWatermark": "Apondre «Fabricat amb Excalidraw»",
@ -61,7 +61,7 @@
"architect": "Arquitècte", "architect": "Arquitècte",
"artist": "Artista", "artist": "Artista",
"cartoonist": "Dessenhaire", "cartoonist": "Dessenhaire",
"fileTitle": "Títol del fichièr", "fileTitle": "Nom del fichièr",
"colorPicker": "Selector de color", "colorPicker": "Selector de color",
"canvasBackground": "Rèireplan del canabàs", "canvasBackground": "Rèireplan del canabàs",
"drawingCanvas": "Zòna de dessenh", "drawingCanvas": "Zòna de dessenh",
@ -92,6 +92,8 @@
"centerHorizontally": "Centrar orizontalament", "centerHorizontally": "Centrar orizontalament",
"distributeHorizontally": "Distribuir orizontalament", "distributeHorizontally": "Distribuir orizontalament",
"distributeVertically": "Distribuir verticalament", "distributeVertically": "Distribuir verticalament",
"flipHorizontal": "Virar orizontalament",
"flipVertical": "Virar verticalament",
"viewMode": "Mòde de vista", "viewMode": "Mòde de vista",
"toggleExportColorScheme": "Alternar lesquèma de color dexpòrt", "toggleExportColorScheme": "Alternar lesquèma de color dexpòrt",
"share": "Partejar" "share": "Partejar"
@ -99,8 +101,8 @@
"buttons": { "buttons": {
"clearReset": "Reïnicializar lo canabàs", "clearReset": "Reïnicializar lo canabàs",
"export": "Exportar", "export": "Exportar",
"exportToPng": "Export en PNG", "exportToPng": "Exportar en PNG",
"exportToSvg": "Export en SVG", "exportToSvg": "Exportar en SVG",
"copyToClipboard": "Copiar al quichapapièrs", "copyToClipboard": "Copiar al quichapapièrs",
"copyPngToClipboard": "Copiar PNG al quichapapièrs", "copyPngToClipboard": "Copiar PNG al quichapapièrs",
"scale": "Escala", "scale": "Escala",
@ -228,7 +230,8 @@
"zoomToSelection": "Zoomar la seleccion" "zoomToSelection": "Zoomar la seleccion"
}, },
"encrypted": { "encrypted": {
"tooltip": "Vòstres dessenhs son chifrats del cap a la fin en consequéncia los servidors dExcalidraw los veiràn pas jamai." "tooltip": "Vòstres dessenhs son chifrats del cap a la fin en consequéncia los servidors dExcalidraw los veiràn pas jamai.",
"link": "Article de blòg sul chiframent del cap a la fin dins Excalidraw"
}, },
"stats": { "stats": {
"angle": "Angle", "angle": "Angle",

View File

@ -61,7 +61,7 @@
"architect": "ਭਵਨ ਨਿਰਮਾਣਕਾਰੀ", "architect": "ਭਵਨ ਨਿਰਮਾਣਕਾਰੀ",
"artist": "ਕਲਾਕਾਰ", "artist": "ਕਲਾਕਾਰ",
"cartoonist": "ਕਾਰਟੂਨਿਸਟ", "cartoonist": "ਕਾਰਟੂਨਿਸਟ",
"fileTitle": "ਫਾਈਲ ਦਾ ਸਿਰਨਾਵਾਂ", "fileTitle": "",
"colorPicker": "ਰੰਗ ਚੋਣਕਾਰ", "colorPicker": "ਰੰਗ ਚੋਣਕਾਰ",
"canvasBackground": "ਕੈਨਵਸ ਦਾ ਬੈਕਗਰਾਉਂਡ", "canvasBackground": "ਕੈਨਵਸ ਦਾ ਬੈਕਗਰਾਉਂਡ",
"drawingCanvas": "ਡਰਾਇੰਗ ਕੈਨਵਸ", "drawingCanvas": "ਡਰਾਇੰਗ ਕੈਨਵਸ",
@ -92,6 +92,8 @@
"centerHorizontally": "ਖੜ੍ਹਵੇਂ ਵਿਚਕਾਰ ਕਰੋ", "centerHorizontally": "ਖੜ੍ਹਵੇਂ ਵਿਚਕਾਰ ਕਰੋ",
"distributeHorizontally": "ਖੜ੍ਹਵੇਂ ਇਕਸਾਰ ਵੰਡੋ", "distributeHorizontally": "ਖੜ੍ਹਵੇਂ ਇਕਸਾਰ ਵੰਡੋ",
"distributeVertically": "ਲੇਟਵੇਂ ਇਕਸਾਰ ਵੰਡੋ", "distributeVertically": "ਲੇਟਵੇਂ ਇਕਸਾਰ ਵੰਡੋ",
"flipHorizontal": "",
"flipVertical": "",
"viewMode": "ਦੇਖੋ ਮੋਡ", "viewMode": "ਦੇਖੋ ਮੋਡ",
"toggleExportColorScheme": "", "toggleExportColorScheme": "",
"share": "" "share": ""
@ -228,7 +230,8 @@
"zoomToSelection": "ਚੋਣ ਤੱਕ ਜ਼ੂਮ ਕਰੋ" "zoomToSelection": "ਚੋਣ ਤੱਕ ਜ਼ੂਮ ਕਰੋ"
}, },
"encrypted": { "encrypted": {
"tooltip": "ਤੁਹਾਡੀ ਡਰਾਇੰਗਾਂ ਸਿਰੇ-ਤੋਂ-ਸਿਰੇ ਤੱਕ ਇਨਕਰਿਪਟ ਕੀਤੀਆਂ ਹੋਈਆਂ ਹਨ, ਇਸ ਲਈ Excalidraw ਦੇ ਸਰਵਰ ਉਹਨਾਂ ਨੂੰ ਕਦੇ ਵੀ ਨਹੀਂ ਦੇਖਣਗੇ।" "tooltip": "ਤੁਹਾਡੀ ਡਰਾਇੰਗਾਂ ਸਿਰੇ-ਤੋਂ-ਸਿਰੇ ਤੱਕ ਇਨਕਰਿਪਟ ਕੀਤੀਆਂ ਹੋਈਆਂ ਹਨ, ਇਸ ਲਈ Excalidraw ਦੇ ਸਰਵਰ ਉਹਨਾਂ ਨੂੰ ਕਦੇ ਵੀ ਨਹੀਂ ਦੇਖਣਗੇ।",
"link": ""
}, },
"stats": { "stats": {
"angle": "ਕੋਣ", "angle": "ਕੋਣ",

View File

@ -1,37 +1,37 @@
{ {
"ar-SA": 83, "ar-SA": 85,
"bg-BG": 94, "bg-BG": 93,
"ca-ES": 100, "ca-ES": 98,
"de-DE": 100, "de-DE": 100,
"el-GR": 98, "el-GR": 98,
"en": 100, "en": 100,
"es-ES": 100, "es-ES": 99,
"fa-IR": 90, "fa-IR": 88,
"fi-FI": 100, "fi-FI": 99,
"fr-FR": 100, "fr-FR": 100,
"he-IL": 91, "he-IL": 89,
"hi-IN": 93, "hi-IN": 91,
"hu-HU": 83, "hu-HU": 81,
"id-ID": 99, "id-ID": 100,
"it-IT": 100, "it-IT": 100,
"ja-JP": 96, "ja-JP": 97,
"kab-KAB": 99, "kab-KAB": 99,
"ko-KR": 94, "ko-KR": 92,
"my-MM": 77, "my-MM": 76,
"nb-NO": 100, "nb-NO": 100,
"nl-NL": 99, "nl-NL": 100,
"nn-NO": 85, "nn-NO": 83,
"oc-FR": 100, "oc-FR": 100,
"pa-IN": 95, "pa-IN": 94,
"pl-PL": 96, "pl-PL": 95,
"pt-BR": 100, "pt-BR": 100,
"pt-PT": 97, "pt-PT": 95,
"ro-RO": 100, "ro-RO": 100,
"ru-RU": 100, "ru-RU": 98,
"sk-SK": 100, "sk-SK": 99,
"sv-SE": 100, "sv-SE": 100,
"tr-TR": 83, "tr-TR": 99,
"uk-UA": 95, "uk-UA": 99,
"zh-CN": 100, "zh-CN": 99,
"zh-TW": 100 "zh-TW": 99
} }

View File

@ -61,7 +61,7 @@
"architect": "Dokładny", "architect": "Dokładny",
"artist": "Artystyczny", "artist": "Artystyczny",
"cartoonist": "Rysunkowy", "cartoonist": "Rysunkowy",
"fileTitle": "Tytuł pliku", "fileTitle": "",
"colorPicker": "Paleta kolorów", "colorPicker": "Paleta kolorów",
"canvasBackground": "Kolor dokumentu", "canvasBackground": "Kolor dokumentu",
"drawingCanvas": "Obszar roboczy", "drawingCanvas": "Obszar roboczy",
@ -92,6 +92,8 @@
"centerHorizontally": "Wyśrodkuj w poziomie", "centerHorizontally": "Wyśrodkuj w poziomie",
"distributeHorizontally": "Rozłóż poziomo", "distributeHorizontally": "Rozłóż poziomo",
"distributeVertically": "Rozłóż pionowo", "distributeVertically": "Rozłóż pionowo",
"flipHorizontal": "",
"flipVertical": "",
"viewMode": "Tryb widoku", "viewMode": "Tryb widoku",
"toggleExportColorScheme": "", "toggleExportColorScheme": "",
"share": "" "share": ""
@ -228,7 +230,8 @@
"zoomToSelection": "Przybliż do zaznaczenia" "zoomToSelection": "Przybliż do zaznaczenia"
}, },
"encrypted": { "encrypted": {
"tooltip": "Twoje rysunki są zabezpieczone szyfrowaniem end-to-end, tak więc nawet w Excalidraw nie jesteśmy w stanie zobaczyć tego co tworzysz." "tooltip": "Twoje rysunki są zabezpieczone szyfrowaniem end-to-end, tak więc nawet w Excalidraw nie jesteśmy w stanie zobaczyć tego co tworzysz.",
"link": ""
}, },
"stats": { "stats": {
"angle": "Kąt", "angle": "Kąt",

View File

@ -61,7 +61,7 @@
"architect": "Arquiteto", "architect": "Arquiteto",
"artist": "Artista", "artist": "Artista",
"cartoonist": "Cartunista", "cartoonist": "Cartunista",
"fileTitle": "Título do arquivo", "fileTitle": "Nome do arquivo",
"colorPicker": "Seletor de cores", "colorPicker": "Seletor de cores",
"canvasBackground": "Fundo da tela", "canvasBackground": "Fundo da tela",
"drawingCanvas": "Tela de desenho", "drawingCanvas": "Tela de desenho",
@ -92,6 +92,8 @@
"centerHorizontally": "Centralizar horizontalmente", "centerHorizontally": "Centralizar horizontalmente",
"distributeHorizontally": "Distribuir horizontalmente", "distributeHorizontally": "Distribuir horizontalmente",
"distributeVertically": "Distribuir verticalmente", "distributeVertically": "Distribuir verticalmente",
"flipHorizontal": "Inverter horizontalmente",
"flipVertical": "Inverter verticalmente",
"viewMode": "Modo de visualização", "viewMode": "Modo de visualização",
"toggleExportColorScheme": "Alternar esquema de cores de exportação", "toggleExportColorScheme": "Alternar esquema de cores de exportação",
"share": "Compartilhar" "share": "Compartilhar"
@ -228,7 +230,8 @@
"zoomToSelection": "Ampliar a seleção" "zoomToSelection": "Ampliar a seleção"
}, },
"encrypted": { "encrypted": {
"tooltip": "Seus desenhos são criptografados de ponta a ponta, então os servidores do Excalidraw nunca os verão." "tooltip": "Seus desenhos são criptografados de ponta a ponta, então os servidores do Excalidraw nunca os verão.",
"link": "Postagem de blog com uma criptografia de ponta a ponta no Excalidraw"
}, },
"stats": { "stats": {
"angle": "Ângulo", "angle": "Ângulo",

View File

@ -61,7 +61,7 @@
"architect": "Arquitecto", "architect": "Arquitecto",
"artist": "Artista", "artist": "Artista",
"cartoonist": "Caricaturista", "cartoonist": "Caricaturista",
"fileTitle": "Título do ficheiro", "fileTitle": "",
"colorPicker": "Seletor de cores", "colorPicker": "Seletor de cores",
"canvasBackground": "Fundo da tela", "canvasBackground": "Fundo da tela",
"drawingCanvas": "Tela de desenho", "drawingCanvas": "Tela de desenho",
@ -92,6 +92,8 @@
"centerHorizontally": "Centralizar horizontalmente", "centerHorizontally": "Centralizar horizontalmente",
"distributeHorizontally": "Distribuir horizontalmente", "distributeHorizontally": "Distribuir horizontalmente",
"distributeVertically": "Distribuir verticalmente", "distributeVertically": "Distribuir verticalmente",
"flipHorizontal": "",
"flipVertical": "",
"viewMode": "Modo de visualização", "viewMode": "Modo de visualização",
"toggleExportColorScheme": "Alternar esquema de cores de exportação", "toggleExportColorScheme": "Alternar esquema de cores de exportação",
"share": "" "share": ""
@ -228,7 +230,8 @@
"zoomToSelection": "Ampliar a seleção" "zoomToSelection": "Ampliar a seleção"
}, },
"encrypted": { "encrypted": {
"tooltip": "Seus desenhos são criptografados de ponta a ponta, então os servidores do Excalidraw nunca os verão." "tooltip": "Seus desenhos são criptografados de ponta a ponta, então os servidores do Excalidraw nunca os verão.",
"link": ""
}, },
"stats": { "stats": {
"angle": "Ângulo", "angle": "Ângulo",

View File

@ -61,7 +61,7 @@
"architect": "Arhitect", "architect": "Arhitect",
"artist": "Artist", "artist": "Artist",
"cartoonist": "Caricaturist", "cartoonist": "Caricaturist",
"fileTitle": "Denumirea fișierului", "fileTitle": "Nume de fișier",
"colorPicker": "Selector de culoare", "colorPicker": "Selector de culoare",
"canvasBackground": "Fundalul pânzei", "canvasBackground": "Fundalul pânzei",
"drawingCanvas": "Pânză pentru desenat", "drawingCanvas": "Pânză pentru desenat",
@ -92,6 +92,8 @@
"centerHorizontally": "Centrare orizontală", "centerHorizontally": "Centrare orizontală",
"distributeHorizontally": "Distribuie orizontal", "distributeHorizontally": "Distribuie orizontal",
"distributeVertically": "Distribuie vertical", "distributeVertically": "Distribuie vertical",
"flipHorizontal": "Răsturnare orizontală",
"flipVertical": "Răsturnare verticală",
"viewMode": "Mod de vizualizare", "viewMode": "Mod de vizualizare",
"toggleExportColorScheme": "Comutare schemă de culori de export", "toggleExportColorScheme": "Comutare schemă de culori de export",
"share": "Distribuie" "share": "Distribuie"
@ -228,7 +230,8 @@
"zoomToSelection": "Panoramare la selecție" "zoomToSelection": "Panoramare la selecție"
}, },
"encrypted": { "encrypted": {
"tooltip": "Desenele tale sunt criptate integral, astfel că serverele Excalidraw nu le vor vedea niciodată." "tooltip": "Desenele tale sunt criptate integral, astfel că serverele Excalidraw nu le vor vedea niciodată.",
"link": "Articol de blog pe criptarea integrală din Excalidraw"
}, },
"stats": { "stats": {
"angle": "Unghi", "angle": "Unghi",

View File

@ -61,7 +61,7 @@
"architect": "Архитектор", "architect": "Архитектор",
"artist": "Художник", "artist": "Художник",
"cartoonist": "Карикатурист", "cartoonist": "Карикатурист",
"fileTitle": "Название файла", "fileTitle": "Имя файла",
"colorPicker": "Выбор цвета", "colorPicker": "Выбор цвета",
"canvasBackground": "Фон холста", "canvasBackground": "Фон холста",
"drawingCanvas": "Полотно", "drawingCanvas": "Полотно",
@ -92,6 +92,8 @@
"centerHorizontally": "Центрировать по горизонтали", "centerHorizontally": "Центрировать по горизонтали",
"distributeHorizontally": "Распределить по горизонтали", "distributeHorizontally": "Распределить по горизонтали",
"distributeVertically": "Распределить по вертикали", "distributeVertically": "Распределить по вертикали",
"flipHorizontal": "",
"flipVertical": "",
"viewMode": "Вид", "viewMode": "Вид",
"toggleExportColorScheme": "Экспортировать цветовую схему", "toggleExportColorScheme": "Экспортировать цветовую схему",
"share": "Поделиться" "share": "Поделиться"
@ -228,7 +230,8 @@
"zoomToSelection": "Увеличить до выделенного" "zoomToSelection": "Увеличить до выделенного"
}, },
"encrypted": { "encrypted": {
"tooltip": "Ваши данные защищены сквозным (End-to-end) шифрованием. Серверы Excalidraw никогда не получат доступ к ним." "tooltip": "Ваши данные защищены сквозным (End-to-end) шифрованием. Серверы Excalidraw никогда не получат доступ к ним.",
"link": ""
}, },
"stats": { "stats": {
"angle": "Угол", "angle": "Угол",

View File

@ -92,6 +92,8 @@
"centerHorizontally": "Zarovnať vodorovne na stred", "centerHorizontally": "Zarovnať vodorovne na stred",
"distributeHorizontally": "Rozmiestniť vodorovne", "distributeHorizontally": "Rozmiestniť vodorovne",
"distributeVertically": "Rozmiestniť zvisle", "distributeVertically": "Rozmiestniť zvisle",
"flipHorizontal": "Prevrátiť vodorovne",
"flipVertical": "Prevrátiť zvislo",
"viewMode": "Režim zobrazenia", "viewMode": "Režim zobrazenia",
"toggleExportColorScheme": "Prepnúť exportovanie farebnej schémy", "toggleExportColorScheme": "Prepnúť exportovanie farebnej schémy",
"share": "Zdieľať" "share": "Zdieľať"
@ -228,7 +230,8 @@
"zoomToSelection": "Priblížiť na výber" "zoomToSelection": "Priblížiť na výber"
}, },
"encrypted": { "encrypted": {
"tooltip": "Vaše kresby používajú end-to-end šifrovanie, takže ich Excalidraw server nedokáže prečítať." "tooltip": "Vaše kresby používajú end-to-end šifrovanie, takže ich Excalidraw server nedokáže prečítať.",
"link": ""
}, },
"stats": { "stats": {
"angle": "Uhol", "angle": "Uhol",

View File

@ -61,7 +61,7 @@
"architect": "Arkitekt", "architect": "Arkitekt",
"artist": "Artist", "artist": "Artist",
"cartoonist": "Serietecknare", "cartoonist": "Serietecknare",
"fileTitle": "Filtitel", "fileTitle": "Filnamn",
"colorPicker": "Färgväljare", "colorPicker": "Färgväljare",
"canvasBackground": "Canvas-bakgrund", "canvasBackground": "Canvas-bakgrund",
"drawingCanvas": "Ritar canvas", "drawingCanvas": "Ritar canvas",
@ -92,6 +92,8 @@
"centerHorizontally": "Centrera horisontellt", "centerHorizontally": "Centrera horisontellt",
"distributeHorizontally": "Fördela horisontellt", "distributeHorizontally": "Fördela horisontellt",
"distributeVertically": "Fördela vertikalt", "distributeVertically": "Fördela vertikalt",
"flipHorizontal": "Vänd horisontellt",
"flipVertical": "Vänd vertikalt",
"viewMode": "Visningsläge", "viewMode": "Visningsläge",
"toggleExportColorScheme": "Växla färgschema för export", "toggleExportColorScheme": "Växla färgschema för export",
"share": "Dela" "share": "Dela"
@ -228,7 +230,8 @@
"zoomToSelection": "Zooma till markering" "zoomToSelection": "Zooma till markering"
}, },
"encrypted": { "encrypted": {
"tooltip": "Dina skisser är krypterade från ände till ände så Excalidraws servrar kommer aldrig att se dem." "tooltip": "Dina skisser är krypterade från ände till ände så Excalidraws servrar kommer aldrig att se dem.",
"link": "Blogginlägg om kryptering från ände till ände i Excalidraw"
}, },
"stats": { "stats": {
"angle": "Vinkel", "angle": "Vinkel",

View File

@ -1,7 +1,7 @@
{ {
"labels": { "labels": {
"paste": "Yapıştır", "paste": "Yapıştır",
"pasteCharts": "Dairesel grafik", "pasteCharts": "Grafikleri yapıştır",
"selectAll": "Tümünü seç", "selectAll": "Tümünü seç",
"multiSelect": "Seçime öge ekle", "multiSelect": "Seçime öge ekle",
"moveCanvas": "Tuvali taşı", "moveCanvas": "Tuvali taşı",
@ -68,7 +68,7 @@
"layers": "Katmanlar", "layers": "Katmanlar",
"actions": "Eylemler", "actions": "Eylemler",
"language": "Dil", "language": "Dil",
"liveCollaboration": "", "liveCollaboration": "Canlı ortak çalışma alanı",
"duplicateSelection": "Çoğalt", "duplicateSelection": "Çoğalt",
"untitled": "Adsız", "untitled": "Adsız",
"name": "İsim", "name": "İsim",
@ -77,7 +77,7 @@
"group": "Seçimi grup yap", "group": "Seçimi grup yap",
"ungroup": "Seçilen grubu dağıt", "ungroup": "Seçilen grubu dağıt",
"collaborators": "Ortaklar", "collaborators": "Ortaklar",
"showGrid": "", "showGrid": "Izgarayı göster",
"addToLibrary": "Kütüphaneye ekle", "addToLibrary": "Kütüphaneye ekle",
"removeFromLibrary": "Kütüphaneden kaldır", "removeFromLibrary": "Kütüphaneden kaldır",
"libraryLoadingMessage": "Kütüphane yükleniyor…", "libraryLoadingMessage": "Kütüphane yükleniyor…",
@ -92,9 +92,11 @@
"centerHorizontally": "Yatayda ortala", "centerHorizontally": "Yatayda ortala",
"distributeHorizontally": "Yatay dağıt", "distributeHorizontally": "Yatay dağıt",
"distributeVertically": "Dikey dağıt", "distributeVertically": "Dikey dağıt",
"viewMode": "", "flipHorizontal": "Yatay döndür",
"toggleExportColorScheme": "", "flipVertical": "Dikey döndür",
"share": "" "viewMode": "Görünüm modu",
"toggleExportColorScheme": "Renk şemasını dışa aktar/aktarma",
"share": "Paylaş"
}, },
"buttons": { "buttons": {
"clearReset": "Tuvali sıfırla", "clearReset": "Tuvali sıfırla",
@ -119,7 +121,7 @@
"edit": "Düzenle", "edit": "Düzenle",
"undo": "Geri Al", "undo": "Geri Al",
"redo": "Yeniden yap", "redo": "Yeniden yap",
"resetLibrary": "", "resetLibrary": "Kütüphaneyi sıfırla",
"createNewRoom": "Yeni oda oluştur", "createNewRoom": "Yeni oda oluştur",
"fullScreen": "Tam ekran", "fullScreen": "Tam ekran",
"darkMode": "Koyu tema", "darkMode": "Koyu tema",
@ -138,13 +140,13 @@
"decryptFailed": "Şifrelenmiş veri çözümlenemedi.", "decryptFailed": "Şifrelenmiş veri çözümlenemedi.",
"uploadedSecurly": "Yükleme uçtan uca şifreleme ile korunmaktadır. Excalidraw sunucusu ve üçüncül şahıslar içeriği okuyamayacaktır.", "uploadedSecurly": "Yükleme uçtan uca şifreleme ile korunmaktadır. Excalidraw sunucusu ve üçüncül şahıslar içeriği okuyamayacaktır.",
"loadSceneOverridePrompt": "Harici çizimler yüklemek mevcut olan içeriği değiştirecektir. Devam etmek istiyor musunuz?", "loadSceneOverridePrompt": "Harici çizimler yüklemek mevcut olan içeriği değiştirecektir. Devam etmek istiyor musunuz?",
"collabStopOverridePrompt": "", "collabStopOverridePrompt": "Oturumu sonlandırmak daha önceki, yerel olarak kaydedilmiş çizimin üzerine kaydedilmesine sebep olacak. Emin misiniz?\n\n(Yerel çiziminizi kaybetmemek için tarayıcı sekmesini kapatabilirsiniz.)",
"errorLoadingLibrary": "Üçüncü taraf kitaplığı yüklerken bir hata oluştu.", "errorLoadingLibrary": "Üçüncü taraf kitaplığı yüklerken bir hata oluştu.",
"confirmAddLibrary": "Bu, kitaplığınıza {{numShapes}} tane şekil ekleyecek. Emin misiniz?", "confirmAddLibrary": "Bu, kitaplığınıza {{numShapes}} tane şekil ekleyecek. Emin misiniz?",
"imageDoesNotContainScene": "Resim ekleme şuan için desteklenmiyor.\nBir sahneyi içeri aktarmak mı istediniz? Bu dosya herhangi bir sahne içeriyor gibi durmuyor. Çıktı alırken sahneyi dahil ettiniz mi?", "imageDoesNotContainScene": "Resim ekleme şuan için desteklenmiyor.\nBir sahneyi içeri aktarmak mı istediniz? Bu dosya herhangi bir sahne içeriyor gibi durmuyor. Çıktı alırken sahneyi dahil ettiniz mi?",
"cannotRestoreFromImage": "Sahne bu dosyadan oluşturulamıyor", "cannotRestoreFromImage": "Sahne bu dosyadan oluşturulamıyor",
"invalidSceneUrl": "", "invalidSceneUrl": "Verilen URL'den çalışma alanı yüklenemedi. Dosya bozuk olabilir veya geçerli bir Excalidraw JSON verisi bulundurmuyor olabilir.",
"resetLibrary": "" "resetLibrary": "Bu işlem kütüphanenizi sıfırlayacak. Emin misiniz?"
}, },
"toolBar": { "toolBar": {
"selection": "Seçme", "selection": "Seçme",
@ -201,34 +203,35 @@
"desc_inProgressIntro": "Ortak çalışma ortamı oluşturuldu.", "desc_inProgressIntro": "Ortak çalışma ortamı oluşturuldu.",
"desc_shareLink": "Bu bağlantıyı birlikte çalışacağınız kişilerle paylaşabilirsiniz:", "desc_shareLink": "Bu bağlantıyı birlikte çalışacağınız kişilerle paylaşabilirsiniz:",
"desc_exitSession": "Çalışma ortamını kapattığınızda ortak çalışmadan ayrılmış olursunuz ancak kendi versiyonunuzda çalışmaya devam edebilirsiniz. Bu durumda ortak çalıştığınız diğer kişiler etkilenmeyecek, çalışma ortamındaki versiyon üzerinden çalışmaya devam edebilecekler.", "desc_exitSession": "Çalışma ortamını kapattığınızda ortak çalışmadan ayrılmış olursunuz ancak kendi versiyonunuzda çalışmaya devam edebilirsiniz. Bu durumda ortak çalıştığınız diğer kişiler etkilenmeyecek, çalışma ortamındaki versiyon üzerinden çalışmaya devam edebilecekler.",
"shareTitle": "" "shareTitle": "Excalidraw'da canlı ortak calışma oturumuna katıl"
}, },
"errorDialog": { "errorDialog": {
"title": "Hata" "title": "Hata"
}, },
"helpDialog": { "helpDialog": {
"blog": "", "blog": "Blog'umuzu okuyun",
"click": "", "click": "tıkla",
"curvedArrow": "", "curvedArrow": "Eğri ok",
"curvedLine": "", "curvedLine": "Eğri çizgi",
"documentation": "", "documentation": "Dokümantasyon",
"drag": "", "drag": "sürükle",
"editor": "", "editor": "Düzenleyici",
"github": "", "github": "Bir hata mı buldun? Bildir",
"howto": "", "howto": "Rehberlerimizi takip edin",
"or": "", "or": "veya",
"preventBinding": "", "preventBinding": "Ok bağlamayı önleyin",
"shapes": "", "shapes": "Şekiller",
"shortcuts": "", "shortcuts": "Klavye kısayolları",
"textFinish": "", "textFinish": "(Metin) düzenlemeyi bitir",
"textNewLine": "", "textNewLine": "Yeni satır ekle (metin)",
"title": "", "title": "Yardım",
"view": "", "view": "Görünüm",
"zoomToFit": "", "zoomToFit": "Tüm öğeleri sığdırmak için yakınlaştır",
"zoomToSelection": "" "zoomToSelection": "Seçime yakınlaş"
}, },
"encrypted": { "encrypted": {
"tooltip": "Çizimleriniz uçtan-uca şifrelenmiştir, Excalidraw'ın sunucuları bile onları göremez." "tooltip": "Çizimleriniz uçtan-uca şifrelenmiştir, Excalidraw'ın sunucuları bile onları göremez.",
"link": ""
}, },
"stats": { "stats": {
"angle": "Açı", "angle": "Açı",
@ -240,18 +243,18 @@
"storage": "Depolama", "storage": "Depolama",
"title": "İnekler için istatistikler", "title": "İnekler için istatistikler",
"total": "Toplam", "total": "Toplam",
"version": "", "version": "Sürüm",
"versionCopy": "", "versionCopy": "Kopyalamak için tıkla",
"versionNotAvailable": "", "versionNotAvailable": "Sürüm mevcut değil",
"width": "Genişlik" "width": "Genişlik"
}, },
"toast": { "toast": {
"copyStyles": "", "copyStyles": "Stiller kopyalandı.",
"copyToClipboard": "", "copyToClipboard": "Panoya kopyalandı.",
"copyToClipboardAsPng": "", "copyToClipboardAsPng": "{{exportSelection}} panoya PNG olarak\n({{exportColorScheme}}) kopyalandı",
"fileSaved": "", "fileSaved": "Dosya kaydedildi.",
"fileSavedToFilename": "", "fileSavedToFilename": "{filename} kaydedildi",
"canvas": "", "canvas": "tuval",
"selection": "" "selection": "seçim"
} }
} }

View File

@ -68,7 +68,7 @@
"layers": "Шари", "layers": "Шари",
"actions": "Дії", "actions": "Дії",
"language": "Мова", "language": "Мова",
"liveCollaboration": "", "liveCollaboration": "Спільна співпраця",
"duplicateSelection": "Дублювати", "duplicateSelection": "Дублювати",
"untitled": "Без назви", "untitled": "Без назви",
"name": "Ім’я", "name": "Ім’я",
@ -92,9 +92,11 @@
"centerHorizontally": "Центрувати по горизонталі", "centerHorizontally": "Центрувати по горизонталі",
"distributeHorizontally": "Розподілити по горизонталі", "distributeHorizontally": "Розподілити по горизонталі",
"distributeVertically": "Розподілити вертикально", "distributeVertically": "Розподілити вертикально",
"flipHorizontal": "Віддзеркалити горизонтально",
"flipVertical": "Віддзеркалити вертикально",
"viewMode": "Режим перегляду", "viewMode": "Режим перегляду",
"toggleExportColorScheme": "", "toggleExportColorScheme": "Переключити колірну схему експорту",
"share": "" "share": "Поділитися"
}, },
"buttons": { "buttons": {
"clearReset": "Очистити полотно", "clearReset": "Очистити полотно",
@ -119,7 +121,7 @@
"edit": "Редагувати", "edit": "Редагувати",
"undo": "Відмінити", "undo": "Відмінити",
"redo": "Повторити", "redo": "Повторити",
"resetLibrary": "", "resetLibrary": "Очистити бібліотеку",
"createNewRoom": "Створити нову кімнату", "createNewRoom": "Створити нову кімнату",
"fullScreen": "Повноекранний режим", "fullScreen": "Повноекранний режим",
"darkMode": "Темний режим", "darkMode": "Темний режим",
@ -143,8 +145,8 @@
"confirmAddLibrary": "Це призведе до додавання {{numShapes}} фігур до вашої бібліотеки. Ви впевнені?", "confirmAddLibrary": "Це призведе до додавання {{numShapes}} фігур до вашої бібліотеки. Ви впевнені?",
"imageDoesNotContainScene": "Імпортування зображень на даний момент не підтримується.\n\nЧи хочете ви імпортувати сцену? Це зображення не містить ніяких даних сцен. Ви увімкнули це під час експорту?", "imageDoesNotContainScene": "Імпортування зображень на даний момент не підтримується.\n\nЧи хочете ви імпортувати сцену? Це зображення не містить ніяких даних сцен. Ви увімкнули це під час експорту?",
"cannotRestoreFromImage": "Сцена не може бути відновлена з цього файлу зображення", "cannotRestoreFromImage": "Сцена не може бути відновлена з цього файлу зображення",
"invalidSceneUrl": "", "invalidSceneUrl": "Не вдалося імпортувати сцену з наданого URL. Він або недоформований, або не містить дійсних даних Excalidraw JSON.",
"resetLibrary": "" "resetLibrary": "Це призведе до очищення бібліотеки. Ви впевнені?"
}, },
"toolBar": { "toolBar": {
"selection": "Виділення", "selection": "Виділення",
@ -201,7 +203,7 @@
"desc_inProgressIntro": "Сесія спільної роботи над кресленням триває.", "desc_inProgressIntro": "Сесія спільної роботи над кресленням триває.",
"desc_shareLink": "Поділіться цим посиланням з будь-ким для спільної роботи:", "desc_shareLink": "Поділіться цим посиланням з будь-ким для спільної роботи:",
"desc_exitSession": "Зупинка сесії відключить вас від кімнати, але ви зможете продовжити роботу з полотном локально. Зверніть увагу, що це не вплине на інших людей, і вони все одно зможуть працювати над їх версією.", "desc_exitSession": "Зупинка сесії відключить вас від кімнати, але ви зможете продовжити роботу з полотном локально. Зверніть увагу, що це не вплине на інших людей, і вони все одно зможуть працювати над їх версією.",
"shareTitle": "" "shareTitle": "Приєднатися до сеансу спільної роботи на Excalidraw"
}, },
"errorDialog": { "errorDialog": {
"title": "Помилка" "title": "Помилка"
@ -228,7 +230,8 @@
"zoomToSelection": "Наблизити вибране" "zoomToSelection": "Наблизити вибране"
}, },
"encrypted": { "encrypted": {
"tooltip": "Ваші креслення захищені наскрізним шифруванням — сервери Excalidraw ніколи їх не побачать." "tooltip": "Ваші креслення захищені наскрізним шифруванням — сервери Excalidraw ніколи їх не побачать.",
"link": ""
}, },
"stats": { "stats": {
"angle": "Кут", "angle": "Кут",
@ -248,10 +251,10 @@
"toast": { "toast": {
"copyStyles": "Скопійовані стилі.", "copyStyles": "Скопійовані стилі.",
"copyToClipboard": "Скопіювати до буферу обміну.", "copyToClipboard": "Скопіювати до буферу обміну.",
"copyToClipboardAsPng": "", "copyToClipboardAsPng": "Скопійовано {{exportSelection}} до буфера обміну як PNG\n({{exportColorScheme}})",
"fileSaved": "Файл збережено.", "fileSaved": "Файл збережено.",
"fileSavedToFilename": "Збережено в {filename}", "fileSavedToFilename": "Збережено в {filename}",
"canvas": "", "canvas": "полотно",
"selection": "" "selection": "виділення"
} }
} }

View File

@ -25,7 +25,7 @@
"strokeStyle_dashed": "虚线", "strokeStyle_dashed": "虚线",
"strokeStyle_dotted": "点虚线", "strokeStyle_dotted": "点虚线",
"sloppiness": "边框", "sloppiness": "边框",
"opacity": "透明度", "opacity": "透明度",
"textAlign": "文本对齐", "textAlign": "文本对齐",
"edges": "边角", "edges": "边角",
"sharp": "尖锐", "sharp": "尖锐",
@ -38,7 +38,7 @@
"fontSize": "字体大小", "fontSize": "字体大小",
"fontFamily": "字体", "fontFamily": "字体",
"onlySelected": "仅被选中", "onlySelected": "仅被选中",
"withBackground": "使用背景", "withBackground": "包括背景",
"exportEmbedScene": "将画布数据嵌入到导出的文件", "exportEmbedScene": "将画布数据嵌入到导出的文件",
"exportEmbedScene_details": "画布数据将被保存到导出的 PNG/SVG 文件,以便恢复。\n将会增加导出的文件大小。", "exportEmbedScene_details": "画布数据将被保存到导出的 PNG/SVG 文件,以便恢复。\n将会增加导出的文件大小。",
"addWatermark": "添加 “使用 Excalidraw 创建” 水印", "addWatermark": "添加 “使用 Excalidraw 创建” 水印",
@ -61,7 +61,7 @@
"architect": "朴素", "architect": "朴素",
"artist": "艺术", "artist": "艺术",
"cartoonist": "漫画家", "cartoonist": "漫画家",
"fileTitle": "文件标题", "fileTitle": "文件",
"colorPicker": "调色盘", "colorPicker": "调色盘",
"canvasBackground": "画布背景", "canvasBackground": "画布背景",
"drawingCanvas": "绘制 Canvas", "drawingCanvas": "绘制 Canvas",
@ -92,6 +92,8 @@
"centerHorizontally": "水平居中", "centerHorizontally": "水平居中",
"distributeHorizontally": "水平等距分布", "distributeHorizontally": "水平等距分布",
"distributeVertically": "垂直等距分布", "distributeVertically": "垂直等距分布",
"flipHorizontal": "水平翻转",
"flipVertical": "垂直翻转",
"viewMode": "查看模式", "viewMode": "查看模式",
"toggleExportColorScheme": "切换导出配色方案", "toggleExportColorScheme": "切换导出配色方案",
"share": "分享" "share": "分享"
@ -128,7 +130,7 @@
"exitZenMode": "退出禅模式" "exitZenMode": "退出禅模式"
}, },
"alerts": { "alerts": {
"clearReset": "这将会清除整个 画板。您是否要继续?", "clearReset": "这将会清除整个画布。您是否要继续?",
"couldNotCreateShareableLink": "无法创建共享链接", "couldNotCreateShareableLink": "无法创建共享链接",
"couldNotCreateShareableLinkTooBig": "无法创建可共享链接:画布过大", "couldNotCreateShareableLinkTooBig": "无法创建可共享链接:画布过大",
"couldNotLoadInvalidFile": "无法加载无效的文件", "couldNotLoadInvalidFile": "无法加载无效的文件",
@ -228,7 +230,8 @@
"zoomToSelection": "缩放到选区" "zoomToSelection": "缩放到选区"
}, },
"encrypted": { "encrypted": {
"tooltip": "您的绘图采用的端到端加密其内容对于Excalidraw服务器是不可见的。" "tooltip": "您的绘图采用的端到端加密其内容对于Excalidraw服务器是不可见的。",
"link": ""
}, },
"stats": { "stats": {
"angle": "角度", "angle": "角度",

View File

@ -61,7 +61,7 @@
"architect": "精確", "architect": "精確",
"artist": "藝術", "artist": "藝術",
"cartoonist": "卡通", "cartoonist": "卡通",
"fileTitle": "檔案標題", "fileTitle": "檔案名稱",
"colorPicker": "色彩選擇工具", "colorPicker": "色彩選擇工具",
"canvasBackground": "Canvas 背景", "canvasBackground": "Canvas 背景",
"drawingCanvas": "繪圖 canvas", "drawingCanvas": "繪圖 canvas",
@ -92,6 +92,8 @@
"centerHorizontally": "水平置中", "centerHorizontally": "水平置中",
"distributeHorizontally": "水平分布", "distributeHorizontally": "水平分布",
"distributeVertically": "垂直分布", "distributeVertically": "垂直分布",
"flipHorizontal": "水平翻轉",
"flipVertical": "垂直翻轉",
"viewMode": "檢視模式", "viewMode": "檢視模式",
"toggleExportColorScheme": "切換輸出配色", "toggleExportColorScheme": "切換輸出配色",
"share": "共享" "share": "共享"
@ -228,7 +230,8 @@
"zoomToSelection": "縮放至選取區" "zoomToSelection": "縮放至選取區"
}, },
"encrypted": { "encrypted": {
"tooltip": "你的作品已使用 end-to-end 方式加密Excalidraw 的伺服器也無法取得其內容。" "tooltip": "你的作品已使用 end-to-end 方式加密Excalidraw 的伺服器也無法取得其內容。",
"link": ""
}, },
"stats": { "stats": {
"angle": "角度", "angle": "角度",

View File

@ -12,6 +12,32 @@ The change should be grouped under one of the below section and must contain PR
Please add the latest change on the top under the correct section. Please add the latest change on the top under the correct section.
--> -->
## Unreleased
## Excalidraw API
### Features
- Add `UIOptions` prop to customise `canvas actions` which includes customising `background color picker`, `clear canvas`, `export`, `load`, `save`, `save as` & `theme toggle` [#3364](https://github.com/excalidraw/excalidraw/pull/3364).
- Calculate `width/height` of canvas based on excalidraw component (".excalidraw" selector) & also resize and update offsets whenever the dimensions of excalidraw component gets updated [#3379](https://github.com/excalidraw/excalidraw/pull/3379). You also don't need to add a resize handler anymore for excalidraw as its handled now in excalidraw itself.
#### BREAKING CHANGE
- `width/height` props have been removed. Instead now it takes `100%` of `width` and `height` of the container so you need to make sure the container in which you are rendering Excalidraw has non zero dimensions (It should have non zero width and height so Excalidraw can match the dimensions of containing block)
- Calculate offsets when excalidraw container resizes using resize observer api [#3374](https://github.com/excalidraw/excalidraw/pull/3374).
- Export types for the package so now it can be used with typescript [#3337](https://github.com/excalidraw/excalidraw/pull/3337). The types are available at `@excalidraw/excalirdraw/types`.
- Add `renderCustomStats` prop to render extra stats on host, and expose `setToastMessage` API via refs which can be used to show toast with custom message [#3360](https://github.com/excalidraw/excalidraw/pull/3360).
- Support passing a CSRF token when importing libraries to prevent prompting before installation. The token is passed from [https://libraries.excalidraw.com](https://libraries.excalidraw.com/) using the `token` URL key [#3329](https://github.com/excalidraw/excalidraw/pull/3329).
- #### BREAKING CHANGE
Use `location.hash` when importing libraries to fix installation issues. This will require host apps to add a `hashchange` listener and call the newly exposed `excalidrawAPI.importLibrary(url)` API when applicable [#3320](https://github.com/excalidraw/excalidraw/pull/3320).
- Append `location.pathname` to `libraryReturnUrl` default url [#3325](https://github.com/excalidraw/excalidraw/pull/3325).
### Build
- Expose separate builds for dev and prod and support source maps in dev build [#3330](https://github.com/excalidraw/excalidraw/pull/3330).
#### BREAKING CHANGE
- If you are using script tag to embed excalidraw then the name of the file will have to be updated to `excalidraw.production.min.js` instead of `excalidraw.min.js`. If you want to use dev build you can use `excalidraw.development.js`
---
## 0.5.0 (2021-03-21) ## 0.5.0 (2021-03-21)
## Excalidraw API ## Excalidraw API

View File

@ -1,6 +1,6 @@
### Excalidraw ### Excalidraw
Excalidraw exported as a component to directly embed in your projects Excalidraw exported as a component to directly embed in your projects.
### Installation ### Installation
@ -28,10 +28,13 @@ If you want to load assets from a different path you can set a variable `window.
[Try here](https://codesandbox.io/s/excalidraw-ehlz3). [Try here](https://codesandbox.io/s/excalidraw-ehlz3).
<details id="usage"> ### Usage
<summary><strong>Usage</strong></summary>
1. If you are using a Web bundler (for instance, Webpack), you can import it as an ES6 module as shown below #### Using Web Bundler
If you are using a Web bundler (for instance, Webpack), you can import it as an ES6 module as shown below
<details><summary><strong>View Example</strong></summary>
```js ```js
import React, { useEffect, useState, useRef } from "react"; import React, { useEffect, useState, useRef } from "react";
@ -168,10 +171,29 @@ To view the full example visit :point_down:
[![Edit excalidraw](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/excalidraw-ehlz3?fontsize=14&hidenavigation=1&theme=dark) [![Edit excalidraw](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/excalidraw-ehlz3?fontsize=14&hidenavigation=1&theme=dark)
2. To use it in a browser directly: Since Excalidraw doesn't support server side rendering yet so you will have to make sure the component is rendered once host is mounted.
```js
import { useState, useEffect } from "react";
export default function IndexPage() {
const [Comp, setComp] = useState(null);
useEffect(() => {
import("@excalidraw/excalidraw").then((comp) => setComp(comp.default));
});
return <>{Comp && <Comp />}</>;
}
```
</details>
#### In Browser
To use it in a browser directly:
You will need to make sure `react`, `react-dom` is available as shown below. You will need to make sure `react`, `react-dom` is available as shown below.
<details><summary><strong>View Example</strong></summary>
```html ```html
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
@ -183,7 +205,7 @@ You will need to make sure `react`, `react-dom` is available as shown below.
<script <script
type="text/javascript" type="text/javascript"
src="https://unpkg.com/@excalidraw/excalidraw@0.4.1/dist/excalidraw.min.js" src="https://unpkg.com/@excalidraw/excalidraw@0.5.0/dist/excalidraw.min.js"
></script> ></script>
</head> </head>
@ -348,23 +370,9 @@ To view the full example visit :point_down:
[![Edit excalidraw-in-browser](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/excalidraw-in-browser-tlqom?fontsize=14&hidenavigation=1&theme=dark) [![Edit excalidraw-in-browser](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/excalidraw-in-browser-tlqom?fontsize=14&hidenavigation=1&theme=dark)
Since Excalidraw doesn't support server side rendering yet so you will have to make sure the component is rendered once host is mounted.
```js
import { useState, useEffect } from "react";
export default function IndexPage() {
const [Comp, setComp] = useState(null);
useEffect(() => {
import("@excalidraw/excalidraw").then((comp) => setComp(comp.default));
});
return <>{Comp && <Comp />}</>;
}
```
</details> </details>
<details id="props"> ### Props
<summary><strong>Props</strong></summary>
| Name | Type | Default | Description | | Name | Type | Default | Description |
| --- | --- | --- | --- | | --- | --- | --- | --- |
@ -537,7 +545,7 @@ This prop indicates whether the shows the grid. When supplied, the value takes p
#### `libraryReturnUrl` #### `libraryReturnUrl`
If supplied, this URL will be used when user tries to install a library from [libraries.excalidraw.com](https://libraries.excalidraw.com). Default to `window.location.origin`. To install the libraries in the same tab from which it was opened, you need to set `window.name` (to any alphanumeric string) — if it's not set it will open in a new tab. If supplied, this URL will be used when user tries to install a library from [libraries.excalidraw.com](https://libraries.excalidraw.com). Defaults to `window.location.origin`. To install the libraries in the same tab from which it was opened, you need to set `window.name` (to any alphanumeric string) — if it's not set it will open in a new tab.
#### `theme` #### `theme`
@ -547,10 +555,11 @@ This prop controls Excalidraw's theme. When supplied, the value takes precedence
This prop sets the name of the drawing which will be used when exporting the drawing. When supplied, the value takes precedence over `intialData.appState.name`, the `name` will be fully controlled by host app and the users won't be able to edit from within Excalidraw. This prop sets the name of the drawing which will be used when exporting the drawing. When supplied, the value takes precedence over `intialData.appState.name`, the `name` will be fully controlled by host app and the users won't be able to edit from within Excalidraw.
</details> ### Does it support collaboration ?
<details id="extra-apis"> No Excalidraw package doesn't come with collaboration, since this would have different implementations on the consumer so we expose the API's which you can use to communicate with Excalidraw as mentioned above. If you are interested in understanding how Excalidraw does it you can check it [here](https://github.com/excalidraw/excalidraw/blob/master/src/excalidraw-app/index.tsx).
<summary><strong>Extra API's</strong></summary>
### Extra API's
#### `getSceneVersion` #### `getSceneVersion`
@ -595,8 +604,7 @@ import { getElementsMap } from "@excalidraw/excalidraw";
This function returns an object where each element is mapped to its id. This function returns an object where each element is mapped to its id.
<details id="restore-utils"> ### Restore utilities
<summary><strong>Restore utilities</strong></summary>
#### `restoreAppState` #### `restoreAppState`
@ -646,10 +654,7 @@ import { restore } from "@excalidraw/excalidraw";
This function makes sure elements and state is set to appropriate values and set to default value if not present. It is combination of [restoreElements](#restoreElements) and [restoreAppState](#restoreAppState) This function makes sure elements and state is set to appropriate values and set to default value if not present. It is combination of [restoreElements](#restoreElements) and [restoreAppState](#restoreAppState)
</details> ### Export utilities
<details id="export-utils">
<summary><strong>Export utilities</strong></summary>
#### `exportToCanvas` #### `exportToCanvas`
@ -733,6 +738,3 @@ This function returns a svg with the exported elements.
| viewBackgroundColor | string | #fff | The default background color | | viewBackgroundColor | string | #fff | The default background color |
| shouldAddWatermark | boolean | false | Indicates whether watermark should be exported | | shouldAddWatermark | boolean | false | Indicates whether watermark should be exported |
| exportWithDarkMode | boolean | false | Indicates whether to export with dark mode | | exportWithDarkMode | boolean | false | Indicates whether to export with dark mode |
</details>
</details>

View File

@ -0,0 +1,734 @@
### Excalidraw
Excalidraw exported as a component to directly embed in your projects.
### Installation
You can use npm
```
npm install react react-dom @excalidraw/excalidraw
```
or via yarn
```
yarn add react react-dom @excalidraw/excalidraw
```
After installation you will see a folder `excalidraw-assets` and `excalidraw-dev-assets` in `dist` directory which contains the assets needed for this app in prod and dev mode respectively.
Move the folder `excalidraw-assets` and `excalidraw-dev-assets` to the path where your assets are served.
By default it will try to load the files from `https://unpkg.com/@excalidraw/excalidraw/{currentVersion}/dist/`
If you want to load assets from a different path you can set a variable `window.EXCALIDRAW_ASSET_PATH` depending on environment (for example if you have different URL's for dev and prod) to the url from where you want to load the assets.
### Demo
[Try here](https://codesandbox.io/s/excalidraw-ehlz3).
### Usage
#### Using Web Bundler
If you are using a Web bundler (for instance, Webpack), you can import it as an ES6 module as shown below
<details><summary><strong>View Example</strong></summary>
```js
import React, { useEffect, useState, useRef } from "react";
import Excalidraw from "@excalidraw/excalidraw";
import InitialData from "./initialData";
import "./styles.scss";
export default function App() {
const excalidrawRef = useRef(null);
const [viewModeEnabled, setViewModeEnabled] = useState(false);
const [zenModeEnabled, setZenModeEnabled] = useState(false);
const [gridModeEnabled, setGridModeEnabled] = useState(false);
const updateScene = () => {
const sceneData = {
elements: [
{
type: "rectangle",
version: 141,
versionNonce: 361174001,
isDeleted: false,
id: "oDVXy8D6rom3H1-LLH2-f",
fillStyle: "hachure",
strokeWidth: 1,
strokeStyle: "solid",
roughness: 1,
opacity: 100,
angle: 0,
x: 100.50390625,
y: 93.67578125,
strokeColor: "#c92a2a",
backgroundColor: "transparent",
width: 186.47265625,
height: 141.9765625,
seed: 1968410350,
groupIds: [],
},
],
appState: {
viewBackgroundColor: "#edf2ff",
},
};
excalidrawRef.current.updateScene(sceneData);
};
return (
<div className="App">
<h1> Excalidraw Example</h1>
<div className="button-wrapper">
<button className="update-scene" onClick={updateScene}>
Update Scene
</button>
<button
className="reset-scene"
onClick={() => {
excalidrawRef.current.resetScene();
}}
>
Reset Scene
</button>
<label>
<input
type="checkbox"
checked={viewModeEnabled}
onChange={() => setViewModeEnabled(!viewModeEnabled)}
/>
View mode
</label>
<label>
<input
type="checkbox"
checked={zenModeEnabled}
onChange={() => setZenModeEnabled(!zenModeEnabled)}
/>
Zen mode
</label>
<label>
<input
type="checkbox"
checked={gridModeEnabled}
onChange={() => setGridModeEnabled(!gridModeEnabled)}
/>
Grid mode
</label>
</div>
<div className="excalidraw-wrapper">
<Excalidraw
ref={excalidrawRef}
initialData={InitialData}
onChange={(elements, state) => {
console.log("Elements :", elements, "State : ", state)
}
onPointerUpdate={(payload) => console.log(payload)}
onCollabButtonClick={() =>
window.alert("You clicked on collab button")
}
viewModeEnabled={viewModeEnabled}
zenModeEnabled={zenModeEnabled}
gridModeEnabled={gridModeEnabled}
/>
</div>
</div>
);
}
```
To view the full example visit :point_down:
[![Edit excalidraw](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/excalidraw-ehlz3?fontsize=14&hidenavigation=1&theme=dark)
</details>
Since Excalidraw doesn't support server side rendering yet so you will have to make sure the component is rendered once host is mounted.
```js
import { useState, useEffect } from "react";
export default function IndexPage() {
const [Comp, setComp] = useState(null);
useEffect(() => {
import("@excalidraw/excalidraw").then((comp) => setComp(comp.default));
});
return <>{Comp && <Comp />}</>;
}
```
The `types` are available at `@excalidraw/excalidraw/types`, you can view [example for typescript](https://codesandbox.io/s/excalidraw-types-9h2dm)
#### In Browser
To use it in a browser directly:
For development use :point_down:
```js
<script
type="text/javascript"
src="https://unpkg.com/@excalidraw/excalidraw@0.6.0/dist/excalidraw.development.js"
></script>
```
For production use :point_down:
```js
<script
type="text/javascript"
src="https://unpkg.com/@excalidraw/excalidraw@0.6.0/dist/excalidraw.production.min.js"
></script>
```
You will need to make sure `react`, `react-dom` is available as shown in the below example. For prod please use the production versions of `react`, `react-dom`.
<details><summary><strong>View Example</strong></summary>
```html
<!DOCTYPE html>
<html>
<head>
<title>Excalidraw in browser</title>
<meta charset="UTF-8" />
<script src="https://unpkg.com/react@16.14.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@16.13.1/umd/react-dom.development.js"></script>
<script
type="text/javascript"
src="https://unpkg.com/@excalidraw/excalidraw@0.6.0/dist/excalidraw.development.js"
></script>
</head>
<body>
<div class="container">
<h1>Excalidraw Embed Example</h1>
<div id="app"></div>
</div>
<script type="text/javascript" src="src/index.js"></script>
</body>
</html>
```
```js
/*eslint-disable */
import "./styles.css";
import InitialData from "./initialData";
const App = () => {
const excalidrawRef = React.useRef(null);
const [viewModeEnabled, setViewModeEnabled] = React.useState(false);
const [zenModeEnabled, setZenModeEnabled] = React.useState(false);
const [gridModeEnabled, setGridModeEnabled] = React.useState(false);
const updateScene = () => {
const sceneData = {
elements: [
{
type: "rectangle",
version: 141,
versionNonce: 361174001,
isDeleted: false,
id: "oDVXy8D6rom3H1-LLH2-f",
fillStyle: "hachure",
strokeWidth: 1,
strokeStyle: "solid",
roughness: 1,
opacity: 100,
angle: 0,
x: 100.50390625,
y: 93.67578125,
strokeColor: "#c92a2a",
backgroundColor: "transparent",
width: 186.47265625,
height: 141.9765625,
seed: 1968410350,
groupIds: [],
},
],
appState: {
viewBackgroundColor: "#edf2ff",
},
};
excalidrawRef.current.updateScene(sceneData);
};
return React.createElement(
React.Fragment,
null,
React.createElement(
"div",
{ className: "button-wrapper" },
React.createElement(
"button",
{
className: "update-scene",
onClick: updateScene,
},
"Update Scene",
),
React.createElement(
"button",
{
className: "reset-scene",
onClick: () => excalidrawRef.current.resetScene(),
},
"Reset Scene",
),
React.createElement(
"label",
null,
React.createElement("input", {
type: "checkbox",
checked: viewModeEnabled,
onChange: () => setViewModeEnabled(!viewModeEnabled),
}),
"View mode",
),
React.createElement(
"label",
null,
React.createElement("input", {
type: "checkbox",
checked: zenModeEnabled,
onChange: () => setZenModeEnabled(!zenModeEnabled),
}),
"Zen mode",
),
React.createElement(
"label",
null,
React.createElement("input", {
type: "checkbox",
checked: gridModeEnabled,
onChange: () => setGridModeEnabled(!gridModeEnabled),
}),
"Grid mode",
),
),
React.createElement(
"div",
{
className: "excalidraw-wrapper",
ref: excalidrawWrapperRef,
},
React.createElement(Excalidraw.default, {
initialData: InitialData,
onChange: (elements, state) =>
console.log("Elements :", elements, "State : ", state),
onPointerUpdate: (payload) => console.log(payload),
onCollabButtonClick: () => window.alert("You clicked on collab button"),
viewModeEnabled: viewModeEnabled,
zenModeEnabled: zenModeEnabled,
gridModeEnabled: gridModeEnabled,
}),
),
);
};
const excalidrawWrapper = document.getElementById("app");
ReactDOM.render(React.createElement(App), excalidrawWrapper);
```
To view the full example visit :point_down:
[![Edit excalidraw-in-browser](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/excalidraw-in-browser-tlqom?fontsize=14&hidenavigation=1&theme=dark)
</details>
### Props
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| [`onChange`](#onChange) | Function | | This callback is triggered whenever the component updates due to any change. This callback will receive the excalidraw elements and the current app state. |
| [`initialData`](#initialData) | <pre>{elements?: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L78">ExcalidrawElement[]</a>, appState?: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L37">AppState<a> } </pre> | null | The initial data with which app loads. |
| [`ref`](#ref) | [`createRef`](https://reactjs.org/docs/refs-and-the-dom.html#creating-refs) or [`callbackRef`](https://reactjs.org/docs/refs-and-the-dom.html#callback-refs) or <pre>{ current: { readyPromise: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/utils.ts#L317">resolvablePromise</a> } }</pre> | | Ref to be passed to Excalidraw |
| [`onCollabButtonClick`](#onCollabButtonClick) | Function | | Callback to be triggered when the collab button is clicked |
| [`isCollaborating`](#isCollaborating) | `boolean` | | This implies if the app is in collaboration mode |
| [`onPointerUpdate`](#onPointerUpdate) | Function | | Callback triggered when mouse pointer is updated. |
| [`onExportToBackend`](#onExportToBackend) | Function | | Callback triggered when link button is clicked on export dialog |
| [`langCode`](#langCode) | string | `en` | Language code string |
| [`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. |
| [`viewModeEnabled`](#viewModeEnabled) | boolean | | This implies if the app is in view mode. |
| [`zenModeEnabled`](#zenModeEnabled) | boolean | | This implies if the zen mode is enabled |
| [`gridModeEnabled`](#gridModeEnabled) | boolean | | This implies if the grid mode is enabled |
| [`libraryReturnUrl`](#libraryReturnUrl) | string | | What URL should [libraries.excalidraw.com](https://libraries.excalidraw.com) be installed to |
| [`theme`](#theme) | `light` or `dark` | | The theme of the Excalidraw component |
| [`name`](#name) | string | | Name of the drawing |
| [`UIOptions`](#UIOptions) | <pre>{ canvasActions: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L208"> CanvasActions<a/> }</pre> | [DEFAULT UI OPTIONS](https://github.com/excalidraw/excalidraw/blob/master/src/constants.ts#L129) | To customise UI options. Currently we support customising [`canvas actions`](#canvasActions) |
### Dimensions of Excalidraw
Excalidraw takes `100%` of `width` and `height` of the containing block so you need to make sure the container in which you are rendering Excalidraw has non zero dimensions (It should have non zero width and height so Excalidraw can match the dimensions of the containing block). This is to make sure you don't have to worry about updating the offsets of dimensions when resizing Excalidraw.
#### `onChange`
Every time component updates, this callback if passed will get triggered and has the below signature.
```js
(excalidrawElements, appState) => void;
```
1.`excalidrawElements`: Array of [excalidrawElements](https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L78) in the scene.
2.`appState`: [AppState](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L37) of the scene
Here you can try saving the data to your backend or local storage for example.
#### `initialData`
This helps to load Excalidraw with `initialData`. It must be an object or a [promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/Promise) which resolves to an object containing the below optional fields.
| Name | Type | Descrption |
| --- | --- | --- |
| `elements` | [ExcalidrawElement[]](https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L78) | The elements with which Excalidraw should be mounted. |
| `appState` | [AppState](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L37) | The App state with which Excalidraw should be mounted. |
| `scrollToContent` | boolean | This attribute implies whether to scroll to the nearest element to center once Excalidraw is mounted. By default, it will not scroll the nearest element to the center. Make sure you pass `initialData.appState.scrollX` and `initialData.appState.scrollY` when `scrollToContent` is false so that scroll positions are retained |
```json
{
"elements": [
{
"type": "rectangle",
"version": 141,
"versionNonce": 361174001,
"isDeleted": false,
"id": "oDVXy8D6rom3H1-LLH2-f",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"x": 100.50390625,
"y": 93.67578125,
"strokeColor": "#000000",
"backgroundColor": "transparent",
"width": 186.47265625,
"height": 141.9765625,
"seed": 1968410350,
"groupIds": []
}
],
"appState": { "zenModeEnabled": true, "viewBackgroundColor": "#AFEEEE" }
}
```
You might want to use this when you want to load excalidraw with some initial elements and app state.
#### `ref`
You can pass a `ref` when you want to access some excalidraw APIs. We expose the below APIs:
| API | signature | Usage |
| --- | --- | --- |
| ready | `boolean` | This is set to true once Excalidraw is rendered |
| readyPromise | [resolvablePromise](https://github.com/excalidraw/excalidraw/blob/master/src/utils.ts#L317) | This promise will be resolved with the api once excalidraw has rendered. This will be helpful when you want do some action on the host app once this promise resolves. For this to work you will have to pass ref as shown [here](#readyPromise) |
| updateScene | <pre>(<a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L192">sceneData</a>)) => void </pre> | updates the scene with the sceneData |
| resetScene | `({ resetLoadingState: boolean }) => void` | Resets the scene. If `resetLoadingState` is passed as true then it will also force set the loading state to false. |
| getSceneElementsIncludingDeleted | <pre> () => <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L78">ExcalidrawElement[]</a></pre> | Returns all the elements including the deleted in the scene |
| getSceneElements | <pre> () => <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L78">ExcalidrawElement[]</a></pre> | Returns all the elements excluding the deleted in the scene |
| getAppState | <pre> () => <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L37">AppState</a></pre> | Returns current appState |
| history | `{ clear: () => void }` | This is the history API. `history.clear()` will clear the history |
| setScrollToContent | <pre> (<a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L78">ExcalidrawElement[]</a>) => void </pre> | Scroll to the nearest element to center |
| setCanvasOffsets | `() => void` | Updates the offsets for the Excalidraw component so that the coordinates are computed correctly (for example the cursor position). You don't have to call this when the position is changed on page scroll or when the excalidraw container resizes (we handle that ourselves). For any other cases if the position of excalidraw is updated (example due to scroll on parent container and not page scroll) you should call this API. |
| importLibrary | `(url: string, token?: string) => void` | Imports library from given URL. You should call this on `hashchange`, passing the `addLibrary` value if you detect it. Optionally pass a CSRF `token` to skip prompting during installation (retrievable via `token` key from the url coming from [https://libraries.excalidraw.com](https://libraries.excalidraw.com/)). |
| setToastMessage | `(message: string) => void` | This API can be used to show the toast with custom message. |
#### `readyPromise`
<pre>
const excalidrawRef = { current: { readyPromise: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/utils.ts#L317">resolvablePromise</a>}}
</pre>
#### `onCollabButtonClick`
This callback is triggered when clicked on the collab button in excalidraw. If not supplied, the collab dialog button is not rendered.
#### `isCollaborating`
This prop indicates if the app is in collaboration mode.
#### `onPointerUpdate`
This callback is triggered when mouse pointer is updated.
```js
({ x, y }, button, pointersMap}) => void;
```
1.`{x, y}`: Pointer coordinates
2.`button`: The position of the button. This will be one of `["down", "up"]`
3.`pointersMap`: [`pointers map`](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L131) of the scene
#### `onExportToBackend`
This callback is triggered when the shareable-link button is clicked in the export dialog. The link button will only be shown if this callback is passed.
```js
(exportedElements, appState, canvas) => void
```
1. `exportedElements`: An array of [non deleted elements](https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L87) which needs to be exported.
2. `appState`: [AppState](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L37) of the scene.
3. `canvas`: The `HTMLCanvasElement` of the scene.
#### `langCode`
Determines the language of the UI. It should be one of the [available language codes](https://github.com/excalidraw/excalidraw/blob/master/src/i18n.ts#L14). Defaults to `en` (English). We also export default language and supported languages which you can import as shown below.
```js
import { defaultLang, languages } from "@excalidraw/excalidraw";
```
| name | type |
| --- | --- |
| defaultLang | string |
| languages | [Language[]](https://github.com/excalidraw/excalidraw/blob/master/src/i18n.ts#L8) |
#### `renderFooter`
A function that renders (returns JSX) custom UI footer. For example, you can use this to render a language picker that was previously being rendered by Excalidraw itself (for now, you'll need to implement your own language picker).
#### `renderCustomStats`
A function that can be used to render custom stats (returns JSX) in the nerd stats dialog. For example you can use this prop to render the size of the elements in the storage.
#### `viewModeEnabled`
This prop indicates whether the app is in `view mode`. When supplied, the value takes precedence over `intialData.appState.viewModeEnabled`, the `view mode` will be fully controlled by the host app, and users won't be able to toggle it from within the app.
#### `zenModeEnabled`
This prop indicates whether the app is in `zen mode`. When supplied, the value takes precedence over `intialData.appState.zenModeEnabled`, the `zen mode` will be fully controlled by the host app, and users won't be able to toggle it from within the app.
#### `gridModeEnabled`
This prop indicates whether the shows the grid. When supplied, the value takes precedence over `intialData.appState.gridModeEnabled`, the grid will be fully controlled by the host app, and users won't be able to toggle it from within the app.
#### `libraryReturnUrl`
If supplied, this URL will be used when user tries to install a library from [libraries.excalidraw.com](https://libraries.excalidraw.com). Defaults to `window.location.origin + window.location.pathname`. To install the libraries in the same tab from which it was opened, you need to set `window.name` (to any alphanumeric string) — if it's not set it will open in a new tab.
#### `theme`
This prop controls Excalidraw's theme. When supplied, the value takes precedence over `intialData.appState.theme`, the theme will be fully controlled by the host app, and users won't be able to toggle it from within the app.
#### `name`
This prop sets the name of the drawing which will be used when exporting the drawing. When supplied, the value takes precedence over `intialData.appState.name`, the `name` will be fully controlled by host app and the users won't be able to edit from within Excalidraw.
### `UIOptions`
This prop can be used to customise UI of Excalidraw. Currently we support customising only [`canvasActions`](#canvasActions). It accepts the below parameters
<pre>
{ canvasActions: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L208"> CanvasActions<a/> }
</pre>
#### canvasActions
| Attribute | Type | Default | Description |
| --- | --- | --- | --- |
| `changeViewBackgroundColor` | boolean | true | Implies whether to show `Background color picker` |
| `clearCanvas` | boolean | true | Implies whether to show `Clear canvas button` |
| `export` | boolean | true | Implies whether to show `Export button` |
| `loadScene` | boolean | true | Implies whether to show `Load button` |
| `saveAsScene` | boolean | true | Implies whether to show `Save as button` |
| `saveScene` | boolean | true | Implies whether to show `Save button` |
| `theme` | boolean | true | Implies whether to show `Theme toggle` |
### Does it support collaboration ?
No Excalidraw package doesn't come with collaboration, since this would have different implementations on the consumer so we expose the API's which you can use to communicate with Excalidraw as mentioned above. If you are interested in understanding how Excalidraw does it you can check it [here](https://github.com/excalidraw/excalidraw/blob/master/src/excalidraw-app/index.tsx).
### Extra API's
#### `getSceneVersion`
**How to use**
<pre>
import { getSceneVersion } from "@excalidraw/excalidraw";
getSceneVersion(elements: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L78">ExcalidrawElement[]</a>)
</pre>
This function returns the current scene version.
#### `getSyncableElements`
**_Signature_**
<pre>
getSyncableElements(elements: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L78">ExcalidrawElement[]</a>):<a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L78">ExcalidrawElement[]</a>
</pre>
**How to use**
```js
import { getSyncableElements } from "@excalidraw/excalidraw";
```
This function returns all the deleted elements of the scene.
#### `getElementMap`
**_Signature_**
<pre>
getElementsMap(elements: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L78">ExcalidrawElement[]</a>): {[id: string]: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L78">ExcalidrawElement</a>}
</pre>
**How to use**
```js
import { getElementsMap } from "@excalidraw/excalidraw";
```
This function returns an object where each element is mapped to its id.
### Restore utilities
#### `restoreAppState`
**_Signature_**
<pre>
restoreAppState(appState: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L17">ImportedDataState["appState"]</a>, localAppState: Partial<<a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L37">AppState</a>> | null): <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L37">AppState</a>
</pre>
**_How to use_**
```js
import { restoreAppState } from "@excalidraw/excalidraw";
```
This function will make sure all the keys have appropriate values in [appState](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L37) and if any key is missing, it will be set to default value. If you pass `localAppState`, `localAppState` value will be preferred over the `appState` passed in params.
#### `restoreElements`
**_Signature_**
<pre>
restoreElements(elements: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L16">ImportedDataState["elements"]</a>): <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L78">ExcalidrawElement[]</a>
</pre>
**_How to use_**
```js
import { restoreElements } from "@excalidraw/excalidraw";
```
This function will make sure all properties of element is correctly set and if any attribute is missing, it will be set to default value.
#### `restore`
**_Signature_**
<pre>
restoreElements(data: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L12">ImportedDataState</a>): <a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L4">DataState</a>
</pre>
**_How to use_**
```js
import { restore } from "@excalidraw/excalidraw";
```
This function makes sure elements and state is set to appropriate values and set to default value if not present. It is combination of [restoreElements](#restoreElements) and [restoreAppState](#restoreAppState)
### Export utilities
#### `exportToCanvas`
**_Signature_**
<pre
>exportToCanvas({
elements,
appState
getDimensions,
}: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/packages/utils.ts#L10">ExportOpts</a>
</pre>
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| elements | [Excalidraw Element []](https://github.com/excalidraw/excalidraw/blob/master/src/element/types) | | The elements to be exported to canvas |
| appState | [AppState](https://github.com/excalidraw/excalidraw/blob/master/src/packages/utils.ts#L12) | [defaultAppState](https://github.com/excalidraw/excalidraw/blob/master/src/appState.ts#L11) | The app state of the scene |
| getDimensions | `(width: number, height: number) => {width: number, height: number, scale: number)` | `(width, height) => ({ width, height, scale: 1 })` | A function which returns the width, height and scale with which canvas is to be exported. |
**How to use**
```js
import { exportToCanvas } from "@excalidraw/excalidraw";
```
This function returns the canvas with the exported elements, appState and dimensions.
#### `exportToBlob`
**_Signature_**
<pre>
exportToBlob(
opts: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/packages/utils.ts#L10">ExportOpts</a> & {
mimeType?: string,
quality?: number;
})
</pre>
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| opts | | | This param is passed to `exportToCanvas`. You can refer to [`exportToCanvas`](#exportToCanvas) |
| mimeType | string | "image/png" | Indicates the image format |
| quality | number | 0.92 | A value between 0 and 1 indicating the [image quality](https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob#parameters). Applies only to `image/jpeg`/`image/webp` MIME types. |
**How to use**
```js
import { exportToBlob } from "@excalidraw/excalidraw";
```
Returns a promise which resolves with a [blob](https://developer.mozilla.org/en-US/docs/Web/API/Blob). It internally uses [canvas.ToBlob](https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob).
#### `exportToSvg`
**_Signature_**
<pre>
exportToSvg({
elements: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L78">ExcalidrawElement[]</a>,
appState: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L42">AppState</a>,
exportPadding?: number,
metadata?: string,
}
</pre>
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| elements | [Excalidraw Element []](https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L78) | | The elements to exported as svg |
| appState | [AppState](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L42) | [defaultAppState](https://github.com/excalidraw/excalidraw/blob/master/src/appState.ts#L11) | The app state of the scene |
| exportPadding | number | 10 | The padding to be added on canvas |
| metadata | string | '' | The metadata to be embedded in svg |
This function returns a svg with the exported elements.
##### Additional attributes of appState for `export\*` APIs
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| exportBackground | boolean | true | Indicates whether background should be exported |
| viewBackgroundColor | string | #fff | The default background color |
| shouldAddWatermark | boolean | false | Indicates whether watermark should be exported |
| exportWithDarkMode | boolean | false | Indicates whether to export with dark mode |

View File

@ -10,11 +10,10 @@ import "../../css/styles.scss";
import { ExcalidrawAPIRefValue, ExcalidrawProps } from "../../types"; import { ExcalidrawAPIRefValue, ExcalidrawProps } from "../../types";
import { IsMobileProvider } from "../../is-mobile"; import { IsMobileProvider } from "../../is-mobile";
import { defaultLang } from "../../i18n"; import { defaultLang } from "../../i18n";
import { DEFAULT_UI_OPTIONS } from "../../constants";
const Excalidraw = (props: ExcalidrawProps) => { const Excalidraw = (props: ExcalidrawProps) => {
const { const {
width,
height,
onChange, onChange,
initialData, initialData,
excalidrawRef, excalidrawRef,
@ -30,8 +29,18 @@ const Excalidraw = (props: ExcalidrawProps) => {
libraryReturnUrl, libraryReturnUrl,
theme, theme,
name, name,
renderCustomStats,
} = props; } = props;
const canvasActions = props.UIOptions?.canvasActions;
const UIOptions = {
canvasActions: {
...DEFAULT_UI_OPTIONS.canvasActions,
...canvasActions,
},
};
useEffect(() => { useEffect(() => {
// Block pinch-zooming on iOS outside of the content area // Block pinch-zooming on iOS outside of the content area
const handleTouchMove = (event: TouchEvent) => { const handleTouchMove = (event: TouchEvent) => {
@ -54,8 +63,6 @@ const Excalidraw = (props: ExcalidrawProps) => {
<InitializeApp langCode={langCode}> <InitializeApp langCode={langCode}>
<IsMobileProvider> <IsMobileProvider>
<App <App
width={width}
height={height}
onChange={onChange} onChange={onChange}
initialData={initialData} initialData={initialData}
excalidrawRef={excalidrawRef} excalidrawRef={excalidrawRef}
@ -71,6 +78,8 @@ const Excalidraw = (props: ExcalidrawProps) => {
libraryReturnUrl={libraryReturnUrl} libraryReturnUrl={libraryReturnUrl}
theme={theme} theme={theme}
name={name} name={name}
renderCustomStats={renderCustomStats}
UIOptions={UIOptions}
/> />
</IsMobileProvider> </IsMobileProvider>
</InitializeApp> </InitializeApp>
@ -96,6 +105,7 @@ const areEqual = (
Excalidraw.defaultProps = { Excalidraw.defaultProps = {
lanCode: defaultLang.code, lanCode: defaultLang.code,
UIOptions: DEFAULT_UI_OPTIONS,
}; };
const forwardedRefComp = forwardRef< const forwardedRefComp = forwardRef<

View File

@ -0,0 +1,5 @@
if (process.env.NODE_ENV === "production") {
module.exports = require("./dist/excalidraw.production.min.js");
} else {
module.exports = require("./dist/excalidraw.development.js");
}

View File

@ -1,9 +1,11 @@
{ {
"name": "@excalidraw/excalidraw", "name": "@excalidraw/excalidraw",
"version": "0.5.0", "version": "0.5.0",
"main": "dist/excalidraw.min.js", "main": "main.js",
"types": "types/packages/excalidraw/index.d.ts",
"files": [ "files": [
"dist/*" "dist/*",
"types/*"
], ],
"publishConfig": { "publishConfig": {
"access": "public" "access": "public"
@ -41,32 +43,34 @@
"react-dom": "^17.0.1" "react-dom": "^17.0.1"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "7.13.10", "@babel/core": "7.13.14",
"@babel/plugin-transform-arrow-functions": "7.13.0", "@babel/plugin-transform-arrow-functions": "7.13.0",
"@babel/plugin-transform-async-to-generator": "7.13.0", "@babel/plugin-transform-async-to-generator": "7.13.0",
"@babel/plugin-transform-runtime": "7.13.10", "@babel/plugin-transform-runtime": "7.13.10",
"@babel/plugin-transform-typescript": "7.13.0", "@babel/plugin-transform-typescript": "7.13.0",
"@babel/preset-env": "7.13.10", "@babel/preset-env": "7.13.12",
"@babel/preset-react": "7.12.13", "@babel/preset-react": "7.13.13",
"@babel/preset-typescript": "7.13.0", "@babel/preset-typescript": "7.13.0",
"babel-loader": "8.2.2", "babel-loader": "8.2.2",
"babel-plugin-transform-class-properties": "6.24.1", "babel-plugin-transform-class-properties": "6.24.1",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"css-loader": "5.1.3", "css-loader": "5.2.0",
"file-loader": "6.2.0", "file-loader": "6.2.0",
"mini-css-extract-plugin": "1.3.9", "mini-css-extract-plugin": "1.4.0",
"sass-loader": "11.0.1", "sass-loader": "11.0.1",
"terser-webpack-plugin": "5.1.1", "terser-webpack-plugin": "5.1.1",
"ts-loader": "8.0.18", "ts-loader": "8.1.0",
"webpack": "5.27.1", "typescript": "4.2.3",
"webpack": "5.30.0",
"webpack-bundle-analyzer": "4.4.0", "webpack-bundle-analyzer": "4.4.0",
"webpack-cli": "4.5.0" "webpack-cli": "4.6.0"
}, },
"bugs": "https://github.com/excalidraw/excalidraw/issues", "bugs": "https://github.com/excalidraw/excalidraw/issues",
"repository": "https://github.com/excalidraw/excalidraw", "repository": "https://github.com/excalidraw/excalidraw",
"homepage": "https://github.com/excalidraw/excalidraw/tree/master/src/packages/excalidraw", "homepage": "https://github.com/excalidraw/excalidraw/tree/master/src/packages/excalidraw",
"scripts": { "scripts": {
"build:umd": "cross-env NODE_ENV=production webpack --config webpack.prod.config.js", "gen:types": "tsc --project ../../../tsconfig-types.json",
"build:umd": "cross-env NODE_ENV=production webpack --config webpack.prod.config.js && cross-env NODE_ENV=development webpack --config webpack.dev.config.js && yarn gen:types",
"build:umd:withAnalyzer": "cross-env NODE_ENV=production ANALYZER=true webpack --config webpack.prod.config.js", "build:umd:withAnalyzer": "cross-env NODE_ENV=production ANALYZER=true webpack --config webpack.prod.config.js",
"pack": "yarn build:umd && yarn pack" "pack": "yarn build:umd && yarn pack"
} }

View File

@ -0,0 +1,81 @@
const path = require("path");
const webpack = require("webpack");
module.exports = {
mode: "development",
devtool: false,
entry: {
"excalidraw.development": "./entry.js",
},
output: {
path: path.resolve(__dirname, "dist"),
library: "Excalidraw",
libraryTarget: "umd",
filename: "[name].js",
chunkFilename: "excalidraw-assets-dev/[name]-[contenthash].js",
publicPath: "",
},
resolve: {
extensions: [".js", ".ts", ".tsx", ".css", ".scss"],
},
module: {
rules: [
{
test: /\.(sa|sc|c)ss$/,
exclude: /node_modules/,
use: ["style-loader", { loader: "css-loader" }, "sass-loader"],
},
{
test: /\.(ts|tsx|js|jsx|mjs)$/,
exclude: /node_modules/,
use: [
{
loader: "ts-loader",
options: {
transpileOnly: true,
configFile: path.resolve(__dirname, "../tsconfig.dev.json"),
},
},
],
},
{
test: /\.(woff|woff2|eot|ttf|otf)$/,
use: [
{
loader: "file-loader",
options: {
name: "[name].[ext]",
outputPath: "excalidraw-assets-dev",
},
},
],
},
],
},
optimization: {
splitChunks: {
chunks: "async",
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/]/,
name: "vendor",
},
},
},
},
plugins: [new webpack.EvalSourceMapDevToolPlugin({ exclude: /vendor/ })],
externals: {
react: {
root: "React",
commonjs2: "react",
commonjs: "react",
amd: "react",
},
"react-dom": {
root: "ReactDOM",
commonjs2: "react-dom",
commonjs: "react-dom",
amd: "react-dom",
},
},
};

View File

@ -6,7 +6,7 @@ const BundleAnalyzerPlugin = require("webpack-bundle-analyzer")
module.exports = { module.exports = {
mode: "production", mode: "production",
entry: { entry: {
"excalidraw.min": "./entry.js", "excalidraw.production.min": "./entry.js",
}, },
output: { output: {
path: path.resolve(__dirname, "dist"), path: path.resolve(__dirname, "dist"),
@ -24,7 +24,13 @@ module.exports = {
{ {
test: /\.(sa|sc|c)ss$/, test: /\.(sa|sc|c)ss$/,
exclude: /node_modules/, exclude: /node_modules/,
use: ["style-loader", { loader: "css-loader" }, "sass-loader"], use: [
"style-loader",
{
loader: "css-loader",
},
"sass-loader",
],
}, },
{ {
test: /\.(ts|tsx|js|jsx|mjs)$/, test: /\.(ts|tsx|js|jsx|mjs)$/,

View File

@ -9,34 +9,33 @@
dependencies: dependencies:
"@babel/highlight" "^7.12.13" "@babel/highlight" "^7.12.13"
"@babel/compat-data@^7.13.0", "@babel/compat-data@^7.13.8": "@babel/compat-data@^7.13.0", "@babel/compat-data@^7.13.12", "@babel/compat-data@^7.13.8":
version "7.13.8" version "7.13.12"
resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.13.8.tgz#5b783b9808f15cef71547f1b691f34f8ff6003a6" resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.13.12.tgz#a8a5ccac19c200f9dd49624cac6e19d7be1236a1"
integrity sha512-EaI33z19T4qN3xLXsGf48M2cDqa6ei9tPZlfLdb2HC+e/cFtREiRd8hdSqDbwdLB0/+gLwqJmCYASH0z2bUdog== integrity sha512-3eJJ841uKxeV8dcN/2yGEUy+RfgQspPEgQat85umsE1rotuquQ2AbIub4S6j7c50a2d+4myc+zSlnXeIHrOnhQ==
"@babel/core@7.13.10": "@babel/core@7.13.14":
version "7.13.10" version "7.13.14"
resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.13.10.tgz#07de050bbd8193fcd8a3c27918c0890613a94559" resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.13.14.tgz#8e46ebbaca460a63497c797e574038ab04ae6d06"
integrity sha512-bfIYcT0BdKeAZrovpMqX2Mx5NrgAckGbwT982AkdS5GNfn3KMGiprlBAtmBcFZRUmpaufS6WZFP8trvx8ptFDw== integrity sha512-wZso/vyF4ki0l0znlgM4inxbdrUvCb+cVz8grxDq+6C9k6qbqoIJteQOKicaKjCipU3ISV+XedCqpL2RJJVehA==
dependencies: dependencies:
"@babel/code-frame" "^7.12.13" "@babel/code-frame" "^7.12.13"
"@babel/generator" "^7.13.9" "@babel/generator" "^7.13.9"
"@babel/helper-compilation-targets" "^7.13.10" "@babel/helper-compilation-targets" "^7.13.13"
"@babel/helper-module-transforms" "^7.13.0" "@babel/helper-module-transforms" "^7.13.14"
"@babel/helpers" "^7.13.10" "@babel/helpers" "^7.13.10"
"@babel/parser" "^7.13.10" "@babel/parser" "^7.13.13"
"@babel/template" "^7.12.13" "@babel/template" "^7.12.13"
"@babel/traverse" "^7.13.0" "@babel/traverse" "^7.13.13"
"@babel/types" "^7.13.0" "@babel/types" "^7.13.14"
convert-source-map "^1.7.0" convert-source-map "^1.7.0"
debug "^4.1.0" debug "^4.1.0"
gensync "^1.0.0-beta.2" gensync "^1.0.0-beta.2"
json5 "^2.1.2" json5 "^2.1.2"
lodash "^4.17.19"
semver "^6.3.0" semver "^6.3.0"
source-map "^0.5.0" source-map "^0.5.0"
"@babel/generator@^7.13.0", "@babel/generator@^7.13.9": "@babel/generator@^7.13.9":
version "7.13.9" version "7.13.9"
resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.13.9.tgz#3a7aa96f9efb8e2be42d38d80e2ceb4c64d8de39" resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.13.9.tgz#3a7aa96f9efb8e2be42d38d80e2ceb4c64d8de39"
integrity sha512-mHOOmY0Axl/JCTkxTU6Lf5sWOg/v8nUa+Xkt4zMTftX0wqmb6Sh7J8gvcehBw7q0AhrhAR+FDacKjCZ2X8K+Sw== integrity sha512-mHOOmY0Axl/JCTkxTU6Lf5sWOg/v8nUa+Xkt4zMTftX0wqmb6Sh7J8gvcehBw7q0AhrhAR+FDacKjCZ2X8K+Sw==
@ -60,12 +59,12 @@
"@babel/helper-explode-assignable-expression" "^7.12.13" "@babel/helper-explode-assignable-expression" "^7.12.13"
"@babel/types" "^7.12.13" "@babel/types" "^7.12.13"
"@babel/helper-compilation-targets@^7.13.0", "@babel/helper-compilation-targets@^7.13.10", "@babel/helper-compilation-targets@^7.13.8": "@babel/helper-compilation-targets@^7.13.0", "@babel/helper-compilation-targets@^7.13.10", "@babel/helper-compilation-targets@^7.13.13", "@babel/helper-compilation-targets@^7.13.8":
version "7.13.10" version "7.13.13"
resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.13.10.tgz#1310a1678cb8427c07a753750da4f8ce442bdd0c" resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.13.13.tgz#2b2972a0926474853f41e4adbc69338f520600e5"
integrity sha512-/Xju7Qg1GQO4mHZ/Kcs6Au7gfafgZnwm+a7sy/ow/tV1sHeraRUHbjdat8/UvDor4Tez+siGKDk6zIKtCPKVJA== integrity sha512-q1kcdHNZehBwD9jYPh3WyXcsFERi39X4I59I3NadciWtNDyZ6x+GboOxncFK0kXlKIv6BJm5acncehXWUjWQMQ==
dependencies: dependencies:
"@babel/compat-data" "^7.13.8" "@babel/compat-data" "^7.13.12"
"@babel/helper-validator-option" "^7.12.17" "@babel/helper-validator-option" "^7.12.17"
browserslist "^4.14.5" browserslist "^4.14.5"
semver "^6.3.0" semver "^6.3.0"
@ -148,6 +147,13 @@
dependencies: dependencies:
"@babel/types" "^7.13.0" "@babel/types" "^7.13.0"
"@babel/helper-member-expression-to-functions@^7.13.12":
version "7.13.12"
resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.13.12.tgz#dfe368f26d426a07299d8d6513821768216e6d72"
integrity sha512-48ql1CLL59aKbU94Y88Xgb2VFy7a95ykGRbJJaaVv+LX5U8wFpLfiGXJJGUozsmA1oEh/o5Bp60Voq7ACyA/Sw==
dependencies:
"@babel/types" "^7.13.12"
"@babel/helper-module-imports@^7.12.13": "@babel/helper-module-imports@^7.12.13":
version "7.12.13" version "7.12.13"
resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.12.13.tgz#ec67e4404f41750463e455cc3203f6a32e93fcb0" resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.12.13.tgz#ec67e4404f41750463e455cc3203f6a32e93fcb0"
@ -155,20 +161,26 @@
dependencies: dependencies:
"@babel/types" "^7.12.13" "@babel/types" "^7.12.13"
"@babel/helper-module-transforms@^7.13.0": "@babel/helper-module-imports@^7.13.12":
version "7.13.0" version "7.13.12"
resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.13.0.tgz#42eb4bd8eea68bab46751212c357bfed8b40f6f1" resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.13.12.tgz#c6a369a6f3621cb25da014078684da9196b61977"
integrity sha512-Ls8/VBwH577+pw7Ku1QkUWIyRRNHpYlts7+qSqBBFCW3I8QteB9DxfcZ5YJpOwH6Ihe/wn8ch7fMGOP1OhEIvw== integrity sha512-4cVvR2/1B693IuOvSI20xqqa/+bl7lqAMR59R4iu39R9aOX8/JoYY1sFaNvUMyMBGnHdwvJgUrzNLoUZxXypxA==
dependencies: dependencies:
"@babel/helper-module-imports" "^7.12.13" "@babel/types" "^7.13.12"
"@babel/helper-replace-supers" "^7.13.0"
"@babel/helper-simple-access" "^7.12.13" "@babel/helper-module-transforms@^7.13.0", "@babel/helper-module-transforms@^7.13.14":
version "7.13.14"
resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.13.14.tgz#e600652ba48ccb1641775413cb32cfa4e8b495ef"
integrity sha512-QuU/OJ0iAOSIatyVZmfqB0lbkVP0kDRiKj34xy+QNsnVZi/PA6BoSoreeqnxxa9EHFAIL0R9XOaAR/G9WlIy5g==
dependencies:
"@babel/helper-module-imports" "^7.13.12"
"@babel/helper-replace-supers" "^7.13.12"
"@babel/helper-simple-access" "^7.13.12"
"@babel/helper-split-export-declaration" "^7.12.13" "@babel/helper-split-export-declaration" "^7.12.13"
"@babel/helper-validator-identifier" "^7.12.11" "@babel/helper-validator-identifier" "^7.12.11"
"@babel/template" "^7.12.13" "@babel/template" "^7.12.13"
"@babel/traverse" "^7.13.0" "@babel/traverse" "^7.13.13"
"@babel/types" "^7.13.0" "@babel/types" "^7.13.14"
lodash "^4.17.19"
"@babel/helper-optimise-call-expression@^7.12.13": "@babel/helper-optimise-call-expression@^7.12.13":
version "7.12.13" version "7.12.13"
@ -211,6 +223,16 @@
"@babel/traverse" "^7.13.0" "@babel/traverse" "^7.13.0"
"@babel/types" "^7.13.0" "@babel/types" "^7.13.0"
"@babel/helper-replace-supers@^7.13.12":
version "7.13.12"
resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.13.12.tgz#6442f4c1ad912502481a564a7386de0c77ff3804"
integrity sha512-Gz1eiX+4yDO8mT+heB94aLVNCL+rbuT2xy4YfyNqu8F+OI6vMvJK891qGBTqL9Uc8wxEvRW92Id6G7sDen3fFw==
dependencies:
"@babel/helper-member-expression-to-functions" "^7.13.12"
"@babel/helper-optimise-call-expression" "^7.12.13"
"@babel/traverse" "^7.13.0"
"@babel/types" "^7.13.12"
"@babel/helper-simple-access@^7.12.13": "@babel/helper-simple-access@^7.12.13":
version "7.12.13" version "7.12.13"
resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.12.13.tgz#8478bcc5cacf6aa1672b251c1d2dde5ccd61a6c4" resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.12.13.tgz#8478bcc5cacf6aa1672b251c1d2dde5ccd61a6c4"
@ -218,6 +240,13 @@
dependencies: dependencies:
"@babel/types" "^7.12.13" "@babel/types" "^7.12.13"
"@babel/helper-simple-access@^7.13.12":
version "7.13.12"
resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.13.12.tgz#dd6c538afb61819d205a012c31792a39c7a5eaf6"
integrity sha512-7FEjbrx5SL9cWvXioDbnlYTppcZGuCY6ow3/D5vMggb2Ywgu4dMrpTJX0JdQAIcRRUElOIxF3yEooa9gUb9ZbA==
dependencies:
"@babel/types" "^7.13.12"
"@babel/helper-skip-transparent-expression-wrappers@^7.12.1": "@babel/helper-skip-transparent-expression-wrappers@^7.12.1":
version "7.12.1" version "7.12.1"
resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.12.1.tgz#462dc63a7e435ade8468385c63d2b84cce4b3cbf" resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.12.1.tgz#462dc63a7e435ade8468385c63d2b84cce4b3cbf"
@ -270,10 +299,19 @@
chalk "^2.0.0" chalk "^2.0.0"
js-tokens "^4.0.0" js-tokens "^4.0.0"
"@babel/parser@^7.12.13", "@babel/parser@^7.13.0", "@babel/parser@^7.13.10": "@babel/parser@^7.12.13", "@babel/parser@^7.13.13":
version "7.13.10" version "7.13.13"
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.13.10.tgz#8f8f9bf7b3afa3eabd061f7a5bcdf4fec3c48409" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.13.13.tgz#42f03862f4aed50461e543270916b47dd501f0df"
integrity sha512-0s7Mlrw9uTWkYua7xWr99Wpk2bnGa0ANleKfksYAES8LpWH4gW1OUr42vqKNf0us5UQNfru2wPqMqRITzq/SIQ== integrity sha512-OhsyMrqygfk5v8HmWwOzlYjJrtLaFhF34MrfG/Z73DgYCI6ojNUTUp2TYbtnjo8PegeJp12eamsNettCQjKjVw==
"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.13.12":
version "7.13.12"
resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.13.12.tgz#a3484d84d0b549f3fc916b99ee4783f26fabad2a"
integrity sha512-d0u3zWKcoZf379fOeJdr1a5WPDny4aOFZ6hlfKivgK0LY7ZxNfoaHL2fWwdGtHyVvra38FC+HVYkO+byfSA8AQ==
dependencies:
"@babel/helper-plugin-utils" "^7.13.0"
"@babel/helper-skip-transparent-expression-wrappers" "^7.12.1"
"@babel/plugin-proposal-optional-chaining" "^7.13.12"
"@babel/plugin-proposal-async-generator-functions@^7.13.8": "@babel/plugin-proposal-async-generator-functions@^7.13.8":
version "7.13.8" version "7.13.8"
@ -359,10 +397,10 @@
"@babel/helper-plugin-utils" "^7.13.0" "@babel/helper-plugin-utils" "^7.13.0"
"@babel/plugin-syntax-optional-catch-binding" "^7.8.3" "@babel/plugin-syntax-optional-catch-binding" "^7.8.3"
"@babel/plugin-proposal-optional-chaining@^7.13.8": "@babel/plugin-proposal-optional-chaining@^7.13.12":
version "7.13.8" version "7.13.12"
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.13.8.tgz#e39df93efe7e7e621841babc197982e140e90756" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.13.12.tgz#ba9feb601d422e0adea6760c2bd6bbb7bfec4866"
integrity sha512-hpbBwbTgd7Cz1QryvwJZRo1U0k1q8uyBmeXOSQUjdg/A2TASkhR/rz7AyqZ/kS8kbpsNA80rOYbxySBJAqmhhQ== integrity sha512-fcEdKOkIB7Tf4IxrgEVeFC4zeJSTr78no9wTdBuZZbqF64kzllU0ybo2zrzm7gUQfxGhBgq4E39oRs8Zx/RMYQ==
dependencies: dependencies:
"@babel/helper-plugin-utils" "^7.13.0" "@babel/helper-plugin-utils" "^7.13.0"
"@babel/helper-skip-transparent-expression-wrappers" "^7.12.1" "@babel/helper-skip-transparent-expression-wrappers" "^7.12.1"
@ -672,23 +710,23 @@
dependencies: dependencies:
"@babel/helper-plugin-utils" "^7.12.13" "@babel/helper-plugin-utils" "^7.12.13"
"@babel/plugin-transform-react-jsx-development@^7.12.12": "@babel/plugin-transform-react-jsx-development@^7.12.17":
version "7.12.16" version "7.12.17"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.12.16.tgz#af187e749d123b54ae49bc7e034057a0c1d4d568" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.12.17.tgz#f510c0fa7cd7234153539f9a362ced41a5ca1447"
integrity sha512-GOp5SkMC4zhHwLbOSYhF+WpIZSf5bGzaKQTT9jWkemJRDM/CE6FtPydXjEYO3pHcna2Zjvg4mQ1lfjOR/4jsaQ== integrity sha512-BPjYV86SVuOaudFhsJR1zjgxxOhJDt6JHNoD48DxWEIxUCAMjV1ys6DYw4SDYZh0b1QsS2vfIA9t/ZsQGsDOUQ==
dependencies: dependencies:
"@babel/plugin-transform-react-jsx" "^7.12.16" "@babel/plugin-transform-react-jsx" "^7.12.17"
"@babel/plugin-transform-react-jsx@^7.12.13", "@babel/plugin-transform-react-jsx@^7.12.16": "@babel/plugin-transform-react-jsx@^7.12.17", "@babel/plugin-transform-react-jsx@^7.13.12":
version "7.12.16" version "7.13.12"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.12.16.tgz#07c341e02a3e4066b00413534f30c42519923230" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.13.12.tgz#1df5dfaf0f4b784b43e96da6f28d630e775f68b3"
integrity sha512-dNu0vAbIk8OkqJfGtYF6ADk6jagoyAl+Ks5aoltbAlfoKv8d6yooi3j+kObeSQaCj9PgN6KMZPB90wWyek5TmQ== integrity sha512-jcEI2UqIcpCqB5U5DRxIl0tQEProI2gcu+g8VTIqxLO5Iidojb4d77q+fwGseCvd8af/lJ9masp4QWzBXFE2xA==
dependencies: dependencies:
"@babel/helper-annotate-as-pure" "^7.12.13" "@babel/helper-annotate-as-pure" "^7.12.13"
"@babel/helper-module-imports" "^7.12.13" "@babel/helper-module-imports" "^7.13.12"
"@babel/helper-plugin-utils" "^7.12.13" "@babel/helper-plugin-utils" "^7.13.0"
"@babel/plugin-syntax-jsx" "^7.12.13" "@babel/plugin-syntax-jsx" "^7.12.13"
"@babel/types" "^7.12.13" "@babel/types" "^7.13.12"
"@babel/plugin-transform-react-pure-annotations@^7.12.1": "@babel/plugin-transform-react-pure-annotations@^7.12.1":
version "7.12.1" version "7.12.1"
@ -784,15 +822,16 @@
"@babel/helper-create-regexp-features-plugin" "^7.12.13" "@babel/helper-create-regexp-features-plugin" "^7.12.13"
"@babel/helper-plugin-utils" "^7.12.13" "@babel/helper-plugin-utils" "^7.12.13"
"@babel/preset-env@7.13.10": "@babel/preset-env@7.13.12":
version "7.13.10" version "7.13.12"
resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.13.10.tgz#b5cde31d5fe77ab2a6ab3d453b59041a1b3a5252" resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.13.12.tgz#6dff470478290582ac282fb77780eadf32480237"
integrity sha512-nOsTScuoRghRtUsRr/c69d042ysfPHcu+KOB4A9aAO9eJYqrkat+LF8G1yp1HD18QiwixT2CisZTr/0b3YZPXQ== integrity sha512-JzElc6jk3Ko6zuZgBtjOd01pf9yYDEIH8BcqVuYIuOkzOwDesoa/Nz4gIo4lBG6K861KTV9TvIgmFuT6ytOaAA==
dependencies: dependencies:
"@babel/compat-data" "^7.13.8" "@babel/compat-data" "^7.13.12"
"@babel/helper-compilation-targets" "^7.13.10" "@babel/helper-compilation-targets" "^7.13.10"
"@babel/helper-plugin-utils" "^7.13.0" "@babel/helper-plugin-utils" "^7.13.0"
"@babel/helper-validator-option" "^7.12.17" "@babel/helper-validator-option" "^7.12.17"
"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining" "^7.13.12"
"@babel/plugin-proposal-async-generator-functions" "^7.13.8" "@babel/plugin-proposal-async-generator-functions" "^7.13.8"
"@babel/plugin-proposal-class-properties" "^7.13.0" "@babel/plugin-proposal-class-properties" "^7.13.0"
"@babel/plugin-proposal-dynamic-import" "^7.13.8" "@babel/plugin-proposal-dynamic-import" "^7.13.8"
@ -803,7 +842,7 @@
"@babel/plugin-proposal-numeric-separator" "^7.12.13" "@babel/plugin-proposal-numeric-separator" "^7.12.13"
"@babel/plugin-proposal-object-rest-spread" "^7.13.8" "@babel/plugin-proposal-object-rest-spread" "^7.13.8"
"@babel/plugin-proposal-optional-catch-binding" "^7.13.8" "@babel/plugin-proposal-optional-catch-binding" "^7.13.8"
"@babel/plugin-proposal-optional-chaining" "^7.13.8" "@babel/plugin-proposal-optional-chaining" "^7.13.12"
"@babel/plugin-proposal-private-methods" "^7.13.0" "@babel/plugin-proposal-private-methods" "^7.13.0"
"@babel/plugin-proposal-unicode-property-regex" "^7.12.13" "@babel/plugin-proposal-unicode-property-regex" "^7.12.13"
"@babel/plugin-syntax-async-generators" "^7.8.4" "@babel/plugin-syntax-async-generators" "^7.8.4"
@ -851,7 +890,7 @@
"@babel/plugin-transform-unicode-escapes" "^7.12.13" "@babel/plugin-transform-unicode-escapes" "^7.12.13"
"@babel/plugin-transform-unicode-regex" "^7.12.13" "@babel/plugin-transform-unicode-regex" "^7.12.13"
"@babel/preset-modules" "^0.1.4" "@babel/preset-modules" "^0.1.4"
"@babel/types" "^7.13.0" "@babel/types" "^7.13.12"
babel-plugin-polyfill-corejs2 "^0.1.4" babel-plugin-polyfill-corejs2 "^0.1.4"
babel-plugin-polyfill-corejs3 "^0.1.3" babel-plugin-polyfill-corejs3 "^0.1.3"
babel-plugin-polyfill-regenerator "^0.1.2" babel-plugin-polyfill-regenerator "^0.1.2"
@ -869,15 +908,16 @@
"@babel/types" "^7.4.4" "@babel/types" "^7.4.4"
esutils "^2.0.2" esutils "^2.0.2"
"@babel/preset-react@7.12.13": "@babel/preset-react@7.13.13":
version "7.12.13" version "7.13.13"
resolved "https://registry.yarnpkg.com/@babel/preset-react/-/preset-react-7.12.13.tgz#5f911b2eb24277fa686820d5bd81cad9a0602a0a" resolved "https://registry.yarnpkg.com/@babel/preset-react/-/preset-react-7.13.13.tgz#fa6895a96c50763fe693f9148568458d5a839761"
integrity sha512-TYM0V9z6Abb6dj1K7i5NrEhA13oS5ujUYQYDfqIBXYHOc2c2VkFgc+q9kyssIyUfy4/hEwqrgSlJ/Qgv8zJLsA== integrity sha512-gx+tDLIE06sRjKJkVtpZ/t3mzCDOnPG+ggHZG9lffUbX8+wC739x20YQc9V35Do6ZAxaUc/HhVHIiOzz5MvDmA==
dependencies: dependencies:
"@babel/helper-plugin-utils" "^7.12.13" "@babel/helper-plugin-utils" "^7.13.0"
"@babel/helper-validator-option" "^7.12.17"
"@babel/plugin-transform-react-display-name" "^7.12.13" "@babel/plugin-transform-react-display-name" "^7.12.13"
"@babel/plugin-transform-react-jsx" "^7.12.13" "@babel/plugin-transform-react-jsx" "^7.13.12"
"@babel/plugin-transform-react-jsx-development" "^7.12.12" "@babel/plugin-transform-react-jsx-development" "^7.12.17"
"@babel/plugin-transform-react-pure-annotations" "^7.12.1" "@babel/plugin-transform-react-pure-annotations" "^7.12.1"
"@babel/preset-typescript@7.13.0": "@babel/preset-typescript@7.13.0":
@ -905,25 +945,24 @@
"@babel/parser" "^7.12.13" "@babel/parser" "^7.12.13"
"@babel/types" "^7.12.13" "@babel/types" "^7.12.13"
"@babel/traverse@^7.12.13", "@babel/traverse@^7.13.0": "@babel/traverse@^7.12.13", "@babel/traverse@^7.13.0", "@babel/traverse@^7.13.13":
version "7.13.0" version "7.13.13"
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.13.0.tgz#6d95752475f86ee7ded06536de309a65fc8966cc" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.13.13.tgz#39aa9c21aab69f74d948a486dd28a2dbdbf5114d"
integrity sha512-xys5xi5JEhzC3RzEmSGrs/b3pJW/o87SypZ+G/PhaE7uqVQNv/jlmVIBXuoh5atqQ434LfXV+sf23Oxj0bchJQ== integrity sha512-CblEcwmXKR6eP43oQGG++0QMTtCjAsa3frUuzHoiIJWpaIIi8dwMyEFUJoXRLxagGqCK+jALRwIO+o3R9p/uUg==
dependencies: dependencies:
"@babel/code-frame" "^7.12.13" "@babel/code-frame" "^7.12.13"
"@babel/generator" "^7.13.0" "@babel/generator" "^7.13.9"
"@babel/helper-function-name" "^7.12.13" "@babel/helper-function-name" "^7.12.13"
"@babel/helper-split-export-declaration" "^7.12.13" "@babel/helper-split-export-declaration" "^7.12.13"
"@babel/parser" "^7.13.0" "@babel/parser" "^7.13.13"
"@babel/types" "^7.13.0" "@babel/types" "^7.13.13"
debug "^4.1.0" debug "^4.1.0"
globals "^11.1.0" globals "^11.1.0"
lodash "^4.17.19"
"@babel/types@^7.12.1", "@babel/types@^7.12.13", "@babel/types@^7.13.0", "@babel/types@^7.4.4": "@babel/types@^7.12.1", "@babel/types@^7.12.13", "@babel/types@^7.13.0", "@babel/types@^7.13.12", "@babel/types@^7.13.13", "@babel/types@^7.13.14", "@babel/types@^7.4.4":
version "7.13.0" version "7.13.14"
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.13.0.tgz#74424d2816f0171b4100f0ab34e9a374efdf7f80" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.13.14.tgz#c35a4abb15c7cd45a2746d78ab328e362cbace0d"
integrity sha512-hE+HE8rnG1Z6Wzo+MhaKE5lM5eMx71T4EHJgku2E3xIfaULhDcxiiRxUYgwX8qwP1BBSlag+TdGOt6JAidIZTA== integrity sha512-A2aa3QTkWoyqsZZFl56MLUsfmh7O0gN41IPvXAE/++8ojpbz12SszD7JEGYVdn4f9Kt4amIei07swF1h4AqmmQ==
dependencies: dependencies:
"@babel/helper-validator-identifier" "^7.12.11" "@babel/helper-validator-identifier" "^7.12.11"
lodash "^4.17.19" lodash "^4.17.19"
@ -1091,22 +1130,22 @@
"@webassemblyjs/ast" "1.11.0" "@webassemblyjs/ast" "1.11.0"
"@xtuc/long" "4.2.2" "@xtuc/long" "4.2.2"
"@webpack-cli/configtest@^1.0.1": "@webpack-cli/configtest@^1.0.2":
version "1.0.1" version "1.0.2"
resolved "https://registry.yarnpkg.com/@webpack-cli/configtest/-/configtest-1.0.1.tgz#241aecfbdc715eee96bed447ed402e12ec171935" resolved "https://registry.yarnpkg.com/@webpack-cli/configtest/-/configtest-1.0.2.tgz#2a20812bfb3a2ebb0b27ee26a52eeb3e3f000836"
integrity sha512-B+4uBUYhpzDXmwuo3V9yBH6cISwxEI4J+NO5ggDaGEEHb0osY/R7MzeKc0bHURXQuZjMM4qD+bSJCKIuI3eNBQ== integrity sha512-3OBzV2fBGZ5TBfdW50cha1lHDVf9vlvRXnjpVbJBa20pSZQaSkMJZiwA8V2vD9ogyeXn8nU5s5A6mHyf5jhMzA==
"@webpack-cli/info@^1.2.2": "@webpack-cli/info@^1.2.3":
version "1.2.2" version "1.2.3"
resolved "https://registry.yarnpkg.com/@webpack-cli/info/-/info-1.2.2.tgz#ef3c0cd947a1fa083e174a59cb74e0b6195c236c" resolved "https://registry.yarnpkg.com/@webpack-cli/info/-/info-1.2.3.tgz#ef819d10ace2976b6d134c7c823a3e79ee31a92c"
integrity sha512-5U9kUJHnwU+FhKH4PWGZuBC1hTEPYyxGSL5jjoBI96Gx8qcYJGOikpiIpFoTq8mmgX3im2zAo2wanv/alD74KQ== integrity sha512-lLek3/T7u40lTqzCGpC6CAbY6+vXhdhmwFRxZLMnRm6/sIF/7qMpT8MocXCRQfz0JAh63wpbXLMnsQ5162WS7Q==
dependencies: dependencies:
envinfo "^7.7.3" envinfo "^7.7.3"
"@webpack-cli/serve@^1.3.0": "@webpack-cli/serve@^1.3.1":
version "1.3.0" version "1.3.1"
resolved "https://registry.yarnpkg.com/@webpack-cli/serve/-/serve-1.3.0.tgz#2730c770f5f1f132767c63dcaaa4ec28f8c56a6c" resolved "https://registry.yarnpkg.com/@webpack-cli/serve/-/serve-1.3.1.tgz#911d1b3ff4a843304b9c3bacf67bb34672418441"
integrity sha512-k2p2VrONcYVX1wRRrf0f3X2VGltLWcv+JzXRBDmvCxGlCeESx4OXw91TsWeKOkp784uNoVQo313vxJFHXPPwfw== integrity sha512-0qXvpeYO6vaNoRBI52/UsbcaBydJCggoBBnIo/ovQQdn6fug0BgwsjorV1hVS7fMqGVTZGcVxv8334gjmbj5hw==
"@xtuc/ieee754@^1.2.0": "@xtuc/ieee754@^1.2.0":
version "1.2.0" version "1.2.0"
@ -1426,12 +1465,7 @@ color-name@~1.1.4:
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
colorette@^1.2.1: colorette@^1.2.1, colorette@^1.2.2:
version "1.2.1"
resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.1.tgz#4d0b921325c14faf92633086a536db6e89564b1b"
integrity sha512-puCDz0CzydiSYOrnXpz/PKd69zRrribezjtE9yd4zvytoRc8+RY/KJPvtPFKZS3E3wP6neGyMe0vOTlHO5L3Pw==
colorette@^1.2.2:
version "1.2.2" version "1.2.2"
resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.2.tgz#cbcc79d5e99caea2dbf10eb3a26fd8b3e6acfa94" resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.2.tgz#cbcc79d5e99caea2dbf10eb3a26fd8b3e6acfa94"
integrity sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w== integrity sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w==
@ -1497,10 +1531,10 @@ cross-spawn@^7.0.1, cross-spawn@^7.0.3:
shebang-command "^2.0.0" shebang-command "^2.0.0"
which "^2.0.1" which "^2.0.1"
css-loader@5.1.3: css-loader@5.2.0:
version "5.1.3" version "5.2.0"
resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-5.1.3.tgz#87f6fc96816b20debe3cf682f85c7e56a963d0d1" resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-5.2.0.tgz#a9ecda190500863673ce4434033710404efbff00"
integrity sha512-CoPZvyh8sLiGARK3gqczpfdedbM74klGWurF2CsNZ2lhNaXdLIUks+3Mfax3WBeRuHoglU+m7KG/+7gY6G4aag== integrity sha512-MfRo2MjEeLXMlUkeUwN71Vx5oc6EJnx5UQ4Yi9iUtYQvrPtwLUucYptz0hc6n++kdNcyF5olYBS4vPjJDAcLkw==
dependencies: dependencies:
camelcase "^6.2.0" camelcase "^6.2.0"
cssesc "^3.0.0" cssesc "^3.0.0"
@ -2030,10 +2064,10 @@ mimic-fn@^2.1.0:
resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b"
integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==
mini-css-extract-plugin@1.3.9: mini-css-extract-plugin@1.4.0:
version "1.3.9" version "1.4.0"
resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-1.3.9.tgz#47a32132b0fd97a119acd530e8421e8f6ab16d5e" resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-1.4.0.tgz#c8e571c4b6d63afa56c47260343adf623349c473"
integrity sha512-Ac4s+xhVbqlyhXS5J/Vh/QXUz3ycXlCqoCPpg0vdfhsIBH9eg/It/9L1r1XhSCH737M1lqcWnMuWL13zcygn5A== integrity sha512-DyQr5DhXXARKZoc4kwvCvD95kh69dUupfuKOmBUqZ4kBTmRaRZcU32lYu3cLd6nEGXhQ1l7LzZ3F/CjItaY6VQ==
dependencies: dependencies:
loader-utils "^2.0.0" loader-utils "^2.0.0"
schema-utils "^3.0.0" schema-utils "^3.0.0"
@ -2535,10 +2569,10 @@ totalist@^1.0.0:
resolved "https://registry.yarnpkg.com/totalist/-/totalist-1.1.0.tgz#a4d65a3e546517701e3e5c37a47a70ac97fe56df" resolved "https://registry.yarnpkg.com/totalist/-/totalist-1.1.0.tgz#a4d65a3e546517701e3e5c37a47a70ac97fe56df"
integrity sha512-gduQwd1rOdDMGxFG1gEvhV88Oirdo2p+KjoYFU7k2g+i7n6AFFbDQ5kMPUsW0pNbfQsB/cwXvT1i4Bue0s9g5g== integrity sha512-gduQwd1rOdDMGxFG1gEvhV88Oirdo2p+KjoYFU7k2g+i7n6AFFbDQ5kMPUsW0pNbfQsB/cwXvT1i4Bue0s9g5g==
ts-loader@8.0.18: ts-loader@8.1.0:
version "8.0.18" version "8.1.0"
resolved "https://registry.yarnpkg.com/ts-loader/-/ts-loader-8.0.18.tgz#b2385cbe81c34ad9f997915129cdde3ad92a61ea" resolved "https://registry.yarnpkg.com/ts-loader/-/ts-loader-8.1.0.tgz#d6292487df279c7cc79b6d3b70bb9d31682b693e"
integrity sha512-hRZzkydPX30XkLaQwJTDcWDoxZHK6IrEMDQpNd7tgcakFruFkeUp/aY+9hBb7BUGb+ZWKI0jiOGMo0MckwzdDQ== integrity sha512-YiQipGGAFj2zBfqLhp28yUvPP9jUGqHxRzrGYuc82Z2wM27YIHbElXiaZDc93c3x0mz4zvBmS6q/DgExpdj37A==
dependencies: dependencies:
chalk "^4.1.0" chalk "^4.1.0"
enhanced-resolve "^4.0.0" enhanced-resolve "^4.0.0"
@ -2551,6 +2585,11 @@ tslib@^1.9.0:
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
typescript@4.2.3:
version "4.2.3"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.2.3.tgz#39062d8019912d43726298f09493d598048c1ce3"
integrity sha512-qOcYwxaByStAWrBf4x0fibwZvMRG+r4cQoTjbPtUlrWjBHbmCAww1i448U0GJ+3cNNEtebDteo/cHOR3xJ4wEw==
unicode-canonical-property-names-ecmascript@^1.0.4: unicode-canonical-property-names-ecmascript@^1.0.4:
version "1.0.4" version "1.0.4"
resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz#2619800c4c825800efdd8343af7dd9933cbe2818" resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz#2619800c4c825800efdd8343af7dd9933cbe2818"
@ -2619,15 +2658,15 @@ webpack-bundle-analyzer@4.4.0:
sirv "^1.0.7" sirv "^1.0.7"
ws "^7.3.1" ws "^7.3.1"
webpack-cli@4.5.0: webpack-cli@4.6.0:
version "4.5.0" version "4.6.0"
resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-4.5.0.tgz#b5213b84adf6e1f5de6391334c9fa53a48850466" resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-4.6.0.tgz#27ae86bfaec0cf393fcfd58abdc5a229ad32fd16"
integrity sha512-wXg/ef6Ibstl2f50mnkcHblRPN/P9J4Nlod5Hg9HGFgSeF8rsqDGHJeVe4aR26q9l62TUJi6vmvC2Qz96YJw1Q== integrity sha512-9YV+qTcGMjQFiY7Nb1kmnupvb1x40lfpj8pwdO/bom+sQiP4OBMKjHq29YQrlDWDPZO9r/qWaRRywKaRDKqBTA==
dependencies: dependencies:
"@discoveryjs/json-ext" "^0.5.0" "@discoveryjs/json-ext" "^0.5.0"
"@webpack-cli/configtest" "^1.0.1" "@webpack-cli/configtest" "^1.0.2"
"@webpack-cli/info" "^1.2.2" "@webpack-cli/info" "^1.2.3"
"@webpack-cli/serve" "^1.3.0" "@webpack-cli/serve" "^1.3.1"
colorette "^1.2.1" colorette "^1.2.1"
commander "^7.0.0" commander "^7.0.0"
enquirer "^2.3.6" enquirer "^2.3.6"
@ -2663,10 +2702,10 @@ webpack-sources@^2.1.1:
source-list-map "^2.0.1" source-list-map "^2.0.1"
source-map "^0.6.1" source-map "^0.6.1"
webpack@5.27.1: webpack@5.30.0:
version "5.27.1" version "5.30.0"
resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.27.1.tgz#6808fb6e45e35290cdb8ae43c7a10884839a3079" resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.30.0.tgz#07d87c182a060e0c2491062f3dc0edc85a29d884"
integrity sha512-rxIDsPZ3Apl3JcqiemiLmWH+hAq04YeOXqvCxNZOnTp8ZgM9NEPtbu4CaMfMEf9KShnx/Ym8uLGmM6P4XnwCoA== integrity sha512-Zr9NIri5yzpfmaMea2lSMV1UygbW0zQsSlGLMgKUm63ACXg6alhd1u4v5UBSBjzYKXJN6BNMGVM7w165e7NxYA==
dependencies: dependencies:
"@types/eslint-scope" "^3.7.0" "@types/eslint-scope" "^3.7.0"
"@types/estree" "^0.0.46" "@types/estree" "^0.0.46"

View File

@ -0,0 +1,11 @@
{
"compilerOptions": {
"target": "es6",
"module": "esNext",
"moduleResolution": "node",
"resolveJsonModule": true,
"jsx": "react-jsx",
"sourceMap": true,
"allowJs": true
}
}

View File

@ -11,7 +11,7 @@ import { restore } from "../data/restore";
type ExportOpts = { type ExportOpts = {
elements: readonly ExcalidrawElement[]; elements: readonly ExcalidrawElement[];
appState?: Partial<Omit<AppState, "offsetTop" | "offsetLeft">>; appState?: Partial<Omit<AppState, "offsetTop" | "offsetLeft">>;
getDimensions: ( getDimensions?: (
width: number, width: number,
height: number, height: number,
) => { width: number; height: number; scale: number }; ) => { width: number; height: number; scale: number };
@ -33,7 +33,7 @@ export const exportToCanvas = ({
} = restoredAppState; } = restoredAppState;
return _exportToCanvas( return _exportToCanvas(
getNonDeletedElements(restoredElements), getNonDeletedElements(restoredElements),
{ ...restoredAppState, offsetTop: 0, offsetLeft: 0 }, { ...restoredAppState, offsetTop: 0, offsetLeft: 0, width: 0, height: 0 },
{ exportBackground, viewBackgroundColor, shouldAddWatermark }, { exportBackground, viewBackgroundColor, shouldAddWatermark },
(width: number, height: number) => { (width: number, height: number) => {
const canvas = document.createElement("canvas"); const canvas = document.createElement("canvas");
@ -83,7 +83,7 @@ export const exportToSvg = ({
appState = getDefaultAppState(), appState = getDefaultAppState(),
exportPadding, exportPadding,
metadata, metadata,
}: ExportOpts & { }: Omit<ExportOpts, "getDimensions"> & {
exportPadding?: number; exportPadding?: number;
metadata?: string; metadata?: string;
}): SVGSVGElement => { }): SVGSVGElement => {

View File

@ -5,3 +5,7 @@
First release of `@excalidraw/utils` to provide utilities functions. First release of `@excalidraw/utils` to provide utilities functions.
- Added `exportToBlob` and `exportToSvg` to export an Excalidraw diagram definition, respectively, to a [Blob](https://developer.mozilla.org/en-US/docs/Web/API/Blob) and to a [SVGElement](https://developer.mozilla.org/en-US/docs/Web/API/SVGElement) ([#2246](https://github.com/excalidraw/excalidraw/pull/2246)) - Added `exportToBlob` and `exportToSvg` to export an Excalidraw diagram definition, respectively, to a [Blob](https://developer.mozilla.org/en-US/docs/Web/API/Blob) and to a [SVGElement](https://developer.mozilla.org/en-US/docs/Web/API/SVGElement) ([#2246](https://github.com/excalidraw/excalidraw/pull/2246))
### Features
- Flip single elements horizontally or vertically [#2520](https://github.com/excalidraw/excalidraw/pull/2520)

View File

@ -34,23 +34,23 @@
] ]
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "7.13.10", "@babel/core": "7.13.14",
"@babel/plugin-transform-arrow-functions": "7.13.0", "@babel/plugin-transform-arrow-functions": "7.13.0",
"@babel/plugin-transform-async-to-generator": "7.13.0", "@babel/plugin-transform-async-to-generator": "7.13.0",
"@babel/plugin-transform-runtime": "^7.12.10", "@babel/plugin-transform-runtime": "^7.12.10",
"@babel/plugin-transform-typescript": "7.13.0", "@babel/plugin-transform-typescript": "7.13.0",
"@babel/preset-env": "7.13.10", "@babel/preset-env": "7.13.12",
"@babel/preset-typescript": "7.13.0", "@babel/preset-typescript": "7.13.0",
"babel-loader": "8.2.2", "babel-loader": "8.2.2",
"babel-plugin-transform-class-properties": "6.24.1", "babel-plugin-transform-class-properties": "6.24.1",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"css-loader": "5.1.3", "css-loader": "5.2.0",
"file-loader": "6.2.0", "file-loader": "6.2.0",
"sass-loader": "11.0.1", "sass-loader": "11.0.1",
"ts-loader": "8.0.18", "ts-loader": "8.1.0",
"webpack": "5.27.1", "webpack": "5.30.0",
"webpack-bundle-analyzer": "4.4.0", "webpack-bundle-analyzer": "4.4.0",
"webpack-cli": "4.5.0" "webpack-cli": "4.6.0"
}, },
"bugs": "https://github.com/excalidraw/excalidraw/issues", "bugs": "https://github.com/excalidraw/excalidraw/issues",
"repository": "https://github.com/excalidraw/excalidraw", "repository": "https://github.com/excalidraw/excalidraw",

View File

@ -9,34 +9,33 @@
dependencies: dependencies:
"@babel/highlight" "^7.12.13" "@babel/highlight" "^7.12.13"
"@babel/compat-data@^7.13.0", "@babel/compat-data@^7.13.8": "@babel/compat-data@^7.13.0", "@babel/compat-data@^7.13.12", "@babel/compat-data@^7.13.8":
version "7.13.8" version "7.13.12"
resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.13.8.tgz#5b783b9808f15cef71547f1b691f34f8ff6003a6" resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.13.12.tgz#a8a5ccac19c200f9dd49624cac6e19d7be1236a1"
integrity sha512-EaI33z19T4qN3xLXsGf48M2cDqa6ei9tPZlfLdb2HC+e/cFtREiRd8hdSqDbwdLB0/+gLwqJmCYASH0z2bUdog== integrity sha512-3eJJ841uKxeV8dcN/2yGEUy+RfgQspPEgQat85umsE1rotuquQ2AbIub4S6j7c50a2d+4myc+zSlnXeIHrOnhQ==
"@babel/core@7.13.10": "@babel/core@7.13.14":
version "7.13.10" version "7.13.14"
resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.13.10.tgz#07de050bbd8193fcd8a3c27918c0890613a94559" resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.13.14.tgz#8e46ebbaca460a63497c797e574038ab04ae6d06"
integrity sha512-bfIYcT0BdKeAZrovpMqX2Mx5NrgAckGbwT982AkdS5GNfn3KMGiprlBAtmBcFZRUmpaufS6WZFP8trvx8ptFDw== integrity sha512-wZso/vyF4ki0l0znlgM4inxbdrUvCb+cVz8grxDq+6C9k6qbqoIJteQOKicaKjCipU3ISV+XedCqpL2RJJVehA==
dependencies: dependencies:
"@babel/code-frame" "^7.12.13" "@babel/code-frame" "^7.12.13"
"@babel/generator" "^7.13.9" "@babel/generator" "^7.13.9"
"@babel/helper-compilation-targets" "^7.13.10" "@babel/helper-compilation-targets" "^7.13.13"
"@babel/helper-module-transforms" "^7.13.0" "@babel/helper-module-transforms" "^7.13.14"
"@babel/helpers" "^7.13.10" "@babel/helpers" "^7.13.10"
"@babel/parser" "^7.13.10" "@babel/parser" "^7.13.13"
"@babel/template" "^7.12.13" "@babel/template" "^7.12.13"
"@babel/traverse" "^7.13.0" "@babel/traverse" "^7.13.13"
"@babel/types" "^7.13.0" "@babel/types" "^7.13.14"
convert-source-map "^1.7.0" convert-source-map "^1.7.0"
debug "^4.1.0" debug "^4.1.0"
gensync "^1.0.0-beta.2" gensync "^1.0.0-beta.2"
json5 "^2.1.2" json5 "^2.1.2"
lodash "^4.17.19"
semver "^6.3.0" semver "^6.3.0"
source-map "^0.5.0" source-map "^0.5.0"
"@babel/generator@^7.13.0", "@babel/generator@^7.13.9": "@babel/generator@^7.13.9":
version "7.13.9" version "7.13.9"
resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.13.9.tgz#3a7aa96f9efb8e2be42d38d80e2ceb4c64d8de39" resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.13.9.tgz#3a7aa96f9efb8e2be42d38d80e2ceb4c64d8de39"
integrity sha512-mHOOmY0Axl/JCTkxTU6Lf5sWOg/v8nUa+Xkt4zMTftX0wqmb6Sh7J8gvcehBw7q0AhrhAR+FDacKjCZ2X8K+Sw== integrity sha512-mHOOmY0Axl/JCTkxTU6Lf5sWOg/v8nUa+Xkt4zMTftX0wqmb6Sh7J8gvcehBw7q0AhrhAR+FDacKjCZ2X8K+Sw==
@ -60,12 +59,12 @@
"@babel/helper-explode-assignable-expression" "^7.12.13" "@babel/helper-explode-assignable-expression" "^7.12.13"
"@babel/types" "^7.12.13" "@babel/types" "^7.12.13"
"@babel/helper-compilation-targets@^7.13.0", "@babel/helper-compilation-targets@^7.13.10", "@babel/helper-compilation-targets@^7.13.8": "@babel/helper-compilation-targets@^7.13.0", "@babel/helper-compilation-targets@^7.13.10", "@babel/helper-compilation-targets@^7.13.13", "@babel/helper-compilation-targets@^7.13.8":
version "7.13.10" version "7.13.13"
resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.13.10.tgz#1310a1678cb8427c07a753750da4f8ce442bdd0c" resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.13.13.tgz#2b2972a0926474853f41e4adbc69338f520600e5"
integrity sha512-/Xju7Qg1GQO4mHZ/Kcs6Au7gfafgZnwm+a7sy/ow/tV1sHeraRUHbjdat8/UvDor4Tez+siGKDk6zIKtCPKVJA== integrity sha512-q1kcdHNZehBwD9jYPh3WyXcsFERi39X4I59I3NadciWtNDyZ6x+GboOxncFK0kXlKIv6BJm5acncehXWUjWQMQ==
dependencies: dependencies:
"@babel/compat-data" "^7.13.8" "@babel/compat-data" "^7.13.12"
"@babel/helper-validator-option" "^7.12.17" "@babel/helper-validator-option" "^7.12.17"
browserslist "^4.14.5" browserslist "^4.14.5"
semver "^6.3.0" semver "^6.3.0"
@ -148,6 +147,13 @@
dependencies: dependencies:
"@babel/types" "^7.13.0" "@babel/types" "^7.13.0"
"@babel/helper-member-expression-to-functions@^7.13.12":
version "7.13.12"
resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.13.12.tgz#dfe368f26d426a07299d8d6513821768216e6d72"
integrity sha512-48ql1CLL59aKbU94Y88Xgb2VFy7a95ykGRbJJaaVv+LX5U8wFpLfiGXJJGUozsmA1oEh/o5Bp60Voq7ACyA/Sw==
dependencies:
"@babel/types" "^7.13.12"
"@babel/helper-module-imports@^7.12.13": "@babel/helper-module-imports@^7.12.13":
version "7.12.13" version "7.12.13"
resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.12.13.tgz#ec67e4404f41750463e455cc3203f6a32e93fcb0" resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.12.13.tgz#ec67e4404f41750463e455cc3203f6a32e93fcb0"
@ -155,20 +161,26 @@
dependencies: dependencies:
"@babel/types" "^7.12.13" "@babel/types" "^7.12.13"
"@babel/helper-module-transforms@^7.13.0": "@babel/helper-module-imports@^7.13.12":
version "7.13.0" version "7.13.12"
resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.13.0.tgz#42eb4bd8eea68bab46751212c357bfed8b40f6f1" resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.13.12.tgz#c6a369a6f3621cb25da014078684da9196b61977"
integrity sha512-Ls8/VBwH577+pw7Ku1QkUWIyRRNHpYlts7+qSqBBFCW3I8QteB9DxfcZ5YJpOwH6Ihe/wn8ch7fMGOP1OhEIvw== integrity sha512-4cVvR2/1B693IuOvSI20xqqa/+bl7lqAMR59R4iu39R9aOX8/JoYY1sFaNvUMyMBGnHdwvJgUrzNLoUZxXypxA==
dependencies: dependencies:
"@babel/helper-module-imports" "^7.12.13" "@babel/types" "^7.13.12"
"@babel/helper-replace-supers" "^7.13.0"
"@babel/helper-simple-access" "^7.12.13" "@babel/helper-module-transforms@^7.13.0", "@babel/helper-module-transforms@^7.13.14":
version "7.13.14"
resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.13.14.tgz#e600652ba48ccb1641775413cb32cfa4e8b495ef"
integrity sha512-QuU/OJ0iAOSIatyVZmfqB0lbkVP0kDRiKj34xy+QNsnVZi/PA6BoSoreeqnxxa9EHFAIL0R9XOaAR/G9WlIy5g==
dependencies:
"@babel/helper-module-imports" "^7.13.12"
"@babel/helper-replace-supers" "^7.13.12"
"@babel/helper-simple-access" "^7.13.12"
"@babel/helper-split-export-declaration" "^7.12.13" "@babel/helper-split-export-declaration" "^7.12.13"
"@babel/helper-validator-identifier" "^7.12.11" "@babel/helper-validator-identifier" "^7.12.11"
"@babel/template" "^7.12.13" "@babel/template" "^7.12.13"
"@babel/traverse" "^7.13.0" "@babel/traverse" "^7.13.13"
"@babel/types" "^7.13.0" "@babel/types" "^7.13.14"
lodash "^4.17.19"
"@babel/helper-optimise-call-expression@^7.12.13": "@babel/helper-optimise-call-expression@^7.12.13":
version "7.12.13" version "7.12.13"
@ -211,6 +223,16 @@
"@babel/traverse" "^7.13.0" "@babel/traverse" "^7.13.0"
"@babel/types" "^7.13.0" "@babel/types" "^7.13.0"
"@babel/helper-replace-supers@^7.13.12":
version "7.13.12"
resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.13.12.tgz#6442f4c1ad912502481a564a7386de0c77ff3804"
integrity sha512-Gz1eiX+4yDO8mT+heB94aLVNCL+rbuT2xy4YfyNqu8F+OI6vMvJK891qGBTqL9Uc8wxEvRW92Id6G7sDen3fFw==
dependencies:
"@babel/helper-member-expression-to-functions" "^7.13.12"
"@babel/helper-optimise-call-expression" "^7.12.13"
"@babel/traverse" "^7.13.0"
"@babel/types" "^7.13.12"
"@babel/helper-simple-access@^7.12.13": "@babel/helper-simple-access@^7.12.13":
version "7.12.13" version "7.12.13"
resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.12.13.tgz#8478bcc5cacf6aa1672b251c1d2dde5ccd61a6c4" resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.12.13.tgz#8478bcc5cacf6aa1672b251c1d2dde5ccd61a6c4"
@ -218,6 +240,13 @@
dependencies: dependencies:
"@babel/types" "^7.12.13" "@babel/types" "^7.12.13"
"@babel/helper-simple-access@^7.13.12":
version "7.13.12"
resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.13.12.tgz#dd6c538afb61819d205a012c31792a39c7a5eaf6"
integrity sha512-7FEjbrx5SL9cWvXioDbnlYTppcZGuCY6ow3/D5vMggb2Ywgu4dMrpTJX0JdQAIcRRUElOIxF3yEooa9gUb9ZbA==
dependencies:
"@babel/types" "^7.13.12"
"@babel/helper-skip-transparent-expression-wrappers@^7.12.1": "@babel/helper-skip-transparent-expression-wrappers@^7.12.1":
version "7.12.1" version "7.12.1"
resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.12.1.tgz#462dc63a7e435ade8468385c63d2b84cce4b3cbf" resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.12.1.tgz#462dc63a7e435ade8468385c63d2b84cce4b3cbf"
@ -270,10 +299,19 @@
chalk "^2.0.0" chalk "^2.0.0"
js-tokens "^4.0.0" js-tokens "^4.0.0"
"@babel/parser@^7.12.13", "@babel/parser@^7.13.0", "@babel/parser@^7.13.10": "@babel/parser@^7.12.13", "@babel/parser@^7.13.13":
version "7.13.10" version "7.13.13"
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.13.10.tgz#8f8f9bf7b3afa3eabd061f7a5bcdf4fec3c48409" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.13.13.tgz#42f03862f4aed50461e543270916b47dd501f0df"
integrity sha512-0s7Mlrw9uTWkYua7xWr99Wpk2bnGa0ANleKfksYAES8LpWH4gW1OUr42vqKNf0us5UQNfru2wPqMqRITzq/SIQ== integrity sha512-OhsyMrqygfk5v8HmWwOzlYjJrtLaFhF34MrfG/Z73DgYCI6ojNUTUp2TYbtnjo8PegeJp12eamsNettCQjKjVw==
"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.13.12":
version "7.13.12"
resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.13.12.tgz#a3484d84d0b549f3fc916b99ee4783f26fabad2a"
integrity sha512-d0u3zWKcoZf379fOeJdr1a5WPDny4aOFZ6hlfKivgK0LY7ZxNfoaHL2fWwdGtHyVvra38FC+HVYkO+byfSA8AQ==
dependencies:
"@babel/helper-plugin-utils" "^7.13.0"
"@babel/helper-skip-transparent-expression-wrappers" "^7.12.1"
"@babel/plugin-proposal-optional-chaining" "^7.13.12"
"@babel/plugin-proposal-async-generator-functions@^7.13.8": "@babel/plugin-proposal-async-generator-functions@^7.13.8":
version "7.13.8" version "7.13.8"
@ -359,10 +397,10 @@
"@babel/helper-plugin-utils" "^7.13.0" "@babel/helper-plugin-utils" "^7.13.0"
"@babel/plugin-syntax-optional-catch-binding" "^7.8.3" "@babel/plugin-syntax-optional-catch-binding" "^7.8.3"
"@babel/plugin-proposal-optional-chaining@^7.13.8": "@babel/plugin-proposal-optional-chaining@^7.13.12":
version "7.13.8" version "7.13.12"
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.13.8.tgz#e39df93efe7e7e621841babc197982e140e90756" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.13.12.tgz#ba9feb601d422e0adea6760c2bd6bbb7bfec4866"
integrity sha512-hpbBwbTgd7Cz1QryvwJZRo1U0k1q8uyBmeXOSQUjdg/A2TASkhR/rz7AyqZ/kS8kbpsNA80rOYbxySBJAqmhhQ== integrity sha512-fcEdKOkIB7Tf4IxrgEVeFC4zeJSTr78no9wTdBuZZbqF64kzllU0ybo2zrzm7gUQfxGhBgq4E39oRs8Zx/RMYQ==
dependencies: dependencies:
"@babel/helper-plugin-utils" "^7.13.0" "@babel/helper-plugin-utils" "^7.13.0"
"@babel/helper-skip-transparent-expression-wrappers" "^7.12.1" "@babel/helper-skip-transparent-expression-wrappers" "^7.12.1"
@ -744,15 +782,16 @@
"@babel/helper-create-regexp-features-plugin" "^7.12.13" "@babel/helper-create-regexp-features-plugin" "^7.12.13"
"@babel/helper-plugin-utils" "^7.12.13" "@babel/helper-plugin-utils" "^7.12.13"
"@babel/preset-env@7.13.10": "@babel/preset-env@7.13.12":
version "7.13.10" version "7.13.12"
resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.13.10.tgz#b5cde31d5fe77ab2a6ab3d453b59041a1b3a5252" resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.13.12.tgz#6dff470478290582ac282fb77780eadf32480237"
integrity sha512-nOsTScuoRghRtUsRr/c69d042ysfPHcu+KOB4A9aAO9eJYqrkat+LF8G1yp1HD18QiwixT2CisZTr/0b3YZPXQ== integrity sha512-JzElc6jk3Ko6zuZgBtjOd01pf9yYDEIH8BcqVuYIuOkzOwDesoa/Nz4gIo4lBG6K861KTV9TvIgmFuT6ytOaAA==
dependencies: dependencies:
"@babel/compat-data" "^7.13.8" "@babel/compat-data" "^7.13.12"
"@babel/helper-compilation-targets" "^7.13.10" "@babel/helper-compilation-targets" "^7.13.10"
"@babel/helper-plugin-utils" "^7.13.0" "@babel/helper-plugin-utils" "^7.13.0"
"@babel/helper-validator-option" "^7.12.17" "@babel/helper-validator-option" "^7.12.17"
"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining" "^7.13.12"
"@babel/plugin-proposal-async-generator-functions" "^7.13.8" "@babel/plugin-proposal-async-generator-functions" "^7.13.8"
"@babel/plugin-proposal-class-properties" "^7.13.0" "@babel/plugin-proposal-class-properties" "^7.13.0"
"@babel/plugin-proposal-dynamic-import" "^7.13.8" "@babel/plugin-proposal-dynamic-import" "^7.13.8"
@ -763,7 +802,7 @@
"@babel/plugin-proposal-numeric-separator" "^7.12.13" "@babel/plugin-proposal-numeric-separator" "^7.12.13"
"@babel/plugin-proposal-object-rest-spread" "^7.13.8" "@babel/plugin-proposal-object-rest-spread" "^7.13.8"
"@babel/plugin-proposal-optional-catch-binding" "^7.13.8" "@babel/plugin-proposal-optional-catch-binding" "^7.13.8"
"@babel/plugin-proposal-optional-chaining" "^7.13.8" "@babel/plugin-proposal-optional-chaining" "^7.13.12"
"@babel/plugin-proposal-private-methods" "^7.13.0" "@babel/plugin-proposal-private-methods" "^7.13.0"
"@babel/plugin-proposal-unicode-property-regex" "^7.12.13" "@babel/plugin-proposal-unicode-property-regex" "^7.12.13"
"@babel/plugin-syntax-async-generators" "^7.8.4" "@babel/plugin-syntax-async-generators" "^7.8.4"
@ -811,7 +850,7 @@
"@babel/plugin-transform-unicode-escapes" "^7.12.13" "@babel/plugin-transform-unicode-escapes" "^7.12.13"
"@babel/plugin-transform-unicode-regex" "^7.12.13" "@babel/plugin-transform-unicode-regex" "^7.12.13"
"@babel/preset-modules" "^0.1.4" "@babel/preset-modules" "^0.1.4"
"@babel/types" "^7.13.0" "@babel/types" "^7.13.12"
babel-plugin-polyfill-corejs2 "^0.1.4" babel-plugin-polyfill-corejs2 "^0.1.4"
babel-plugin-polyfill-corejs3 "^0.1.3" babel-plugin-polyfill-corejs3 "^0.1.3"
babel-plugin-polyfill-regenerator "^0.1.2" babel-plugin-polyfill-regenerator "^0.1.2"
@ -854,25 +893,24 @@
"@babel/parser" "^7.12.13" "@babel/parser" "^7.12.13"
"@babel/types" "^7.12.13" "@babel/types" "^7.12.13"
"@babel/traverse@^7.12.13", "@babel/traverse@^7.13.0": "@babel/traverse@^7.12.13", "@babel/traverse@^7.13.0", "@babel/traverse@^7.13.13":
version "7.13.0" version "7.13.13"
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.13.0.tgz#6d95752475f86ee7ded06536de309a65fc8966cc" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.13.13.tgz#39aa9c21aab69f74d948a486dd28a2dbdbf5114d"
integrity sha512-xys5xi5JEhzC3RzEmSGrs/b3pJW/o87SypZ+G/PhaE7uqVQNv/jlmVIBXuoh5atqQ434LfXV+sf23Oxj0bchJQ== integrity sha512-CblEcwmXKR6eP43oQGG++0QMTtCjAsa3frUuzHoiIJWpaIIi8dwMyEFUJoXRLxagGqCK+jALRwIO+o3R9p/uUg==
dependencies: dependencies:
"@babel/code-frame" "^7.12.13" "@babel/code-frame" "^7.12.13"
"@babel/generator" "^7.13.0" "@babel/generator" "^7.13.9"
"@babel/helper-function-name" "^7.12.13" "@babel/helper-function-name" "^7.12.13"
"@babel/helper-split-export-declaration" "^7.12.13" "@babel/helper-split-export-declaration" "^7.12.13"
"@babel/parser" "^7.13.0" "@babel/parser" "^7.13.13"
"@babel/types" "^7.13.0" "@babel/types" "^7.13.13"
debug "^4.1.0" debug "^4.1.0"
globals "^11.1.0" globals "^11.1.0"
lodash "^4.17.19"
"@babel/types@^7.12.1", "@babel/types@^7.12.13", "@babel/types@^7.13.0", "@babel/types@^7.4.4": "@babel/types@^7.12.1", "@babel/types@^7.12.13", "@babel/types@^7.13.0", "@babel/types@^7.13.12", "@babel/types@^7.13.13", "@babel/types@^7.13.14", "@babel/types@^7.4.4":
version "7.13.0" version "7.13.14"
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.13.0.tgz#74424d2816f0171b4100f0ab34e9a374efdf7f80" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.13.14.tgz#c35a4abb15c7cd45a2746d78ab328e362cbace0d"
integrity sha512-hE+HE8rnG1Z6Wzo+MhaKE5lM5eMx71T4EHJgku2E3xIfaULhDcxiiRxUYgwX8qwP1BBSlag+TdGOt6JAidIZTA== integrity sha512-A2aa3QTkWoyqsZZFl56MLUsfmh7O0gN41IPvXAE/++8ojpbz12SszD7JEGYVdn4f9Kt4amIei07swF1h4AqmmQ==
dependencies: dependencies:
"@babel/helper-validator-identifier" "^7.12.11" "@babel/helper-validator-identifier" "^7.12.11"
lodash "^4.17.19" lodash "^4.17.19"
@ -1040,22 +1078,22 @@
"@webassemblyjs/ast" "1.11.0" "@webassemblyjs/ast" "1.11.0"
"@xtuc/long" "4.2.2" "@xtuc/long" "4.2.2"
"@webpack-cli/configtest@^1.0.1": "@webpack-cli/configtest@^1.0.2":
version "1.0.1" version "1.0.2"
resolved "https://registry.yarnpkg.com/@webpack-cli/configtest/-/configtest-1.0.1.tgz#241aecfbdc715eee96bed447ed402e12ec171935" resolved "https://registry.yarnpkg.com/@webpack-cli/configtest/-/configtest-1.0.2.tgz#2a20812bfb3a2ebb0b27ee26a52eeb3e3f000836"
integrity sha512-B+4uBUYhpzDXmwuo3V9yBH6cISwxEI4J+NO5ggDaGEEHb0osY/R7MzeKc0bHURXQuZjMM4qD+bSJCKIuI3eNBQ== integrity sha512-3OBzV2fBGZ5TBfdW50cha1lHDVf9vlvRXnjpVbJBa20pSZQaSkMJZiwA8V2vD9ogyeXn8nU5s5A6mHyf5jhMzA==
"@webpack-cli/info@^1.2.2": "@webpack-cli/info@^1.2.3":
version "1.2.2" version "1.2.3"
resolved "https://registry.yarnpkg.com/@webpack-cli/info/-/info-1.2.2.tgz#ef3c0cd947a1fa083e174a59cb74e0b6195c236c" resolved "https://registry.yarnpkg.com/@webpack-cli/info/-/info-1.2.3.tgz#ef819d10ace2976b6d134c7c823a3e79ee31a92c"
integrity sha512-5U9kUJHnwU+FhKH4PWGZuBC1hTEPYyxGSL5jjoBI96Gx8qcYJGOikpiIpFoTq8mmgX3im2zAo2wanv/alD74KQ== integrity sha512-lLek3/T7u40lTqzCGpC6CAbY6+vXhdhmwFRxZLMnRm6/sIF/7qMpT8MocXCRQfz0JAh63wpbXLMnsQ5162WS7Q==
dependencies: dependencies:
envinfo "^7.7.3" envinfo "^7.7.3"
"@webpack-cli/serve@^1.3.0": "@webpack-cli/serve@^1.3.1":
version "1.3.0" version "1.3.1"
resolved "https://registry.yarnpkg.com/@webpack-cli/serve/-/serve-1.3.0.tgz#2730c770f5f1f132767c63dcaaa4ec28f8c56a6c" resolved "https://registry.yarnpkg.com/@webpack-cli/serve/-/serve-1.3.1.tgz#911d1b3ff4a843304b9c3bacf67bb34672418441"
integrity sha512-k2p2VrONcYVX1wRRrf0f3X2VGltLWcv+JzXRBDmvCxGlCeESx4OXw91TsWeKOkp784uNoVQo313vxJFHXPPwfw== integrity sha512-0qXvpeYO6vaNoRBI52/UsbcaBydJCggoBBnIo/ovQQdn6fug0BgwsjorV1hVS7fMqGVTZGcVxv8334gjmbj5hw==
"@xtuc/ieee754@^1.2.0": "@xtuc/ieee754@^1.2.0":
version "1.2.0" version "1.2.0"
@ -1375,12 +1413,7 @@ color-name@~1.1.4:
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
colorette@^1.2.1: colorette@^1.2.1, colorette@^1.2.2:
version "1.2.1"
resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.1.tgz#4d0b921325c14faf92633086a536db6e89564b1b"
integrity sha512-puCDz0CzydiSYOrnXpz/PKd69zRrribezjtE9yd4zvytoRc8+RY/KJPvtPFKZS3E3wP6neGyMe0vOTlHO5L3Pw==
colorette@^1.2.2:
version "1.2.2" version "1.2.2"
resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.2.tgz#cbcc79d5e99caea2dbf10eb3a26fd8b3e6acfa94" resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.2.tgz#cbcc79d5e99caea2dbf10eb3a26fd8b3e6acfa94"
integrity sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w== integrity sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w==
@ -1446,10 +1479,10 @@ cross-spawn@^7.0.1, cross-spawn@^7.0.3:
shebang-command "^2.0.0" shebang-command "^2.0.0"
which "^2.0.1" which "^2.0.1"
css-loader@5.1.3: css-loader@5.2.0:
version "5.1.3" version "5.2.0"
resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-5.1.3.tgz#87f6fc96816b20debe3cf682f85c7e56a963d0d1" resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-5.2.0.tgz#a9ecda190500863673ce4434033710404efbff00"
integrity sha512-CoPZvyh8sLiGARK3gqczpfdedbM74klGWurF2CsNZ2lhNaXdLIUks+3Mfax3WBeRuHoglU+m7KG/+7gY6G4aag== integrity sha512-MfRo2MjEeLXMlUkeUwN71Vx5oc6EJnx5UQ4Yi9iUtYQvrPtwLUucYptz0hc6n++kdNcyF5olYBS4vPjJDAcLkw==
dependencies: dependencies:
camelcase "^6.2.0" camelcase "^6.2.0"
cssesc "^3.0.0" cssesc "^3.0.0"
@ -2475,10 +2508,10 @@ totalist@^1.0.0:
resolved "https://registry.yarnpkg.com/totalist/-/totalist-1.1.0.tgz#a4d65a3e546517701e3e5c37a47a70ac97fe56df" resolved "https://registry.yarnpkg.com/totalist/-/totalist-1.1.0.tgz#a4d65a3e546517701e3e5c37a47a70ac97fe56df"
integrity sha512-gduQwd1rOdDMGxFG1gEvhV88Oirdo2p+KjoYFU7k2g+i7n6AFFbDQ5kMPUsW0pNbfQsB/cwXvT1i4Bue0s9g5g== integrity sha512-gduQwd1rOdDMGxFG1gEvhV88Oirdo2p+KjoYFU7k2g+i7n6AFFbDQ5kMPUsW0pNbfQsB/cwXvT1i4Bue0s9g5g==
ts-loader@8.0.18: ts-loader@8.1.0:
version "8.0.18" version "8.1.0"
resolved "https://registry.yarnpkg.com/ts-loader/-/ts-loader-8.0.18.tgz#b2385cbe81c34ad9f997915129cdde3ad92a61ea" resolved "https://registry.yarnpkg.com/ts-loader/-/ts-loader-8.1.0.tgz#d6292487df279c7cc79b6d3b70bb9d31682b693e"
integrity sha512-hRZzkydPX30XkLaQwJTDcWDoxZHK6IrEMDQpNd7tgcakFruFkeUp/aY+9hBb7BUGb+ZWKI0jiOGMo0MckwzdDQ== integrity sha512-YiQipGGAFj2zBfqLhp28yUvPP9jUGqHxRzrGYuc82Z2wM27YIHbElXiaZDc93c3x0mz4zvBmS6q/DgExpdj37A==
dependencies: dependencies:
chalk "^4.1.0" chalk "^4.1.0"
enhanced-resolve "^4.0.0" enhanced-resolve "^4.0.0"
@ -2559,15 +2592,15 @@ webpack-bundle-analyzer@4.4.0:
sirv "^1.0.7" sirv "^1.0.7"
ws "^7.3.1" ws "^7.3.1"
webpack-cli@4.5.0: webpack-cli@4.6.0:
version "4.5.0" version "4.6.0"
resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-4.5.0.tgz#b5213b84adf6e1f5de6391334c9fa53a48850466" resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-4.6.0.tgz#27ae86bfaec0cf393fcfd58abdc5a229ad32fd16"
integrity sha512-wXg/ef6Ibstl2f50mnkcHblRPN/P9J4Nlod5Hg9HGFgSeF8rsqDGHJeVe4aR26q9l62TUJi6vmvC2Qz96YJw1Q== integrity sha512-9YV+qTcGMjQFiY7Nb1kmnupvb1x40lfpj8pwdO/bom+sQiP4OBMKjHq29YQrlDWDPZO9r/qWaRRywKaRDKqBTA==
dependencies: dependencies:
"@discoveryjs/json-ext" "^0.5.0" "@discoveryjs/json-ext" "^0.5.0"
"@webpack-cli/configtest" "^1.0.1" "@webpack-cli/configtest" "^1.0.2"
"@webpack-cli/info" "^1.2.2" "@webpack-cli/info" "^1.2.3"
"@webpack-cli/serve" "^1.3.0" "@webpack-cli/serve" "^1.3.1"
colorette "^1.2.1" colorette "^1.2.1"
commander "^7.0.0" commander "^7.0.0"
enquirer "^2.3.6" enquirer "^2.3.6"
@ -2595,10 +2628,10 @@ webpack-sources@^2.1.1:
source-list-map "^2.0.1" source-list-map "^2.0.1"
source-map "^0.6.1" source-map "^0.6.1"
webpack@5.27.1: webpack@5.30.0:
version "5.27.1" version "5.30.0"
resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.27.1.tgz#6808fb6e45e35290cdb8ae43c7a10884839a3079" resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.30.0.tgz#07d87c182a060e0c2491062f3dc0edc85a29d884"
integrity sha512-rxIDsPZ3Apl3JcqiemiLmWH+hAq04YeOXqvCxNZOnTp8ZgM9NEPtbu4CaMfMEf9KShnx/Ym8uLGmM6P4XnwCoA== integrity sha512-Zr9NIri5yzpfmaMea2lSMV1UygbW0zQsSlGLMgKUm63ACXg6alhd1u4v5UBSBjzYKXJN6BNMGVM7w165e7NxYA==
dependencies: dependencies:
"@types/eslint-scope" "^3.7.0" "@types/eslint-scope" "^3.7.0"
"@types/estree" "^0.0.46" "@types/estree" "^0.0.46"

View File

@ -349,12 +349,12 @@ const generateElementShape = (
if (element.type === "arrow") { if (element.type === "arrow") {
const { startArrowhead = null, endArrowhead = "arrow" } = element; const { startArrowhead = null, endArrowhead = "arrow" } = element;
function getArrowheadShapes( const getArrowheadShapes = (
element: ExcalidrawLinearElement, element: ExcalidrawLinearElement,
shape: Drawable[], shape: Drawable[],
position: "start" | "end", position: "start" | "end",
arrowhead: Arrowhead, arrowhead: Arrowhead,
) { ) => {
const arrowheadPoints = getArrowheadPoints( const arrowheadPoints = getArrowheadPoints(
element, element,
shape, shape,
@ -392,7 +392,7 @@ const generateElementShape = (
generator.line(x3, y3, x2, y2, options), generator.line(x3, y3, x2, y2, options),
generator.line(x4, y4, x2, y2, options), generator.line(x4, y4, x2, y2, options),
]; ];
} };
if (startArrowhead !== null) { if (startArrowhead !== null) {
const shapes = getArrowheadShapes( const shapes = getArrowheadShapes(

View File

@ -48,7 +48,7 @@ import {
TransformHandleType, TransformHandleType,
} from "../element/transformHandles"; } from "../element/transformHandles";
import { viewportCoordsToSceneCoords, supportsEmoji } from "../utils"; import { viewportCoordsToSceneCoords, supportsEmoji } from "../utils";
import { UserIdleState } from "../excalidraw-app/collab/types"; import { UserIdleState } from "../types";
import { THEME_FILTER } from "../constants"; import { THEME_FILTER } from "../constants";
const hasEmojiSupport = supportsEmoji(); const hasEmojiSupport = supportsEmoji();

View File

@ -0,0 +1,439 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<Excalidraw/> Test UIOptions prop Test canvasActions should not hide any UI element when canvasActions is "undefined" 1`] = `
<section
aria-labelledby="canvasActions-title"
class="zen-mode-transition"
>
<h2
class="visually-hidden"
id="canvasActions-title"
>
Canvas actions
</h2>
<div
class="Island"
style="--padding: 2; z-index: 1;"
>
<div
class="Stack Stack_vertical"
style="--gap: 4;"
>
<div
class="Stack Stack_horizontal"
style="--gap: 1; justify-content: space-between;"
>
<button
aria-label="Load"
class="ToolIcon_type_button ToolIcon_size_m ToolIcon_type_button--show ToolIcon"
data-testid="load-button"
title="Load"
type="button"
>
<div
aria-hidden="true"
class="ToolIcon__icon"
>
<svg
aria-hidden="true"
class="rtl-mirror"
focusable="false"
role="img"
viewBox="0 0 576 512"
>
<path
d="M572.694 292.093L500.27 416.248A63.997 63.997 0 0 1 444.989 448H45.025c-18.523 0-30.064-20.093-20.731-36.093l72.424-124.155A64 64 0 0 1 152 256h399.964c18.523 0 30.064 20.093 20.73 36.093zM152 224h328v-48c0-26.51-21.49-48-48-48H272l-64-64H48C21.49 64 0 85.49 0 112v278.046l69.077-118.418C86.214 242.25 117.989 224 152 224z"
fill="currentColor"
/>
</svg>
</div>
</button>
<button
aria-label="Save"
class="ToolIcon_type_button ToolIcon_size_m ToolIcon_type_button--show ToolIcon"
data-testid="save-button"
title="Save"
type="button"
>
<div
aria-hidden="true"
class="ToolIcon__icon"
>
<svg
aria-hidden="true"
class=""
focusable="false"
role="img"
viewBox="0 0 448 512"
>
<path
d="M433.941 129.941l-83.882-83.882A48 48 0 0 0 316.118 32H48C21.49 32 0 53.49 0 80v352c0 26.51 21.49 48 48 48h352c26.51 0 48-21.49 48-48V163.882a48 48 0 0 0-14.059-33.941zM224 416c-35.346 0-64-28.654-64-64 0-35.346 28.654-64 64-64s64 28.654 64 64c0 35.346-28.654 64-64 64zm96-304.52V212c0 6.627-5.373 12-12 12H76c-6.627 0-12-5.373-12-12V108c0-6.627 5.373-12 12-12h228.52c3.183 0 6.235 1.264 8.485 3.515l3.48 3.48A11.996 11.996 0 0 1 320 111.48z"
fill="currentColor"
/>
</svg>
</div>
</button>
<button
aria-label="Save as"
class="ToolIcon_type_button ToolIcon_size_m ToolIcon_type_button--hide"
data-testid="save-as-button"
hidden=""
title="Save as"
type="button"
>
<div
aria-hidden="true"
class="ToolIcon__icon"
>
<svg
aria-hidden="true"
class=""
focusable="false"
role="img"
viewBox="0 0 448 512"
>
<path
d="M252 54L203 8a28 27 0 00-20-8H28C12 0 0 12 0 27v195c0 15 12 26 28 26h204c15 0 28-11 28-26V73a28 27 0 00-8-19zM130 213c-21 0-37-16-37-36 0-19 16-35 37-35 20 0 37 16 37 35 0 20-17 36-37 36zm56-169v56c0 4-4 6-7 6H44c-4 0-7-2-7-6V42c0-4 3-7 7-7h133l4 2 3 2a7 7 0 012 5z M296 201l87 95-188 205-78 9c-10 1-19-8-18-20l9-84zm141-14l-41-44a31 31 0 00-46 0l-38 41 87 95 38-42c13-14 13-36 0-50z"
fill="currentColor"
/>
</svg>
</div>
</button>
<button
aria-label="Export"
class="ToolIcon_type_button ToolIcon_size_m ToolIcon_type_button--show ToolIcon"
data-testid="export-button"
title="Export"
type="button"
>
<div
aria-hidden="true"
class="ToolIcon__icon"
>
<svg
aria-hidden="true"
class="rtl-mirror"
focusable="false"
role="img"
viewBox="0 0 576 512"
>
<path
d="M384 121.9c0-6.3-2.5-12.4-7-16.9L279.1 7c-4.5-4.5-10.6-7-17-7H256v128h128zM571 308l-95.7-96.4c-10.1-10.1-27.4-3-27.4 11.3V288h-64v64h64v65.2c0 14.3 17.3 21.4 27.4 11.3L571 332c6.6-6.6 6.6-17.4 0-24zm-379 28v-32c0-8.8 7.2-16 16-16h176V160H248c-13.2 0-24-10.8-24-24V0H24C10.7 0 0 10.7 0 24v464c0 13.3 10.7 24 24 24h336c13.3 0 24-10.7 24-24V352H208c-8.8 0-16-7.2-16-16z"
fill="currentColor"
/>
</svg>
</div>
</button>
<button
aria-label="Reset the canvas"
class="ToolIcon_type_button ToolIcon_size_m ToolIcon_type_button--show ToolIcon"
data-testid="clear-canvas-button"
title="Reset the canvas"
type="button"
>
<div
aria-hidden="true"
class="ToolIcon__icon"
>
<svg
aria-hidden="true"
class=""
focusable="false"
role="img"
viewBox="0 0 448 512"
>
<path
d="M32 464a48 48 0 0 0 48 48h288a48 48 0 0 0 48-48V128H32zm272-256a16 16 0 0 1 32 0v224a16 16 0 0 1-32 0zm-96 0a16 16 0 0 1 32 0v224a16 16 0 0 1-32 0zm-96 0a16 16 0 0 1 32 0v224a16 16 0 0 1-32 0zM432 32H312l-9.4-18.7A24 24 0 0 0 281.1 0H166.8a23.72 23.72 0 0 0-21.4 13.3L136 32H16A16 16 0 0 0 0 48v32a16 16 0 0 0 16 16h416a16 16 0 0 0 16-16V48a16 16 0 0 0-16-16z"
fill="currentColor"
/>
</svg>
</div>
</button>
</div>
<div
style="display: flex;"
>
<div
style="position: relative;"
>
<div>
<div
class="color-picker-control-container"
>
<button
aria-label="Canvas background"
class="color-picker-label-swatch"
style="--swatch-color: #ffffff;"
/>
<label
class="color-input-container"
>
<div
class="color-picker-hash"
>
#
</div>
<input
aria-label="Canvas background"
class="color-picker-input"
spellcheck="false"
value="ffffff"
/>
</label>
</div>
</div>
</div>
<div
style="margin-inline-start: 0.25rem;"
>
<label
class="ToolIcon ToolIcon_type_floating ToolIcon_size_M"
data-testid="toggle-dark-mode"
title="Dark mode"
>
<input
aria-label="Dark mode"
class="ToolIcon_type_checkbox ToolIcon_toggle_opaque"
type="checkbox"
/>
<div
class="ToolIcon__icon"
>
<svg
class="rtl-mirror"
height="512"
viewBox="0 0 512 512"
width="512"
>
<path
d="M283.211 512c78.962 0 151.079-35.925 198.857-94.792 7.068-8.708-.639-21.43-11.562-19.35-124.203 23.654-238.262-71.576-238.262-196.954 0-72.222 38.662-138.635 101.498-174.394 9.686-5.512 7.25-20.197-3.756-22.23A258.156 258.156 0 0 0 283.211 0c-141.309 0-256 114.511-256 256 0 141.309 114.511 256 256 256z"
fill="currentColor"
/>
</svg>
</div>
</label>
</div>
</div>
</div>
</div>
</section>
`;
exports[`<Excalidraw/> Test UIOptions prop should not hide any UI element when the UIOptions prop is "undefined" 1`] = `
<section
aria-labelledby="canvasActions-title"
class="zen-mode-transition"
>
<h2
class="visually-hidden"
id="canvasActions-title"
>
Canvas actions
</h2>
<div
class="Island"
style="--padding: 2; z-index: 1;"
>
<div
class="Stack Stack_vertical"
style="--gap: 4;"
>
<div
class="Stack Stack_horizontal"
style="--gap: 1; justify-content: space-between;"
>
<button
aria-label="Load"
class="ToolIcon_type_button ToolIcon_size_m ToolIcon_type_button--show ToolIcon"
data-testid="load-button"
title="Load"
type="button"
>
<div
aria-hidden="true"
class="ToolIcon__icon"
>
<svg
aria-hidden="true"
class="rtl-mirror"
focusable="false"
role="img"
viewBox="0 0 576 512"
>
<path
d="M572.694 292.093L500.27 416.248A63.997 63.997 0 0 1 444.989 448H45.025c-18.523 0-30.064-20.093-20.731-36.093l72.424-124.155A64 64 0 0 1 152 256h399.964c18.523 0 30.064 20.093 20.73 36.093zM152 224h328v-48c0-26.51-21.49-48-48-48H272l-64-64H48C21.49 64 0 85.49 0 112v278.046l69.077-118.418C86.214 242.25 117.989 224 152 224z"
fill="currentColor"
/>
</svg>
</div>
</button>
<button
aria-label="Save"
class="ToolIcon_type_button ToolIcon_size_m ToolIcon_type_button--show ToolIcon"
data-testid="save-button"
title="Save"
type="button"
>
<div
aria-hidden="true"
class="ToolIcon__icon"
>
<svg
aria-hidden="true"
class=""
focusable="false"
role="img"
viewBox="0 0 448 512"
>
<path
d="M433.941 129.941l-83.882-83.882A48 48 0 0 0 316.118 32H48C21.49 32 0 53.49 0 80v352c0 26.51 21.49 48 48 48h352c26.51 0 48-21.49 48-48V163.882a48 48 0 0 0-14.059-33.941zM224 416c-35.346 0-64-28.654-64-64 0-35.346 28.654-64 64-64s64 28.654 64 64c0 35.346-28.654 64-64 64zm96-304.52V212c0 6.627-5.373 12-12 12H76c-6.627 0-12-5.373-12-12V108c0-6.627 5.373-12 12-12h228.52c3.183 0 6.235 1.264 8.485 3.515l3.48 3.48A11.996 11.996 0 0 1 320 111.48z"
fill="currentColor"
/>
</svg>
</div>
</button>
<button
aria-label="Save as"
class="ToolIcon_type_button ToolIcon_size_m ToolIcon_type_button--hide"
data-testid="save-as-button"
hidden=""
title="Save as"
type="button"
>
<div
aria-hidden="true"
class="ToolIcon__icon"
>
<svg
aria-hidden="true"
class=""
focusable="false"
role="img"
viewBox="0 0 448 512"
>
<path
d="M252 54L203 8a28 27 0 00-20-8H28C12 0 0 12 0 27v195c0 15 12 26 28 26h204c15 0 28-11 28-26V73a28 27 0 00-8-19zM130 213c-21 0-37-16-37-36 0-19 16-35 37-35 20 0 37 16 37 35 0 20-17 36-37 36zm56-169v56c0 4-4 6-7 6H44c-4 0-7-2-7-6V42c0-4 3-7 7-7h133l4 2 3 2a7 7 0 012 5z M296 201l87 95-188 205-78 9c-10 1-19-8-18-20l9-84zm141-14l-41-44a31 31 0 00-46 0l-38 41 87 95 38-42c13-14 13-36 0-50z"
fill="currentColor"
/>
</svg>
</div>
</button>
<button
aria-label="Export"
class="ToolIcon_type_button ToolIcon_size_m ToolIcon_type_button--show ToolIcon"
data-testid="export-button"
title="Export"
type="button"
>
<div
aria-hidden="true"
class="ToolIcon__icon"
>
<svg
aria-hidden="true"
class="rtl-mirror"
focusable="false"
role="img"
viewBox="0 0 576 512"
>
<path
d="M384 121.9c0-6.3-2.5-12.4-7-16.9L279.1 7c-4.5-4.5-10.6-7-17-7H256v128h128zM571 308l-95.7-96.4c-10.1-10.1-27.4-3-27.4 11.3V288h-64v64h64v65.2c0 14.3 17.3 21.4 27.4 11.3L571 332c6.6-6.6 6.6-17.4 0-24zm-379 28v-32c0-8.8 7.2-16 16-16h176V160H248c-13.2 0-24-10.8-24-24V0H24C10.7 0 0 10.7 0 24v464c0 13.3 10.7 24 24 24h336c13.3 0 24-10.7 24-24V352H208c-8.8 0-16-7.2-16-16z"
fill="currentColor"
/>
</svg>
</div>
</button>
<button
aria-label="Reset the canvas"
class="ToolIcon_type_button ToolIcon_size_m ToolIcon_type_button--show ToolIcon"
data-testid="clear-canvas-button"
title="Reset the canvas"
type="button"
>
<div
aria-hidden="true"
class="ToolIcon__icon"
>
<svg
aria-hidden="true"
class=""
focusable="false"
role="img"
viewBox="0 0 448 512"
>
<path
d="M32 464a48 48 0 0 0 48 48h288a48 48 0 0 0 48-48V128H32zm272-256a16 16 0 0 1 32 0v224a16 16 0 0 1-32 0zm-96 0a16 16 0 0 1 32 0v224a16 16 0 0 1-32 0zm-96 0a16 16 0 0 1 32 0v224a16 16 0 0 1-32 0zM432 32H312l-9.4-18.7A24 24 0 0 0 281.1 0H166.8a23.72 23.72 0 0 0-21.4 13.3L136 32H16A16 16 0 0 0 0 48v32a16 16 0 0 0 16 16h416a16 16 0 0 0 16-16V48a16 16 0 0 0-16-16z"
fill="currentColor"
/>
</svg>
</div>
</button>
</div>
<div
style="display: flex;"
>
<div
style="position: relative;"
>
<div>
<div
class="color-picker-control-container"
>
<button
aria-label="Canvas background"
class="color-picker-label-swatch"
style="--swatch-color: #ffffff;"
/>
<label
class="color-input-container"
>
<div
class="color-picker-hash"
>
#
</div>
<input
aria-label="Canvas background"
class="color-picker-input"
spellcheck="false"
value="ffffff"
/>
</label>
</div>
</div>
</div>
<div
style="margin-inline-start: 0.25rem;"
>
<label
class="ToolIcon ToolIcon_type_floating ToolIcon_size_M"
data-testid="toggle-dark-mode"
title="Dark mode"
>
<input
aria-label="Dark mode"
class="ToolIcon_type_checkbox ToolIcon_toggle_opaque"
type="checkbox"
/>
<div
class="ToolIcon__icon"
>
<svg
class="rtl-mirror"
height="512"
viewBox="0 0 512 512"
width="512"
>
<path
d="M283.211 512c78.962 0 151.079-35.925 198.857-94.792 7.068-8.708-.639-21.43-11.562-19.35-124.203 23.654-238.262-71.576-238.262-196.954 0-72.222 38.662-138.635 101.498-174.394 9.686-5.512 7.25-20.197-3.756-22.23A258.156 258.156 0 0 0 283.211 0c-141.309 0-256 114.511-256 256 0 141.309 114.511 256 256 256z"
fill="currentColor"
/>
</svg>
</div>
</label>
</div>
</div>
</div>
</div>
</section>
`;

View File

@ -460,7 +460,7 @@ Object {
exports[`given element A and group of elements B and given both are selected when user clicks on B, on pointer up only elements from B should be selected: [end of test] number of elements 1`] = `3`; exports[`given element A and group of elements B and given both are selected when user clicks on B, on pointer up only elements from B should be selected: [end of test] number of elements 1`] = `3`;
exports[`given element A and group of elements B and given both are selected when user clicks on B, on pointer up only elements from B should be selected: [end of test] number of renders 1`] = `26`; exports[`given element A and group of elements B and given both are selected when user clicks on B, on pointer up only elements from B should be selected: [end of test] number of renders 1`] = `27`;
exports[`given element A and group of elements B and given both are selected when user shift-clicks on B, on pointer up only element A should be selected: [end of test] appState 1`] = ` exports[`given element A and group of elements B and given both are selected when user shift-clicks on B, on pointer up only element A should be selected: [end of test] appState 1`] = `
Object { Object {
@ -928,7 +928,7 @@ Object {
exports[`given element A and group of elements B and given both are selected when user shift-clicks on B, on pointer up only element A should be selected: [end of test] number of elements 1`] = `3`; exports[`given element A and group of elements B and given both are selected when user shift-clicks on B, on pointer up only element A should be selected: [end of test] number of elements 1`] = `3`;
exports[`given element A and group of elements B and given both are selected when user shift-clicks on B, on pointer up only element A should be selected: [end of test] number of renders 1`] = `22`; exports[`given element A and group of elements B and given both are selected when user shift-clicks on B, on pointer up only element A should be selected: [end of test] number of renders 1`] = `23`;
exports[`regression tests Cmd/Ctrl-click exclusively select element under pointer: [end of test] appState 1`] = ` exports[`regression tests Cmd/Ctrl-click exclusively select element under pointer: [end of test] appState 1`] = `
Object { Object {
@ -1705,7 +1705,7 @@ Object {
exports[`regression tests Cmd/Ctrl-click exclusively select element under pointer: [end of test] number of elements 1`] = `3`; exports[`regression tests Cmd/Ctrl-click exclusively select element under pointer: [end of test] number of elements 1`] = `3`;
exports[`regression tests Cmd/Ctrl-click exclusively select element under pointer: [end of test] number of renders 1`] = `41`; exports[`regression tests Cmd/Ctrl-click exclusively select element under pointer: [end of test] number of renders 1`] = `42`;
exports[`regression tests Drags selected element when hitting only bounding box and keeps element selected: [end of test] appState 1`] = ` exports[`regression tests Drags selected element when hitting only bounding box and keeps element selected: [end of test] appState 1`] = `
Object { Object {
@ -1910,7 +1910,7 @@ Object {
exports[`regression tests Drags selected element when hitting only bounding box and keeps element selected: [end of test] number of elements 1`] = `1`; exports[`regression tests Drags selected element when hitting only bounding box and keeps element selected: [end of test] number of elements 1`] = `1`;
exports[`regression tests Drags selected element when hitting only bounding box and keeps element selected: [end of test] number of renders 1`] = `10`; exports[`regression tests Drags selected element when hitting only bounding box and keeps element selected: [end of test] number of renders 1`] = `11`;
exports[`regression tests adjusts z order when grouping: [end of test] appState 1`] = ` exports[`regression tests adjusts z order when grouping: [end of test] appState 1`] = `
Object { Object {
@ -2369,7 +2369,7 @@ Object {
exports[`regression tests adjusts z order when grouping: [end of test] number of elements 1`] = `3`; exports[`regression tests adjusts z order when grouping: [end of test] number of elements 1`] = `3`;
exports[`regression tests adjusts z order when grouping: [end of test] number of renders 1`] = `20`; exports[`regression tests adjusts z order when grouping: [end of test] number of renders 1`] = `21`;
exports[`regression tests alt-drag duplicates an element: [end of test] appState 1`] = ` exports[`regression tests alt-drag duplicates an element: [end of test] appState 1`] = `
Object { Object {
@ -2623,7 +2623,7 @@ Object {
exports[`regression tests alt-drag duplicates an element: [end of test] number of elements 1`] = `2`; exports[`regression tests alt-drag duplicates an element: [end of test] number of elements 1`] = `2`;
exports[`regression tests alt-drag duplicates an element: [end of test] number of renders 1`] = `10`; exports[`regression tests alt-drag duplicates an element: [end of test] number of renders 1`] = `11`;
exports[`regression tests arrow keys: [end of test] appState 1`] = ` exports[`regression tests arrow keys: [end of test] appState 1`] = `
Object { Object {
@ -2788,7 +2788,7 @@ Object {
exports[`regression tests arrow keys: [end of test] number of elements 1`] = `1`; exports[`regression tests arrow keys: [end of test] number of elements 1`] = `1`;
exports[`regression tests arrow keys: [end of test] number of renders 1`] = `19`; exports[`regression tests arrow keys: [end of test] number of renders 1`] = `20`;
exports[`regression tests can drag element that covers another element, while another elem is selected: [end of test] appState 1`] = ` exports[`regression tests can drag element that covers another element, while another elem is selected: [end of test] appState 1`] = `
Object { Object {
@ -3266,7 +3266,7 @@ Object {
exports[`regression tests can drag element that covers another element, while another elem is selected: [end of test] number of elements 1`] = `3`; exports[`regression tests can drag element that covers another element, while another elem is selected: [end of test] number of elements 1`] = `3`;
exports[`regression tests can drag element that covers another element, while another elem is selected: [end of test] number of renders 1`] = `18`; exports[`regression tests can drag element that covers another element, while another elem is selected: [end of test] number of renders 1`] = `19`;
exports[`regression tests change the properties of a shape: [end of test] appState 1`] = ` exports[`regression tests change the properties of a shape: [end of test] appState 1`] = `
Object { Object {
@ -3503,7 +3503,7 @@ Object {
exports[`regression tests change the properties of a shape: [end of test] number of elements 1`] = `1`; exports[`regression tests change the properties of a shape: [end of test] number of elements 1`] = `1`;
exports[`regression tests change the properties of a shape: [end of test] number of renders 1`] = `11`; exports[`regression tests change the properties of a shape: [end of test] number of renders 1`] = `12`;
exports[`regression tests click on an element and drag it: [dragged] appState 1`] = ` exports[`regression tests click on an element and drag it: [dragged] appState 1`] = `
Object { Object {
@ -3708,7 +3708,7 @@ Object {
exports[`regression tests click on an element and drag it: [dragged] number of elements 1`] = `1`; exports[`regression tests click on an element and drag it: [dragged] number of elements 1`] = `1`;
exports[`regression tests click on an element and drag it: [dragged] number of renders 1`] = `10`; exports[`regression tests click on an element and drag it: [dragged] number of renders 1`] = `11`;
exports[`regression tests click on an element and drag it: [end of test] appState 1`] = ` exports[`regression tests click on an element and drag it: [end of test] appState 1`] = `
Object { Object {
@ -3953,7 +3953,7 @@ Object {
exports[`regression tests click on an element and drag it: [end of test] number of elements 1`] = `1`; exports[`regression tests click on an element and drag it: [end of test] number of elements 1`] = `1`;
exports[`regression tests click on an element and drag it: [end of test] number of renders 1`] = `13`; exports[`regression tests click on an element and drag it: [end of test] number of renders 1`] = `14`;
exports[`regression tests click to select a shape: [end of test] appState 1`] = ` exports[`regression tests click to select a shape: [end of test] appState 1`] = `
Object { Object {
@ -4206,7 +4206,7 @@ Object {
exports[`regression tests click to select a shape: [end of test] number of elements 1`] = `2`; exports[`regression tests click to select a shape: [end of test] number of elements 1`] = `2`;
exports[`regression tests click to select a shape: [end of test] number of renders 1`] = `13`; exports[`regression tests click to select a shape: [end of test] number of renders 1`] = `14`;
exports[`regression tests click-drag to select a group: [end of test] appState 1`] = ` exports[`regression tests click-drag to select a group: [end of test] appState 1`] = `
Object { Object {
@ -4568,7 +4568,7 @@ Object {
exports[`regression tests click-drag to select a group: [end of test] number of elements 1`] = `3`; exports[`regression tests click-drag to select a group: [end of test] number of elements 1`] = `3`;
exports[`regression tests click-drag to select a group: [end of test] number of renders 1`] = `19`; exports[`regression tests click-drag to select a group: [end of test] number of renders 1`] = `20`;
exports[`regression tests deselects group of selected elements on pointer down when pointer doesn't hit any element: [end of test] appState 1`] = ` exports[`regression tests deselects group of selected elements on pointer down when pointer doesn't hit any element: [end of test] appState 1`] = `
Object { Object {
@ -4864,7 +4864,7 @@ Object {
exports[`regression tests deselects group of selected elements on pointer down when pointer doesn't hit any element: [end of test] number of elements 1`] = `2`; exports[`regression tests deselects group of selected elements on pointer down when pointer doesn't hit any element: [end of test] number of elements 1`] = `2`;
exports[`regression tests deselects group of selected elements on pointer down when pointer doesn't hit any element: [end of test] number of renders 1`] = `14`; exports[`regression tests deselects group of selected elements on pointer down when pointer doesn't hit any element: [end of test] number of renders 1`] = `15`;
exports[`regression tests deselects group of selected elements on pointer up when pointer hits common bounding box without hitting any element: [end of test] appState 1`] = ` exports[`regression tests deselects group of selected elements on pointer up when pointer hits common bounding box without hitting any element: [end of test] appState 1`] = `
Object { Object {
@ -5172,7 +5172,7 @@ Object {
exports[`regression tests deselects group of selected elements on pointer up when pointer hits common bounding box without hitting any element: [end of test] number of elements 1`] = `2`; exports[`regression tests deselects group of selected elements on pointer up when pointer hits common bounding box without hitting any element: [end of test] number of elements 1`] = `2`;
exports[`regression tests deselects group of selected elements on pointer up when pointer hits common bounding box without hitting any element: [end of test] number of renders 1`] = `15`; exports[`regression tests deselects group of selected elements on pointer up when pointer hits common bounding box without hitting any element: [end of test] number of renders 1`] = `16`;
exports[`regression tests deselects selected element on pointer down when pointer doesn't hit any element: [end of test] appState 1`] = ` exports[`regression tests deselects selected element on pointer down when pointer doesn't hit any element: [end of test] appState 1`] = `
Object { Object {
@ -5381,7 +5381,7 @@ Object {
exports[`regression tests deselects selected element on pointer down when pointer doesn't hit any element: [end of test] number of elements 1`] = `1`; exports[`regression tests deselects selected element on pointer down when pointer doesn't hit any element: [end of test] number of elements 1`] = `1`;
exports[`regression tests deselects selected element on pointer down when pointer doesn't hit any element: [end of test] number of renders 1`] = `8`; exports[`regression tests deselects selected element on pointer down when pointer doesn't hit any element: [end of test] number of renders 1`] = `9`;
exports[`regression tests deselects selected element, on pointer up, when click hits element bounding box but doesn't hit the element: [end of test] appState 1`] = ` exports[`regression tests deselects selected element, on pointer up, when click hits element bounding box but doesn't hit the element: [end of test] appState 1`] = `
Object { Object {
@ -5568,7 +5568,7 @@ Object {
exports[`regression tests deselects selected element, on pointer up, when click hits element bounding box but doesn't hit the element: [end of test] number of elements 1`] = `1`; exports[`regression tests deselects selected element, on pointer up, when click hits element bounding box but doesn't hit the element: [end of test] number of elements 1`] = `1`;
exports[`regression tests deselects selected element, on pointer up, when click hits element bounding box but doesn't hit the element: [end of test] number of renders 1`] = `9`; exports[`regression tests deselects selected element, on pointer up, when click hits element bounding box but doesn't hit the element: [end of test] number of renders 1`] = `10`;
exports[`regression tests double click to edit a group: [end of test] appState 1`] = ` exports[`regression tests double click to edit a group: [end of test] appState 1`] = `
Object { Object {
@ -6022,7 +6022,7 @@ Object {
exports[`regression tests double click to edit a group: [end of test] number of elements 1`] = `3`; exports[`regression tests double click to edit a group: [end of test] number of elements 1`] = `3`;
exports[`regression tests double click to edit a group: [end of test] number of renders 1`] = `18`; exports[`regression tests double click to edit a group: [end of test] number of renders 1`] = `19`;
exports[`regression tests drags selected elements from point inside common bounding box that doesn't hit any element and keeps elements selected after dragging: [end of test] appState 1`] = ` exports[`regression tests drags selected elements from point inside common bounding box that doesn't hit any element and keeps elements selected after dragging: [end of test] appState 1`] = `
Object { Object {
@ -6341,7 +6341,7 @@ Object {
exports[`regression tests drags selected elements from point inside common bounding box that doesn't hit any element and keeps elements selected after dragging: [end of test] number of elements 1`] = `2`; exports[`regression tests drags selected elements from point inside common bounding box that doesn't hit any element and keeps elements selected after dragging: [end of test] number of elements 1`] = `2`;
exports[`regression tests drags selected elements from point inside common bounding box that doesn't hit any element and keeps elements selected after dragging: [end of test] number of renders 1`] = `16`; exports[`regression tests drags selected elements from point inside common bounding box that doesn't hit any element and keeps elements selected after dragging: [end of test] number of renders 1`] = `17`;
exports[`regression tests draw every type of shape: [end of test] appState 1`] = ` exports[`regression tests draw every type of shape: [end of test] appState 1`] = `
Object { Object {
@ -8376,7 +8376,7 @@ Object {
exports[`regression tests draw every type of shape: [end of test] number of elements 1`] = `8`; exports[`regression tests draw every type of shape: [end of test] number of elements 1`] = `8`;
exports[`regression tests draw every type of shape: [end of test] number of renders 1`] = `51`; exports[`regression tests draw every type of shape: [end of test] number of renders 1`] = `52`;
exports[`regression tests given a group of selected elements with an element that is not selected inside the group common bounding box when element that is not selected is clicked should switch selection to not selected element on pointer up: [end of test] appState 1`] = ` exports[`regression tests given a group of selected elements with an element that is not selected inside the group common bounding box when element that is not selected is clicked should switch selection to not selected element on pointer up: [end of test] appState 1`] = `
Object { Object {
@ -8739,7 +8739,7 @@ Object {
exports[`regression tests given a group of selected elements with an element that is not selected inside the group common bounding box when element that is not selected is clicked should switch selection to not selected element on pointer up: [end of test] number of elements 1`] = `3`; exports[`regression tests given a group of selected elements with an element that is not selected inside the group common bounding box when element that is not selected is clicked should switch selection to not selected element on pointer up: [end of test] number of elements 1`] = `3`;
exports[`regression tests given a group of selected elements with an element that is not selected inside the group common bounding box when element that is not selected is clicked should switch selection to not selected element on pointer up: [end of test] number of renders 1`] = `19`; exports[`regression tests given a group of selected elements with an element that is not selected inside the group common bounding box when element that is not selected is clicked should switch selection to not selected element on pointer up: [end of test] number of renders 1`] = `20`;
exports[`regression tests given a selected element A and a not selected element B with higher z-index than A and given B partialy overlaps A when there's a shift-click on the overlapped section B is added to the selection: [end of test] appState 1`] = ` exports[`regression tests given a selected element A and a not selected element B with higher z-index than A and given B partialy overlaps A when there's a shift-click on the overlapped section B is added to the selection: [end of test] appState 1`] = `
Object { Object {
@ -8995,7 +8995,7 @@ Object {
exports[`regression tests given a selected element A and a not selected element B with higher z-index than A and given B partialy overlaps A when there's a shift-click on the overlapped section B is added to the selection: [end of test] number of elements 1`] = `2`; exports[`regression tests given a selected element A and a not selected element B with higher z-index than A and given B partialy overlaps A when there's a shift-click on the overlapped section B is added to the selection: [end of test] number of elements 1`] = `2`;
exports[`regression tests given a selected element A and a not selected element B with higher z-index than A and given B partialy overlaps A when there's a shift-click on the overlapped section B is added to the selection: [end of test] number of renders 1`] = `17`; exports[`regression tests given a selected element A and a not selected element B with higher z-index than A and given B partialy overlaps A when there's a shift-click on the overlapped section B is added to the selection: [end of test] number of renders 1`] = `18`;
exports[`regression tests given selected element A with lower z-index than unselected element B and given B is partially over A when clicking intersection between A and B B should be selected on pointer up: [end of test] appState 1`] = ` exports[`regression tests given selected element A with lower z-index than unselected element B and given B is partially over A when clicking intersection between A and B B should be selected on pointer up: [end of test] appState 1`] = `
Object { Object {
@ -9249,7 +9249,7 @@ Object {
exports[`regression tests given selected element A with lower z-index than unselected element B and given B is partially over A when clicking intersection between A and B B should be selected on pointer up: [end of test] number of elements 1`] = `2`; exports[`regression tests given selected element A with lower z-index than unselected element B and given B is partially over A when clicking intersection between A and B B should be selected on pointer up: [end of test] number of elements 1`] = `2`;
exports[`regression tests given selected element A with lower z-index than unselected element B and given B is partially over A when clicking intersection between A and B B should be selected on pointer up: [end of test] number of renders 1`] = `17`; exports[`regression tests given selected element A with lower z-index than unselected element B and given B is partially over A when clicking intersection between A and B B should be selected on pointer up: [end of test] number of renders 1`] = `18`;
exports[`regression tests given selected element A with lower z-index than unselected element B and given B is partially over A when dragging on intersection between A and B A should be dragged and keep being selected: [end of test] appState 1`] = ` exports[`regression tests given selected element A with lower z-index than unselected element B and given B is partially over A when dragging on intersection between A and B A should be dragged and keep being selected: [end of test] appState 1`] = `
Object { Object {
@ -9565,7 +9565,7 @@ Object {
exports[`regression tests given selected element A with lower z-index than unselected element B and given B is partially over A when dragging on intersection between A and B A should be dragged and keep being selected: [end of test] number of elements 1`] = `2`; exports[`regression tests given selected element A with lower z-index than unselected element B and given B is partially over A when dragging on intersection between A and B A should be dragged and keep being selected: [end of test] number of elements 1`] = `2`;
exports[`regression tests given selected element A with lower z-index than unselected element B and given B is partially over A when dragging on intersection between A and B A should be dragged and keep being selected: [end of test] number of renders 1`] = `18`; exports[`regression tests given selected element A with lower z-index than unselected element B and given B is partially over A when dragging on intersection between A and B A should be dragged and keep being selected: [end of test] number of renders 1`] = `19`;
exports[`regression tests key 2 selects rectangle tool: [end of test] appState 1`] = ` exports[`regression tests key 2 selects rectangle tool: [end of test] appState 1`] = `
Object { Object {
@ -9730,7 +9730,7 @@ Object {
exports[`regression tests key 2 selects rectangle tool: [end of test] number of elements 1`] = `1`; exports[`regression tests key 2 selects rectangle tool: [end of test] number of elements 1`] = `1`;
exports[`regression tests key 2 selects rectangle tool: [end of test] number of renders 1`] = `7`; exports[`regression tests key 2 selects rectangle tool: [end of test] number of renders 1`] = `8`;
exports[`regression tests key 3 selects diamond tool: [end of test] appState 1`] = ` exports[`regression tests key 3 selects diamond tool: [end of test] appState 1`] = `
Object { Object {
@ -9895,7 +9895,7 @@ Object {
exports[`regression tests key 3 selects diamond tool: [end of test] number of elements 1`] = `1`; exports[`regression tests key 3 selects diamond tool: [end of test] number of elements 1`] = `1`;
exports[`regression tests key 3 selects diamond tool: [end of test] number of renders 1`] = `7`; exports[`regression tests key 3 selects diamond tool: [end of test] number of renders 1`] = `8`;
exports[`regression tests key 4 selects ellipse tool: [end of test] appState 1`] = ` exports[`regression tests key 4 selects ellipse tool: [end of test] appState 1`] = `
Object { Object {
@ -10060,7 +10060,7 @@ Object {
exports[`regression tests key 4 selects ellipse tool: [end of test] number of elements 1`] = `1`; exports[`regression tests key 4 selects ellipse tool: [end of test] number of elements 1`] = `1`;
exports[`regression tests key 4 selects ellipse tool: [end of test] number of renders 1`] = `7`; exports[`regression tests key 4 selects ellipse tool: [end of test] number of renders 1`] = `8`;
exports[`regression tests key 5 selects arrow tool: [end of test] appState 1`] = ` exports[`regression tests key 5 selects arrow tool: [end of test] appState 1`] = `
Object { Object {
@ -10255,7 +10255,7 @@ Object {
exports[`regression tests key 5 selects arrow tool: [end of test] number of elements 1`] = `1`; exports[`regression tests key 5 selects arrow tool: [end of test] number of elements 1`] = `1`;
exports[`regression tests key 5 selects arrow tool: [end of test] number of renders 1`] = `8`; exports[`regression tests key 5 selects arrow tool: [end of test] number of renders 1`] = `9`;
exports[`regression tests key 6 selects line tool: [end of test] appState 1`] = ` exports[`regression tests key 6 selects line tool: [end of test] appState 1`] = `
Object { Object {
@ -10450,7 +10450,7 @@ Object {
exports[`regression tests key 6 selects line tool: [end of test] number of elements 1`] = `1`; exports[`regression tests key 6 selects line tool: [end of test] number of elements 1`] = `1`;
exports[`regression tests key 6 selects line tool: [end of test] number of renders 1`] = `7`; exports[`regression tests key 6 selects line tool: [end of test] number of renders 1`] = `8`;
exports[`regression tests key 7 selects draw tool: [end of test] appState 1`] = ` exports[`regression tests key 7 selects draw tool: [end of test] appState 1`] = `
Object { Object {
@ -10645,7 +10645,7 @@ Object {
exports[`regression tests key 7 selects draw tool: [end of test] number of elements 1`] = `1`; exports[`regression tests key 7 selects draw tool: [end of test] number of elements 1`] = `1`;
exports[`regression tests key 7 selects draw tool: [end of test] number of renders 1`] = `7`; exports[`regression tests key 7 selects draw tool: [end of test] number of renders 1`] = `8`;
exports[`regression tests key a selects arrow tool: [end of test] appState 1`] = ` exports[`regression tests key a selects arrow tool: [end of test] appState 1`] = `
Object { Object {
@ -10840,7 +10840,7 @@ Object {
exports[`regression tests key a selects arrow tool: [end of test] number of elements 1`] = `1`; exports[`regression tests key a selects arrow tool: [end of test] number of elements 1`] = `1`;
exports[`regression tests key a selects arrow tool: [end of test] number of renders 1`] = `8`; exports[`regression tests key a selects arrow tool: [end of test] number of renders 1`] = `9`;
exports[`regression tests key d selects diamond tool: [end of test] appState 1`] = ` exports[`regression tests key d selects diamond tool: [end of test] appState 1`] = `
Object { Object {
@ -11005,7 +11005,7 @@ Object {
exports[`regression tests key d selects diamond tool: [end of test] number of elements 1`] = `1`; exports[`regression tests key d selects diamond tool: [end of test] number of elements 1`] = `1`;
exports[`regression tests key d selects diamond tool: [end of test] number of renders 1`] = `7`; exports[`regression tests key d selects diamond tool: [end of test] number of renders 1`] = `8`;
exports[`regression tests key e selects ellipse tool: [end of test] appState 1`] = ` exports[`regression tests key e selects ellipse tool: [end of test] appState 1`] = `
Object { Object {
@ -11170,7 +11170,7 @@ Object {
exports[`regression tests key e selects ellipse tool: [end of test] number of elements 1`] = `1`; exports[`regression tests key e selects ellipse tool: [end of test] number of elements 1`] = `1`;
exports[`regression tests key e selects ellipse tool: [end of test] number of renders 1`] = `7`; exports[`regression tests key e selects ellipse tool: [end of test] number of renders 1`] = `8`;
exports[`regression tests key l selects line tool: [end of test] appState 1`] = ` exports[`regression tests key l selects line tool: [end of test] appState 1`] = `
Object { Object {
@ -11365,7 +11365,7 @@ Object {
exports[`regression tests key l selects line tool: [end of test] number of elements 1`] = `1`; exports[`regression tests key l selects line tool: [end of test] number of elements 1`] = `1`;
exports[`regression tests key l selects line tool: [end of test] number of renders 1`] = `7`; exports[`regression tests key l selects line tool: [end of test] number of renders 1`] = `8`;
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 {
@ -11530,7 +11530,7 @@ Object {
exports[`regression tests key r selects rectangle tool: [end of test] number of elements 1`] = `1`; exports[`regression tests key r selects rectangle tool: [end of test] number of elements 1`] = `1`;
exports[`regression tests key r selects rectangle tool: [end of test] number of renders 1`] = `7`; exports[`regression tests key r selects rectangle tool: [end of test] number of renders 1`] = `8`;
exports[`regression tests key x selects draw tool: [end of test] appState 1`] = ` exports[`regression tests key x selects draw tool: [end of test] appState 1`] = `
Object { Object {
@ -11725,7 +11725,7 @@ Object {
exports[`regression tests key x selects draw tool: [end of test] number of elements 1`] = `1`; exports[`regression tests key x selects draw tool: [end of test] number of elements 1`] = `1`;
exports[`regression tests key x selects draw tool: [end of test] number of renders 1`] = `7`; exports[`regression tests key x selects draw tool: [end of test] number of renders 1`] = `8`;
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 {
@ -12442,7 +12442,7 @@ Object {
exports[`regression tests make a group and duplicate it: [end of test] number of elements 1`] = `6`; exports[`regression tests make a group and duplicate it: [end of test] number of elements 1`] = `6`;
exports[`regression tests make a group and duplicate it: [end of test] number of renders 1`] = `22`; exports[`regression tests make a group and duplicate it: [end of test] number of renders 1`] = `23`;
exports[`regression tests noop interaction after undo shouldn't create history entry: [end of test] appState 1`] = ` exports[`regression tests noop interaction after undo shouldn't create history entry: [end of test] appState 1`] = `
Object { Object {
@ -12696,7 +12696,7 @@ Object {
exports[`regression tests noop interaction after undo shouldn't create history entry: [end of test] number of elements 1`] = `2`; exports[`regression tests noop interaction after undo shouldn't create history entry: [end of test] number of elements 1`] = `2`;
exports[`regression tests noop interaction after undo shouldn't create history entry: [end of test] number of renders 1`] = `19`; exports[`regression tests noop interaction after undo shouldn't create history entry: [end of test] number of renders 1`] = `20`;
exports[`regression tests pinch-to-zoom works: [end of test] appState 1`] = ` exports[`regression tests pinch-to-zoom works: [end of test] appState 1`] = `
Object { Object {
@ -12799,7 +12799,7 @@ Object {
exports[`regression tests pinch-to-zoom works: [end of test] number of elements 1`] = `0`; exports[`regression tests pinch-to-zoom works: [end of test] number of elements 1`] = `0`;
exports[`regression tests pinch-to-zoom works: [end of test] number of renders 1`] = `9`; exports[`regression tests pinch-to-zoom works: [end of test] number of renders 1`] = `10`;
exports[`regression tests rerenders UI on language change: [end of test] appState 1`] = ` exports[`regression tests rerenders UI on language change: [end of test] appState 1`] = `
Object { Object {
@ -12900,7 +12900,7 @@ Object {
exports[`regression tests rerenders UI on language change: [end of test] number of elements 1`] = `0`; exports[`regression tests rerenders UI on language change: [end of test] number of elements 1`] = `0`;
exports[`regression tests rerenders UI on language change: [end of test] number of renders 1`] = `8`; exports[`regression tests rerenders UI on language change: [end of test] number of renders 1`] = `9`;
exports[`regression tests selecting 'Add to library' in context menu adds element to library: [end of test] appState 1`] = ` exports[`regression tests selecting 'Add to library' in context menu adds element to library: [end of test] appState 1`] = `
Object { Object {
@ -13065,7 +13065,7 @@ Object {
exports[`regression tests selecting 'Add to library' in context menu adds element to library: [end of test] number of elements 1`] = `1`; exports[`regression tests selecting 'Add to library' in context menu adds element to library: [end of test] number of elements 1`] = `1`;
exports[`regression tests selecting 'Add to library' in context menu adds element to library: [end of test] number of renders 1`] = `7`; exports[`regression tests selecting 'Add to library' in context menu adds element to library: [end of test] number of renders 1`] = `8`;
exports[`regression tests selecting 'Bring forward' in context menu brings element forward: [end of test] appState 1`] = ` exports[`regression tests selecting 'Bring forward' in context menu brings element forward: [end of test] appState 1`] = `
Object { Object {
@ -13374,7 +13374,7 @@ Object {
exports[`regression tests selecting 'Bring forward' in context menu brings element forward: [end of test] number of elements 1`] = `2`; exports[`regression tests selecting 'Bring forward' in context menu brings element forward: [end of test] number of elements 1`] = `2`;
exports[`regression tests selecting 'Bring forward' in context menu brings element forward: [end of test] number of renders 1`] = `13`; exports[`regression tests selecting 'Bring forward' in context menu brings element forward: [end of test] number of renders 1`] = `14`;
exports[`regression tests selecting 'Bring to front' in context menu brings element to front: [end of test] appState 1`] = ` exports[`regression tests selecting 'Bring to front' in context menu brings element to front: [end of test] appState 1`] = `
Object { Object {
@ -13683,7 +13683,7 @@ Object {
exports[`regression tests selecting 'Bring to front' in context menu brings element to front: [end of test] number of elements 1`] = `2`; exports[`regression tests selecting 'Bring to front' in context menu brings element to front: [end of test] number of elements 1`] = `2`;
exports[`regression tests selecting 'Bring to front' in context menu brings element to front: [end of test] number of renders 1`] = `13`; exports[`regression tests selecting 'Bring to front' in context menu brings element to front: [end of test] number of renders 1`] = `14`;
exports[`regression tests selecting 'Copy styles' in context menu copies styles: [end of test] appState 1`] = ` exports[`regression tests selecting 'Copy styles' in context menu copies styles: [end of test] appState 1`] = `
Object { Object {
@ -13848,7 +13848,7 @@ Object {
exports[`regression tests selecting 'Copy styles' in context menu copies styles: [end of test] number of elements 1`] = `1`; exports[`regression tests selecting 'Copy styles' in context menu copies styles: [end of test] number of elements 1`] = `1`;
exports[`regression tests selecting 'Copy styles' in context menu copies styles: [end of test] number of renders 1`] = `8`; exports[`regression tests selecting 'Copy styles' in context menu copies styles: [end of test] number of renders 1`] = `9`;
exports[`regression tests selecting 'Delete' in context menu deletes element: [end of test] appState 1`] = ` exports[`regression tests selecting 'Delete' in context menu deletes element: [end of test] appState 1`] = `
Object { Object {
@ -14045,7 +14045,7 @@ Object {
exports[`regression tests selecting 'Delete' in context menu deletes element: [end of test] number of elements 1`] = `1`; exports[`regression tests selecting 'Delete' in context menu deletes element: [end of test] number of elements 1`] = `1`;
exports[`regression tests selecting 'Delete' in context menu deletes element: [end of test] number of renders 1`] = `8`; exports[`regression tests selecting 'Delete' in context menu deletes element: [end of test] number of renders 1`] = `9`;
exports[`regression tests selecting 'Duplicate' in context menu duplicates element: [end of test] appState 1`] = ` exports[`regression tests selecting 'Duplicate' in context menu duplicates element: [end of test] appState 1`] = `
Object { Object {
@ -14295,7 +14295,7 @@ Object {
exports[`regression tests selecting 'Duplicate' in context menu duplicates element: [end of test] number of elements 1`] = `2`; exports[`regression tests selecting 'Duplicate' in context menu duplicates element: [end of test] number of elements 1`] = `2`;
exports[`regression tests selecting 'Duplicate' in context menu duplicates element: [end of test] number of renders 1`] = `8`; exports[`regression tests selecting 'Duplicate' in context menu duplicates element: [end of test] number of renders 1`] = `9`;
exports[`regression tests selecting 'Group selection' in context menu groups selected elements: [end of test] appState 1`] = ` exports[`regression tests selecting 'Group selection' in context menu groups selected elements: [end of test] appState 1`] = `
Object { Object {
@ -14620,7 +14620,7 @@ Object {
exports[`regression tests selecting 'Group selection' in context menu groups selected elements: [end of test] number of elements 1`] = `2`; exports[`regression tests selecting 'Group selection' in context menu groups selected elements: [end of test] number of elements 1`] = `2`;
exports[`regression tests selecting 'Group selection' in context menu groups selected elements: [end of test] number of renders 1`] = `14`; exports[`regression tests selecting 'Group selection' in context menu groups selected elements: [end of test] number of renders 1`] = `15`;
exports[`regression tests selecting 'Paste styles' in context menu pastes styles: [end of test] appState 1`] = ` exports[`regression tests selecting 'Paste styles' in context menu pastes styles: [end of test] appState 1`] = `
Object { Object {
@ -15342,7 +15342,7 @@ Object {
exports[`regression tests selecting 'Paste styles' in context menu pastes styles: [end of test] number of elements 1`] = `2`; exports[`regression tests selecting 'Paste styles' in context menu pastes styles: [end of test] number of elements 1`] = `2`;
exports[`regression tests selecting 'Paste styles' in context menu pastes styles: [end of test] number of renders 1`] = `23`; exports[`regression tests selecting 'Paste styles' in context menu pastes styles: [end of test] number of renders 1`] = `24`;
exports[`regression tests selecting 'Send backward' in context menu sends element backward: [end of test] appState 1`] = ` exports[`regression tests selecting 'Send backward' in context menu sends element backward: [end of test] appState 1`] = `
Object { Object {
@ -15651,7 +15651,7 @@ Object {
exports[`regression tests selecting 'Send backward' in context menu sends element backward: [end of test] number of elements 1`] = `2`; exports[`regression tests selecting 'Send backward' in context menu sends element backward: [end of test] number of elements 1`] = `2`;
exports[`regression tests selecting 'Send backward' in context menu sends element backward: [end of test] number of renders 1`] = `12`; exports[`regression tests selecting 'Send backward' in context menu sends element backward: [end of test] number of renders 1`] = `13`;
exports[`regression tests selecting 'Send to back' in context menu sends element to back: [end of test] appState 1`] = ` exports[`regression tests selecting 'Send to back' in context menu sends element to back: [end of test] appState 1`] = `
Object { Object {
@ -15960,7 +15960,7 @@ Object {
exports[`regression tests selecting 'Send to back' in context menu sends element to back: [end of test] number of elements 1`] = `2`; exports[`regression tests selecting 'Send to back' in context menu sends element to back: [end of test] number of elements 1`] = `2`;
exports[`regression tests selecting 'Send to back' in context menu sends element to back: [end of test] number of renders 1`] = `12`; exports[`regression tests selecting 'Send to back' in context menu sends element to back: [end of test] number of renders 1`] = `13`;
exports[`regression tests selecting 'Ungroup selection' in context menu ungroups selected group: [end of test] appState 1`] = ` exports[`regression tests selecting 'Ungroup selection' in context menu ungroups selected group: [end of test] appState 1`] = `
Object { Object {
@ -16340,7 +16340,7 @@ Object {
exports[`regression tests selecting 'Ungroup selection' in context menu ungroups selected group: [end of test] number of elements 1`] = `2`; exports[`regression tests selecting 'Ungroup selection' in context menu ungroups selected group: [end of test] number of elements 1`] = `2`;
exports[`regression tests selecting 'Ungroup selection' in context menu ungroups selected group: [end of test] number of renders 1`] = `15`; exports[`regression tests selecting 'Ungroup selection' in context menu ungroups selected group: [end of test] number of renders 1`] = `16`;
exports[`regression tests shift click on selected element should deselect it on pointer up: [end of test] appState 1`] = ` exports[`regression tests shift click on selected element should deselect it on pointer up: [end of test] appState 1`] = `
Object { Object {
@ -16508,7 +16508,7 @@ Object {
exports[`regression tests shift click on selected element should deselect it on pointer up: [end of test] number of elements 1`] = `1`; exports[`regression tests shift click on selected element should deselect it on pointer up: [end of test] number of elements 1`] = `1`;
exports[`regression tests shift click on selected element should deselect it on pointer up: [end of test] number of renders 1`] = `9`; exports[`regression tests shift click on selected element should deselect it on pointer up: [end of test] number of renders 1`] = `10`;
exports[`regression tests shift-click to multiselect, then drag: [end of test] appState 1`] = ` exports[`regression tests shift-click to multiselect, then drag: [end of test] appState 1`] = `
Object { Object {
@ -16830,7 +16830,7 @@ Object {
exports[`regression tests shift-click to multiselect, then drag: [end of test] number of elements 1`] = `2`; exports[`regression tests shift-click to multiselect, then drag: [end of test] number of elements 1`] = `2`;
exports[`regression tests shift-click to multiselect, then drag: [end of test] number of renders 1`] = `18`; exports[`regression tests shift-click to multiselect, then drag: [end of test] number of renders 1`] = `19`;
exports[`regression tests should show fill icons when element has non transparent background: [end of test] appState 1`] = ` exports[`regression tests should show fill icons when element has non transparent background: [end of test] appState 1`] = `
Object { Object {
@ -17034,7 +17034,7 @@ Object {
exports[`regression tests should show fill icons when element has non transparent background: [end of test] number of elements 1`] = `1`; exports[`regression tests should show fill icons when element has non transparent background: [end of test] number of elements 1`] = `1`;
exports[`regression tests should show fill icons when element has non transparent background: [end of test] number of renders 1`] = `11`; exports[`regression tests should show fill icons when element has non transparent background: [end of test] number of renders 1`] = `12`;
exports[`regression tests shows 'Group selection' in context menu for multiple selected elements: [end of test] appState 1`] = ` exports[`regression tests shows 'Group selection' in context menu for multiple selected elements: [end of test] appState 1`] = `
Object { Object {
@ -17290,7 +17290,7 @@ Object {
exports[`regression tests shows 'Group selection' in context menu for multiple selected elements: [end of test] number of elements 1`] = `2`; exports[`regression tests shows 'Group selection' in context menu for multiple selected elements: [end of test] number of elements 1`] = `2`;
exports[`regression tests shows 'Group selection' in context menu for multiple selected elements: [end of test] number of renders 1`] = `15`; exports[`regression tests shows 'Group selection' in context menu for multiple selected elements: [end of test] number of renders 1`] = `16`;
exports[`regression tests shows 'Ungroup selection' in context menu for group inside selected elements: [end of test] appState 1`] = ` exports[`regression tests shows 'Ungroup selection' in context menu for group inside selected elements: [end of test] appState 1`] = `
Object { Object {
@ -17618,7 +17618,7 @@ Object {
exports[`regression tests shows 'Ungroup selection' in context menu for group inside selected elements: [end of test] number of elements 1`] = `2`; exports[`regression tests shows 'Ungroup selection' in context menu for group inside selected elements: [end of test] number of elements 1`] = `2`;
exports[`regression tests shows 'Ungroup selection' in context menu for group inside selected elements: [end of test] number of renders 1`] = `16`; exports[`regression tests shows 'Ungroup selection' in context menu for group inside selected elements: [end of test] number of renders 1`] = `17`;
exports[`regression tests shows context menu for canvas: [end of test] appState 1`] = ` exports[`regression tests shows context menu for canvas: [end of test] appState 1`] = `
Object { Object {
@ -17719,7 +17719,7 @@ Object {
exports[`regression tests shows context menu for canvas: [end of test] number of elements 1`] = `0`; exports[`regression tests shows context menu for canvas: [end of test] number of elements 1`] = `0`;
exports[`regression tests shows context menu for canvas: [end of test] number of renders 1`] = `3`; exports[`regression tests shows context menu for canvas: [end of test] number of renders 1`] = `4`;
exports[`regression tests shows context menu for element: [end of test] appState 1`] = ` exports[`regression tests shows context menu for element: [end of test] appState 1`] = `
Object { Object {
@ -17884,7 +17884,7 @@ Object {
exports[`regression tests shows context menu for element: [end of test] number of elements 1`] = `1`; exports[`regression tests shows context menu for element: [end of test] number of elements 1`] = `1`;
exports[`regression tests shows context menu for element: [end of test] number of renders 1`] = `7`; exports[`regression tests shows context menu for element: [end of test] number of renders 1`] = `8`;
exports[`regression tests single-clicking on a subgroup of a selected group should not alter selection: [end of test] appState 1`] = ` exports[`regression tests single-clicking on a subgroup of a selected group should not alter selection: [end of test] appState 1`] = `
Object { Object {
@ -18706,7 +18706,7 @@ Object {
exports[`regression tests single-clicking on a subgroup of a selected group should not alter selection: [end of test] number of elements 1`] = `4`; exports[`regression tests single-clicking on a subgroup of a selected group should not alter selection: [end of test] number of elements 1`] = `4`;
exports[`regression tests single-clicking on a subgroup of a selected group should not alter selection: [end of test] number of renders 1`] = `37`; exports[`regression tests single-clicking on a subgroup of a selected group should not alter selection: [end of test] number of renders 1`] = `38`;
exports[`regression tests spacebar + drag scrolls the canvas: [end of test] appState 1`] = ` exports[`regression tests spacebar + drag scrolls the canvas: [end of test] appState 1`] = `
Object { Object {
@ -18807,7 +18807,7 @@ Object {
exports[`regression tests spacebar + drag scrolls the canvas: [end of test] number of elements 1`] = `0`; exports[`regression tests spacebar + drag scrolls the canvas: [end of test] number of elements 1`] = `0`;
exports[`regression tests spacebar + drag scrolls the canvas: [end of test] number of renders 1`] = `6`; exports[`regression tests spacebar + drag scrolls the canvas: [end of test] number of renders 1`] = `7`;
exports[`regression tests supports nested groups: [end of test] appState 1`] = ` exports[`regression tests supports nested groups: [end of test] appState 1`] = `
Object { Object {
@ -19540,7 +19540,7 @@ Object {
exports[`regression tests supports nested groups: [end of test] number of elements 1`] = `3`; exports[`regression tests supports nested groups: [end of test] number of elements 1`] = `3`;
exports[`regression tests supports nested groups: [end of test] number of renders 1`] = `30`; exports[`regression tests supports nested groups: [end of test] number of renders 1`] = `31`;
exports[`regression tests switches from group of selected elements to another element on pointer down: [end of test] appState 1`] = ` exports[`regression tests switches from group of selected elements to another element on pointer down: [end of test] appState 1`] = `
Object { Object {
@ -19946,7 +19946,7 @@ Object {
exports[`regression tests switches from group of selected elements to another element on pointer down: [end of test] number of elements 1`] = `3`; exports[`regression tests switches from group of selected elements to another element on pointer down: [end of test] number of elements 1`] = `3`;
exports[`regression tests switches from group of selected elements to another element on pointer down: [end of test] number of renders 1`] = `18`; exports[`regression tests switches from group of selected elements to another element on pointer down: [end of test] number of renders 1`] = `19`;
exports[`regression tests switches selected element on pointer down: [end of test] appState 1`] = ` exports[`regression tests switches selected element on pointer down: [end of test] appState 1`] = `
Object { Object {
@ -20242,7 +20242,7 @@ Object {
exports[`regression tests switches selected element on pointer down: [end of test] number of elements 1`] = `2`; exports[`regression tests switches selected element on pointer down: [end of test] number of elements 1`] = `2`;
exports[`regression tests switches selected element on pointer down: [end of test] number of renders 1`] = `12`; exports[`regression tests switches selected element on pointer down: [end of test] number of renders 1`] = `13`;
exports[`regression tests two-finger scroll works: [end of test] appState 1`] = ` exports[`regression tests two-finger scroll works: [end of test] appState 1`] = `
Object { Object {
@ -20345,7 +20345,7 @@ Object {
exports[`regression tests two-finger scroll works: [end of test] number of elements 1`] = `0`; exports[`regression tests two-finger scroll works: [end of test] number of elements 1`] = `0`;
exports[`regression tests two-finger scroll works: [end of test] number of renders 1`] = `11`; exports[`regression tests two-finger scroll works: [end of test] number of renders 1`] = `12`;
exports[`regression tests undo/redo drawing an element: [end of test] appState 1`] = ` exports[`regression tests undo/redo drawing an element: [end of test] appState 1`] = `
Object { Object {
@ -20844,7 +20844,7 @@ Object {
exports[`regression tests undo/redo drawing an element: [end of test] number of elements 1`] = `3`; exports[`regression tests undo/redo drawing an element: [end of test] number of elements 1`] = `3`;
exports[`regression tests undo/redo drawing an element: [end of test] number of renders 1`] = `28`; exports[`regression tests undo/redo drawing an element: [end of test] number of renders 1`] = `29`;
exports[`regression tests updates fontSize & fontFamily appState: [end of test] appState 1`] = ` exports[`regression tests updates fontSize & fontFamily appState: [end of test] appState 1`] = `
Object { Object {
@ -20945,7 +20945,7 @@ Object {
exports[`regression tests updates fontSize & fontFamily appState: [end of test] number of elements 1`] = `0`; exports[`regression tests updates fontSize & fontFamily appState: [end of test] number of elements 1`] = `0`;
exports[`regression tests updates fontSize & fontFamily appState: [end of test] number of renders 1`] = `5`; exports[`regression tests updates fontSize & fontFamily appState: [end of test] number of renders 1`] = `6`;
exports[`regression tests zoom hotkeys: [end of test] appState 1`] = ` exports[`regression tests zoom hotkeys: [end of test] appState 1`] = `
Object { Object {
@ -21046,4 +21046,4 @@ Object {
exports[`regression tests zoom hotkeys: [end of test] number of elements 1`] = `0`; exports[`regression tests zoom hotkeys: [end of test] number of elements 1`] = `0`;
exports[`regression tests zoom hotkeys: [end of test] number of renders 1`] = `5`; exports[`regression tests zoom hotkeys: [end of test] number of renders 1`] = `6`;

View File

@ -36,8 +36,8 @@ Object {
"version": 3, "version": 3,
"versionNonce": 449462985, "versionNonce": 449462985,
"width": 30, "width": 30,
"x": 30, "x": 10,
"y": 20, "y": 10,
} }
`; `;
@ -77,8 +77,8 @@ Object {
"version": 3, "version": 3,
"versionNonce": 449462985, "versionNonce": 449462985,
"width": 30, "width": 30,
"x": 30, "x": 10,
"y": 20, "y": 10,
} }
`; `;
@ -103,8 +103,8 @@ Object {
"version": 2, "version": 2,
"versionNonce": 1278240551, "versionNonce": 1278240551,
"width": 30, "width": 30,
"x": 30, "x": 10,
"y": 20, "y": 10,
} }
`; `;
@ -129,8 +129,8 @@ Object {
"version": 2, "version": 2,
"versionNonce": 1278240551, "versionNonce": 1278240551,
"width": 30, "width": 30,
"x": 30, "x": 10,
"y": 20, "y": 10,
} }
`; `;
@ -155,7 +155,7 @@ Object {
"version": 2, "version": 2,
"versionNonce": 1278240551, "versionNonce": 1278240551,
"width": 30, "width": 30,
"x": 30, "x": 10,
"y": 20, "y": 10,
} }
`; `;

View File

@ -60,7 +60,7 @@ describe("collaboration", () => {
]); ]);
expect(API.getStateHistory().length).toBe(1); expect(API.getStateHistory().length).toBe(1);
}); });
h.collab.openPortal(); window.collab.openPortal();
await waitFor(() => { await waitFor(() => {
expect(h.elements).toEqual([expect.objectContaining({ id: "A" })]); expect(h.elements).toEqual([expect.objectContaining({ id: "A" })]);
expect(API.getStateHistory().length).toBe(1); expect(API.getStateHistory().length).toBe(1);

View File

@ -3,7 +3,12 @@ import ReactDOM from "react-dom";
import ExcalidrawApp from "../excalidraw-app"; import ExcalidrawApp from "../excalidraw-app";
import * as Renderer from "../renderer/renderScene"; import * as Renderer from "../renderer/renderScene";
import { KEYS } from "../keys"; import { KEYS } from "../keys";
import { render, fireEvent } from "./test-utils"; import {
render,
fireEvent,
mockBoundingClientRect,
restoreOriginalGetBoundingClientRect,
} from "./test-utils";
import { ExcalidrawLinearElement } from "../element/types"; import { ExcalidrawLinearElement } from "../element/types";
import { reseed } from "../random"; import { reseed } from "../random";
@ -37,7 +42,7 @@ describe("add element to the scene when pointer dragging long enough", () => {
// finish (position does not matter) // finish (position does not matter)
fireEvent.pointerUp(canvas); fireEvent.pointerUp(canvas);
expect(renderScene).toHaveBeenCalledTimes(7); expect(renderScene).toHaveBeenCalledTimes(8);
expect(h.state.selectionElement).toBeNull(); expect(h.state.selectionElement).toBeNull();
expect(h.elements.length).toEqual(1); expect(h.elements.length).toEqual(1);
@ -68,7 +73,7 @@ describe("add element to the scene when pointer dragging long enough", () => {
// finish (position does not matter) // finish (position does not matter)
fireEvent.pointerUp(canvas); fireEvent.pointerUp(canvas);
expect(renderScene).toHaveBeenCalledTimes(7); expect(renderScene).toHaveBeenCalledTimes(8);
expect(h.state.selectionElement).toBeNull(); expect(h.state.selectionElement).toBeNull();
expect(h.elements.length).toEqual(1); expect(h.elements.length).toEqual(1);
@ -99,7 +104,7 @@ describe("add element to the scene when pointer dragging long enough", () => {
// finish (position does not matter) // finish (position does not matter)
fireEvent.pointerUp(canvas); fireEvent.pointerUp(canvas);
expect(renderScene).toHaveBeenCalledTimes(7); expect(renderScene).toHaveBeenCalledTimes(8);
expect(h.state.selectionElement).toBeNull(); expect(h.state.selectionElement).toBeNull();
expect(h.elements.length).toEqual(1); expect(h.elements.length).toEqual(1);
@ -130,7 +135,7 @@ describe("add element to the scene when pointer dragging long enough", () => {
// finish (position does not matter) // finish (position does not matter)
fireEvent.pointerUp(canvas); fireEvent.pointerUp(canvas);
expect(renderScene).toHaveBeenCalledTimes(7); expect(renderScene).toHaveBeenCalledTimes(8);
expect(h.state.selectionElement).toBeNull(); expect(h.state.selectionElement).toBeNull();
expect(h.elements.length).toEqual(1); expect(h.elements.length).toEqual(1);
@ -165,7 +170,7 @@ describe("add element to the scene when pointer dragging long enough", () => {
// finish (position does not matter) // finish (position does not matter)
fireEvent.pointerUp(canvas); fireEvent.pointerUp(canvas);
expect(renderScene).toHaveBeenCalledTimes(7); expect(renderScene).toHaveBeenCalledTimes(8);
expect(h.state.selectionElement).toBeNull(); expect(h.state.selectionElement).toBeNull();
expect(h.elements.length).toEqual(1); expect(h.elements.length).toEqual(1);
@ -184,6 +189,13 @@ describe("add element to the scene when pointer dragging long enough", () => {
}); });
describe("do not add element to the scene if size is too small", () => { describe("do not add element to the scene if size is too small", () => {
beforeAll(() => {
mockBoundingClientRect();
});
afterAll(() => {
restoreOriginalGetBoundingClientRect();
});
it("rectangle", async () => { it("rectangle", async () => {
const { getByToolName, container } = await render(<ExcalidrawApp />); const { getByToolName, container } = await render(<ExcalidrawApp />);
// select tool // select tool

View File

@ -130,4 +130,86 @@ describe("<Excalidraw/>", () => {
expect(textInput?.nodeName).toBe("SPAN"); expect(textInput?.nodeName).toBe("SPAN");
}); });
}); });
describe("Test UIOptions prop", () => {
it('should not hide any UI element when the UIOptions prop is "undefined"', async () => {
await render(<Excalidraw />);
const canvasActions = document.querySelector(
'section[aria-labelledby="canvasActions-title"]',
);
expect(canvasActions).toMatchSnapshot();
});
describe("Test canvasActions", () => {
it('should not hide any UI element when canvasActions is "undefined"', async () => {
await render(<Excalidraw UIOptions={{}} />);
const canvasActions = document.querySelector(
'section[aria-labelledby="canvasActions-title"]',
);
expect(canvasActions).toMatchSnapshot();
});
it("should hide clear canvas button when clearCanvas is false", async () => {
const { container } = await render(
<Excalidraw UIOptions={{ canvasActions: { clearCanvas: false } }} />,
);
expect(queryByTestId(container, "clear-canvas-button")).toBeNull();
});
it("should hide export button when export is false", async () => {
const { container } = await render(
<Excalidraw UIOptions={{ canvasActions: { export: false } }} />,
);
expect(queryByTestId(container, "export-button")).toBeNull();
});
it("should hide load button when loadScene is false", async () => {
const { container } = await render(
<Excalidraw UIOptions={{ canvasActions: { loadScene: false } }} />,
);
expect(queryByTestId(container, "load-button")).toBeNull();
});
it("should hide save as button when saveAsScene is false", async () => {
const { container } = await render(
<Excalidraw UIOptions={{ canvasActions: { saveAsScene: false } }} />,
);
expect(queryByTestId(container, "save-as-button")).toBeNull();
});
it("should hide save button when saveScene is false", async () => {
const { container } = await render(
<Excalidraw UIOptions={{ canvasActions: { saveScene: false } }} />,
);
expect(queryByTestId(container, "save-button")).toBeNull();
});
it("should hide the canvas background picker when changeViewBackgroundColor is false", async () => {
const { container } = await render(
<Excalidraw
UIOptions={{ canvasActions: { changeViewBackgroundColor: false } }}
/>,
);
expect(queryByTestId(container, "canvas-background-picker")).toBeNull();
});
it("should hide the theme toggle when theme is false", async () => {
const { container } = await render(
<Excalidraw UIOptions={{ canvasActions: { theme: false } }} />,
);
expect(queryByTestId(container, "toggle-dark-mode")).toBeNull();
});
});
});
}); });

31
src/tests/fixtures/diagramFixture.ts vendored Normal file
View File

@ -0,0 +1,31 @@
import {
diamondFixture,
ellipseFixture,
rectangleFixture,
} from "./elementFixture";
export const diagramFixture = {
type: "excalidraw",
version: 2,
source: "https://excalidraw.com",
elements: [diamondFixture, ellipseFixture, rectangleFixture],
appState: {
viewBackgroundColor: "#ffffff",
gridSize: null,
},
};
export const diagramFactory = ({
overrides = {},
elementOverrides = {},
} = {}) => ({
...diagramFixture,
elements: [
{ ...diamondFixture, ...elementOverrides },
{ ...ellipseFixture, ...elementOverrides },
{ ...rectangleFixture, ...elementOverrides },
],
...overrides,
});
export default diagramFixture;

37
src/tests/fixtures/elementFixture.ts vendored Normal file
View File

@ -0,0 +1,37 @@
import { ExcalidrawElement } from "../../element/types";
const elementBase: Omit<ExcalidrawElement, "type"> = {
id: "vWrqOAfkind2qcm7LDAGZ",
x: 414,
y: 237,
width: 214,
height: 214,
angle: 0,
strokeColor: "#000000",
backgroundColor: "#15aabf",
fillStyle: "hachure",
strokeWidth: 1,
strokeStyle: "solid",
roughness: 1,
opacity: 100,
groupIds: [],
strokeSharpness: "sharp",
seed: 1041657908,
version: 120,
versionNonce: 1188004276,
isDeleted: false,
boundElementIds: null,
};
export const rectangleFixture: ExcalidrawElement = {
...elementBase,
type: "rectangle",
};
export const ellipseFixture: ExcalidrawElement = {
...elementBase,
type: "ellipse",
};
export const diamondFixture: ExcalidrawElement = {
...elementBase,
type: "diamond",
};

Some files were not shown because too many files have changed in this diff Show More