mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-11-11 08:14:17 +01:00
Compare commits
14 Commits
kb/auto-sa
...
updatescen
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eb206cc932 | ||
|
|
16c287c848 | ||
|
|
78024873e5 | ||
|
|
4e41bd9dbb | ||
|
|
edc23b854f | ||
|
|
4843c49556 | ||
|
|
d565413082 | ||
|
|
dcda7184d0 | ||
|
|
8d413670c8 | ||
|
|
f774452124 | ||
|
|
db4ed1ecb1 | ||
|
|
489f45b910 | ||
|
|
a17be085b0 | ||
|
|
4e07a608d3 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -20,4 +20,3 @@ package-lock.json
|
||||
static
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
src/packages/excalidraw/types
|
||||
|
||||
16
package.json
16
package.json
@@ -21,18 +21,18 @@
|
||||
"dependencies": {
|
||||
"@sentry/browser": "6.2.2",
|
||||
"@sentry/integrations": "6.2.1",
|
||||
"@testing-library/jest-dom": "5.11.10",
|
||||
"@testing-library/jest-dom": "5.11.9",
|
||||
"@testing-library/react": "11.2.5",
|
||||
"@types/jest": "26.0.22",
|
||||
"@types/react": "17.0.3",
|
||||
"@types/react-dom": "17.0.2",
|
||||
"@types/jest": "26.0.20",
|
||||
"@types/react": "17.0.2",
|
||||
"@types/react-dom": "17.0.1",
|
||||
"@types/socket.io-client": "1.4.36",
|
||||
"browser-fs-access": "0.16.2",
|
||||
"browser-fs-access": "0.14.2",
|
||||
"clsx": "1.1.1",
|
||||
"firebase": "8.2.10",
|
||||
"i18next-browser-languagedetector": "6.0.1",
|
||||
"lodash.throttle": "4.1.1",
|
||||
"nanoid": "3.1.22",
|
||||
"nanoid": "3.1.21",
|
||||
"open-color": "1.8.0",
|
||||
"pako": "1.0.11",
|
||||
"png-chunk-text": "1.0.0",
|
||||
@@ -40,8 +40,8 @@
|
||||
"png-chunks-extract": "1.0.0",
|
||||
"points-on-curve": "0.2.0",
|
||||
"pwacompat": "2.0.17",
|
||||
"react": "17.0.2",
|
||||
"react-dom": "17.0.2",
|
||||
"react": "17.0.1",
|
||||
"react-dom": "17.0.1",
|
||||
"react-scripts": "4.0.3",
|
||||
"roughjs": "4.3.1",
|
||||
"sass": "1.32.8",
|
||||
|
||||
@@ -51,7 +51,8 @@
|
||||
name="twitter:description"
|
||||
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" />
|
||||
|
||||
<!-- Excalidraw version -->
|
||||
@@ -87,8 +88,6 @@
|
||||
<link rel="stylesheet" href="fonts.css" type="text/css" />
|
||||
<script>
|
||||
window.EXCALIDRAW_ASSET_PATH = "/";
|
||||
// setting this so that libraries installation reuses this window tab.
|
||||
window.name = "_excalidraw";
|
||||
</script>
|
||||
<% if (process.env.REACT_APP_GOOGLE_ANALYTICS_ID) { %>
|
||||
<script
|
||||
@@ -147,9 +146,6 @@
|
||||
color: var(--popup-text-color);
|
||||
font-size: 1.3em;
|
||||
}
|
||||
#root {
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
|
||||
@@ -39,37 +39,5 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"screenshots": [
|
||||
{
|
||||
"src": "/screenshots/virtual-whiteboard.png",
|
||||
"type": "image/png",
|
||||
"sizes": "462x945"
|
||||
},
|
||||
{
|
||||
"src": "/screenshots/wireframe.png",
|
||||
"type": "image/png",
|
||||
"sizes": "462x945"
|
||||
},
|
||||
{
|
||||
"src": "/screenshots/illustration.png",
|
||||
"type": "image/png",
|
||||
"sizes": "462x945"
|
||||
},
|
||||
{
|
||||
"src": "/screenshots/shapes.png",
|
||||
"type": "image/png",
|
||||
"sizes": "462x945"
|
||||
},
|
||||
{
|
||||
"src": "/screenshots/collaboration.png",
|
||||
"type": "image/png",
|
||||
"sizes": "462x945"
|
||||
},
|
||||
{
|
||||
"src": "/screenshots/export.png",
|
||||
"type": "image/png",
|
||||
"sizes": "462x945"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 28 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 25 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 47 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 25 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 27 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 27 KiB |
@@ -8,7 +8,7 @@ import { getCommonBounds, getNonDeletedElements } from "../element";
|
||||
import { newElementWith } from "../element/mutateElement";
|
||||
import { ExcalidrawElement } from "../element/types";
|
||||
import { t } from "../i18n";
|
||||
import { useIsMobile } from "../is-mobile";
|
||||
import useIsMobile from "../is-mobile";
|
||||
import { CODES, KEYS } from "../keys";
|
||||
import { getNormalizedZoom, getSelectedElements } from "../scene";
|
||||
import { centerScrollOn } from "../scene/scroll";
|
||||
@@ -33,7 +33,6 @@ export const actionChangeViewBackgroundColor = register({
|
||||
type="canvasBackground"
|
||||
color={appState.viewBackgroundColor}
|
||||
onChange={(color) => updateData(color)}
|
||||
data-testid="canvas-background-picker"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
@@ -73,7 +72,6 @@ export const actionClearCanvas = register({
|
||||
updateData(null);
|
||||
}
|
||||
}}
|
||||
data-testid="clear-canvas-button"
|
||||
/>
|
||||
),
|
||||
});
|
||||
|
||||
@@ -8,10 +8,9 @@ import { Tooltip } from "../components/Tooltip";
|
||||
import { DarkModeToggle, Appearence } from "../components/DarkModeToggle";
|
||||
import { loadFromJSON, saveAsJSON } from "../data";
|
||||
import { t } from "../i18n";
|
||||
import { useIsMobile } from "../is-mobile";
|
||||
import useIsMobile from "../is-mobile";
|
||||
import { KEYS } from "../keys";
|
||||
import { register } from "./register";
|
||||
import { supported } from "browser-fs-access";
|
||||
|
||||
export const actionChangeProjectName = register({
|
||||
name: "changeProjectName",
|
||||
@@ -24,9 +23,7 @@ export const actionChangeProjectName = register({
|
||||
label={t("labels.fileTitle")}
|
||||
value={appState.name || "Unnamed"}
|
||||
onChange={(name: string) => updateData(name)}
|
||||
isNameEditable={
|
||||
typeof appProps.name === "undefined" && !appState.viewModeEnabled
|
||||
}
|
||||
isNameEditable={typeof appProps.name === "undefined"}
|
||||
/>
|
||||
),
|
||||
});
|
||||
@@ -136,7 +133,6 @@ export const actionSaveScene = register({
|
||||
aria-label={t("buttons.save")}
|
||||
showAriaLabel={useIsMobile()}
|
||||
onClick={() => updateData(null)}
|
||||
data-testid="save-button"
|
||||
/>
|
||||
),
|
||||
});
|
||||
@@ -166,9 +162,10 @@ export const actionSaveAsScene = register({
|
||||
title={t("buttons.saveAs")}
|
||||
aria-label={t("buttons.saveAs")}
|
||||
showAriaLabel={useIsMobile()}
|
||||
hidden={!supported}
|
||||
hidden={
|
||||
!("chooseFileSystemEntries" in window || "showOpenFilePicker" in window)
|
||||
}
|
||||
onClick={() => updateData(null)}
|
||||
data-testid="save-as-button"
|
||||
/>
|
||||
),
|
||||
});
|
||||
@@ -206,7 +203,6 @@ export const actionLoadScene = register({
|
||||
aria-label={t("buttons.load")}
|
||||
showAriaLabel={useIsMobile()}
|
||||
onClick={updateData}
|
||||
data-testid="load-button"
|
||||
/>
|
||||
),
|
||||
});
|
||||
@@ -238,37 +234,3 @@ export const actionExportWithDarkMode = register({
|
||||
</div>
|
||||
),
|
||||
});
|
||||
|
||||
export const actionToggleAutosave = register({
|
||||
name: "toggleAutosave",
|
||||
perform(elements, appState) {
|
||||
trackEvent("toggle", "autosave");
|
||||
return {
|
||||
appState: {
|
||||
...appState,
|
||||
autosave: !appState.autosave,
|
||||
},
|
||||
commitToHistory: false,
|
||||
};
|
||||
},
|
||||
PanelComponent: ({ appState, updateData }) =>
|
||||
supported && appState.fileHandle ? (
|
||||
<label style={{ display: "flex" }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={appState.autosave}
|
||||
onChange={(event) => updateData(event.target.checked)}
|
||||
/>{" "}
|
||||
{t("labels.toggleAutosave")}
|
||||
<Tooltip
|
||||
label={t("labels.toggleAutosave_details")}
|
||||
position="above"
|
||||
long={true}
|
||||
>
|
||||
<div className="TooltipIcon">{questionCircle}</div>
|
||||
</Tooltip>
|
||||
</label>
|
||||
) : (
|
||||
<></>
|
||||
),
|
||||
});
|
||||
|
||||
@@ -1,207 +0,0 @@
|
||||
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,
|
||||
});
|
||||
};
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from "react";
|
||||
import { AppState } from "../../src/types";
|
||||
import { ButtonIconSelect } from "../components/ButtonIconSelect";
|
||||
import { ButtonSelect } from "../components/ButtonSelect";
|
||||
import { ColorPicker } from "../components/ColorPicker";
|
||||
import { IconPicker } from "../components/IconPicker";
|
||||
import {
|
||||
@@ -20,16 +21,6 @@ import {
|
||||
StrokeStyleDottedIcon,
|
||||
StrokeStyleSolidIcon,
|
||||
StrokeWidthIcon,
|
||||
FontSizeSmallIcon,
|
||||
FontSizeMediumIcon,
|
||||
FontSizeLargeIcon,
|
||||
FontSizeExtraLargeIcon,
|
||||
FontFamilyHandDrawnIcon,
|
||||
FontFamilyNormalIcon,
|
||||
FontFamilyCodeIcon,
|
||||
TextAlignLeftIcon,
|
||||
TextAlignCenterIcon,
|
||||
TextAlignRightIcon,
|
||||
} from "../components/icons";
|
||||
import { DEFAULT_FONT_FAMILY, DEFAULT_FONT_SIZE } from "../constants";
|
||||
import {
|
||||
@@ -422,29 +413,13 @@ export const actionChangeFontSize = register({
|
||||
PanelComponent: ({ elements, appState, updateData }) => (
|
||||
<fieldset>
|
||||
<legend>{t("labels.fontSize")}</legend>
|
||||
<ButtonIconSelect
|
||||
<ButtonSelect
|
||||
group="font-size"
|
||||
options={[
|
||||
{
|
||||
value: 16,
|
||||
text: t("labels.small"),
|
||||
icon: <FontSizeSmallIcon theme={appState.theme} />,
|
||||
},
|
||||
{
|
||||
value: 20,
|
||||
text: t("labels.medium"),
|
||||
icon: <FontSizeMediumIcon theme={appState.theme} />,
|
||||
},
|
||||
{
|
||||
value: 28,
|
||||
text: t("labels.large"),
|
||||
icon: <FontSizeLargeIcon theme={appState.theme} />,
|
||||
},
|
||||
{
|
||||
value: 36,
|
||||
text: t("labels.veryLarge"),
|
||||
icon: <FontSizeExtraLargeIcon theme={appState.theme} />,
|
||||
},
|
||||
{ value: 16, text: t("labels.small") },
|
||||
{ value: 20, text: t("labels.medium") },
|
||||
{ value: 28, text: t("labels.large") },
|
||||
{ value: 36, text: t("labels.veryLarge") },
|
||||
]}
|
||||
value={getFormValue(
|
||||
elements,
|
||||
@@ -481,28 +456,16 @@ export const actionChangeFontFamily = register({
|
||||
};
|
||||
},
|
||||
PanelComponent: ({ elements, appState, updateData }) => {
|
||||
const options: { value: FontFamily; text: string; icon: JSX.Element }[] = [
|
||||
{
|
||||
value: 1,
|
||||
text: t("labels.handDrawn"),
|
||||
icon: <FontFamilyHandDrawnIcon theme={appState.theme} />,
|
||||
},
|
||||
{
|
||||
value: 2,
|
||||
text: t("labels.normal"),
|
||||
icon: <FontFamilyNormalIcon theme={appState.theme} />,
|
||||
},
|
||||
{
|
||||
value: 3,
|
||||
text: t("labels.code"),
|
||||
icon: <FontFamilyCodeIcon theme={appState.theme} />,
|
||||
},
|
||||
const options: { value: FontFamily; text: string }[] = [
|
||||
{ value: 1, text: t("labels.handDrawn") },
|
||||
{ value: 2, text: t("labels.normal") },
|
||||
{ value: 3, text: t("labels.code") },
|
||||
];
|
||||
|
||||
return (
|
||||
<fieldset>
|
||||
<legend>{t("labels.fontFamily")}</legend>
|
||||
<ButtonIconSelect<FontFamily | false>
|
||||
<ButtonSelect<FontFamily | false>
|
||||
group="font-family"
|
||||
options={options}
|
||||
value={getFormValue(
|
||||
@@ -543,24 +506,12 @@ export const actionChangeTextAlign = register({
|
||||
PanelComponent: ({ elements, appState, updateData }) => (
|
||||
<fieldset>
|
||||
<legend>{t("labels.textAlign")}</legend>
|
||||
<ButtonIconSelect<TextAlign | false>
|
||||
<ButtonSelect<TextAlign | false>
|
||||
group="text-align"
|
||||
options={[
|
||||
{
|
||||
value: "left",
|
||||
text: t("labels.left"),
|
||||
icon: <TextAlignLeftIcon theme={appState.theme} />,
|
||||
},
|
||||
{
|
||||
value: "center",
|
||||
text: t("labels.center"),
|
||||
icon: <TextAlignCenterIcon theme={appState.theme} />,
|
||||
},
|
||||
{
|
||||
value: "right",
|
||||
text: t("labels.right"),
|
||||
icon: <TextAlignRightIcon theme={appState.theme} />,
|
||||
},
|
||||
{ value: "left", text: t("labels.left") },
|
||||
{ value: "center", text: t("labels.center") },
|
||||
{ value: "right", text: t("labels.right") },
|
||||
]}
|
||||
value={getFormValue(
|
||||
elements,
|
||||
|
||||
@@ -33,7 +33,6 @@ export { actionFinalize } from "./actionFinalize";
|
||||
export {
|
||||
actionChangeProjectName,
|
||||
actionChangeExportBackground,
|
||||
actionToggleAutosave,
|
||||
actionSaveScene,
|
||||
actionSaveAsScene,
|
||||
actionLoadScene,
|
||||
@@ -67,8 +66,6 @@ export {
|
||||
distributeVertically,
|
||||
} from "./actionDistribute";
|
||||
|
||||
export { actionFlipHorizontal, actionFlipVertical } from "./actionFlip";
|
||||
|
||||
export {
|
||||
actionCopy,
|
||||
actionCut,
|
||||
|
||||
@@ -7,12 +7,12 @@ import {
|
||||
ActionResult,
|
||||
} from "./types";
|
||||
import { ExcalidrawElement } from "../element/types";
|
||||
import { AppProps, AppState } from "../types";
|
||||
import { AppState, ExcalidrawProps } from "../types";
|
||||
import { MODES } from "../constants";
|
||||
|
||||
// This is the <App> component, but for now we don't care about anything but its
|
||||
// `canvas` state.
|
||||
type App = { canvas: HTMLCanvasElement | null; props: AppProps };
|
||||
type App = { canvas: HTMLCanvasElement | null; props: ExcalidrawProps };
|
||||
|
||||
export class ActionManager implements ActionsManagerInterface {
|
||||
actions = {} as ActionsManagerInterface["actions"];
|
||||
@@ -52,14 +52,10 @@ export class ActionManager implements ActionsManagerInterface {
|
||||
}
|
||||
|
||||
handleKeyDown(event: KeyboardEvent) {
|
||||
const canvasActions = this.app.props.UIOptions.canvasActions;
|
||||
const data = Object.values(this.actions)
|
||||
.sort((a, b) => (b.keyPriority || 0) - (a.keyPriority || 0))
|
||||
.filter(
|
||||
(action) =>
|
||||
(action.name in canvasActions
|
||||
? canvasActions[action.name as keyof typeof canvasActions]
|
||||
: true) &&
|
||||
action.keyTest &&
|
||||
action.keyTest(
|
||||
event,
|
||||
@@ -106,15 +102,7 @@ export class ActionManager implements ActionsManagerInterface {
|
||||
// like the user list. We can use this key to extract more
|
||||
// data from app state. This is an alternative to generic prop hell!
|
||||
renderAction = (name: ActionName, id?: string) => {
|
||||
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)
|
||||
) {
|
||||
if (this.actions[name] && "PanelComponent" in this.actions[name]) {
|
||||
const action = this.actions[name];
|
||||
const PanelComponent = action.PanelComponent!;
|
||||
const updateData = (formState?: any) => {
|
||||
|
||||
@@ -23,9 +23,7 @@ export type ShortcutName =
|
||||
| "zenMode"
|
||||
| "stats"
|
||||
| "addToLibrary"
|
||||
| "viewMode"
|
||||
| "flipHorizontal"
|
||||
| "flipVertical";
|
||||
| "viewMode";
|
||||
|
||||
const shortcutMap: Record<ShortcutName, string[]> = {
|
||||
cut: [getShortcutKey("CtrlOrCmd+X")],
|
||||
@@ -59,8 +57,6 @@ const shortcutMap: Record<ShortcutName, string[]> = {
|
||||
zenMode: [getShortcutKey("Alt+Z")],
|
||||
stats: [],
|
||||
addToLibrary: [],
|
||||
flipHorizontal: [getShortcutKey("Shift+H")],
|
||||
flipVertical: [getShortcutKey("Shift+V")],
|
||||
viewMode: [getShortcutKey("Alt+R")],
|
||||
};
|
||||
|
||||
|
||||
@@ -6,10 +6,7 @@ import { AppState, ExcalidrawProps } from "../types";
|
||||
export type ActionResult =
|
||||
| {
|
||||
elements?: readonly ExcalidrawElement[] | null;
|
||||
appState?: MarkOptional<
|
||||
AppState,
|
||||
"offsetTop" | "offsetLeft" | "width" | "height"
|
||||
> | null;
|
||||
appState?: MarkOptional<AppState, "offsetTop" | "offsetLeft"> | null;
|
||||
commitToHistory: boolean;
|
||||
syncHistory?: boolean;
|
||||
}
|
||||
@@ -51,7 +48,6 @@ export type ActionName =
|
||||
| "changeOpacity"
|
||||
| "changeFontSize"
|
||||
| "toggleCanvasMenu"
|
||||
| "toggleAutosave"
|
||||
| "toggleEditMenu"
|
||||
| "undo"
|
||||
| "redo"
|
||||
@@ -89,8 +85,6 @@ export type ActionName =
|
||||
| "alignHorizontallyCentered"
|
||||
| "distributeHorizontally"
|
||||
| "distributeVertically"
|
||||
| "flipHorizontal"
|
||||
| "flipVertical"
|
||||
| "viewMode"
|
||||
| "exportWithDarkMode";
|
||||
|
||||
|
||||
@@ -10,10 +10,9 @@ import { getDateTime } from "./utils";
|
||||
|
||||
export const getDefaultAppState = (): Omit<
|
||||
AppState,
|
||||
"offsetTop" | "offsetLeft" | "width" | "height"
|
||||
"offsetTop" | "offsetLeft"
|
||||
> => {
|
||||
return {
|
||||
autosave: false,
|
||||
theme: "light",
|
||||
collaborators: new Map(),
|
||||
currentChartType: "bar",
|
||||
@@ -44,6 +43,7 @@ export const getDefaultAppState = (): Omit<
|
||||
exportWithDarkMode: false,
|
||||
fileHandle: null,
|
||||
gridSize: null,
|
||||
height: window.innerHeight,
|
||||
isBindingEnabled: true,
|
||||
isLibraryOpen: false,
|
||||
isLoading: false,
|
||||
@@ -70,6 +70,7 @@ export const getDefaultAppState = (): Omit<
|
||||
suggestedBindings: [],
|
||||
toastMessage: null,
|
||||
viewBackgroundColor: oc.white,
|
||||
width: window.innerWidth,
|
||||
zenModeEnabled: false,
|
||||
zoom: { value: 1 as NormalizedZoomValue, translation: { x: 0, y: 0 } },
|
||||
viewModeEnabled: false,
|
||||
@@ -91,7 +92,6 @@ const APP_STATE_STORAGE_CONF = (<
|
||||
>(
|
||||
config: { [K in keyof T]: K extends keyof AppState ? T[K] : never },
|
||||
) => config)({
|
||||
autosave: { browser: true, export: false },
|
||||
theme: { browser: true, export: false },
|
||||
collaborators: { browser: false, export: false },
|
||||
currentChartType: { browser: true, export: false },
|
||||
|
||||
@@ -7,10 +7,12 @@ import { AppState } from "./types";
|
||||
import { SVG_EXPORT_TAG } from "./scene/export";
|
||||
import { tryParseSpreadsheet, Spreadsheet, VALID_SPREADSHEET } from "./charts";
|
||||
import { canvasToBlob } from "./data/blob";
|
||||
import { EXPORT_DATA_TYPES } from "./constants";
|
||||
|
||||
const TYPE_ELEMENTS = "excalidraw/elements";
|
||||
|
||||
type ElementsClipboard = {
|
||||
type: typeof EXPORT_DATA_TYPES.excalidrawClipboard;
|
||||
type: typeof TYPE_ELEMENTS;
|
||||
created: number;
|
||||
elements: ExcalidrawElement[];
|
||||
};
|
||||
|
||||
@@ -29,16 +31,8 @@ export const probablySupportsClipboardBlob =
|
||||
"ClipboardItem" in window &&
|
||||
"toBlob" in HTMLCanvasElement.prototype;
|
||||
|
||||
const clipboardContainsElements = (
|
||||
contents: any,
|
||||
): contents is { elements: ExcalidrawElement[] } => {
|
||||
if (
|
||||
[
|
||||
EXPORT_DATA_TYPES.excalidraw,
|
||||
EXPORT_DATA_TYPES.excalidrawClipboard,
|
||||
].includes(contents?.type) &&
|
||||
Array.isArray(contents.elements)
|
||||
) {
|
||||
const isElementsClipboard = (contents: any): contents is ElementsClipboard => {
|
||||
if (contents?.type === TYPE_ELEMENTS) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -49,7 +43,8 @@ export const copyToClipboard = async (
|
||||
appState: AppState,
|
||||
) => {
|
||||
const contents: ElementsClipboard = {
|
||||
type: EXPORT_DATA_TYPES.excalidrawClipboard,
|
||||
type: TYPE_ELEMENTS,
|
||||
created: Date.now(),
|
||||
elements: getSelectedElements(elements, appState),
|
||||
};
|
||||
const json = JSON.stringify(contents);
|
||||
@@ -136,9 +131,15 @@ export const parseClipboard = async (
|
||||
|
||||
try {
|
||||
const systemClipboardData = JSON.parse(systemClipboard);
|
||||
if (clipboardContainsElements(systemClipboardData)) {
|
||||
// system clipboard elements are newer than in-app clipboard
|
||||
if (
|
||||
isElementsClipboard(systemClipboardData) &&
|
||||
(!appClipboardData?.created ||
|
||||
appClipboardData.created < systemClipboardData.created)
|
||||
) {
|
||||
return { elements: systemClipboardData.elements };
|
||||
}
|
||||
// in-app clipboard is newer than system clipboard
|
||||
return appClipboardData;
|
||||
} catch {
|
||||
// system clipboard doesn't contain excalidraw elements → return plaintext
|
||||
|
||||
@@ -3,7 +3,7 @@ import { ActionManager } from "../actions/manager";
|
||||
import { getNonDeletedElements } from "../element";
|
||||
import { ExcalidrawElement } from "../element/types";
|
||||
import { t } from "../i18n";
|
||||
import { useIsMobile } from "../is-mobile";
|
||||
import useIsMobile from "../is-mobile";
|
||||
import {
|
||||
canChangeSharpness,
|
||||
canHaveArrowheads,
|
||||
|
||||
@@ -3,7 +3,6 @@ import React from "react";
|
||||
import { RoughCanvas } from "roughjs/bin/canvas";
|
||||
import rough from "roughjs/bin/rough";
|
||||
import clsx from "clsx";
|
||||
import { supported } from "browser-fs-access";
|
||||
|
||||
import {
|
||||
actionAddToLibrary,
|
||||
@@ -17,8 +16,6 @@ import {
|
||||
actionDeleteSelected,
|
||||
actionDuplicateSelection,
|
||||
actionFinalize,
|
||||
actionFlipHorizontal,
|
||||
actionFlipVertical,
|
||||
actionGroup,
|
||||
actionPasteStyles,
|
||||
actionSelectAll,
|
||||
@@ -44,7 +41,6 @@ import {
|
||||
import {
|
||||
APP_NAME,
|
||||
CURSOR_TYPE,
|
||||
DEFAULT_UI_OPTIONS,
|
||||
DEFAULT_VERTICAL_ALIGN,
|
||||
DRAGGING_THRESHOLD,
|
||||
ELEMENT_SHIFT_TRANSLATE_AMOUNT,
|
||||
@@ -56,16 +52,13 @@ import {
|
||||
MIME_TYPES,
|
||||
POINTER_BUTTON,
|
||||
SCROLL_TIMEOUT,
|
||||
AUTO_SAVE_TIMEOUT,
|
||||
TAP_TWICE_TIMEOUT,
|
||||
TEXT_TO_CENTER_SNAP_THRESHOLD,
|
||||
TOUCH_CTX_MENU_TIMEOUT,
|
||||
URL_HASH_KEYS,
|
||||
URL_QUERY_KEYS,
|
||||
ZOOM_STEP,
|
||||
} from "../constants";
|
||||
import { loadFromBlob } from "../data";
|
||||
import { saveAsJSON, isValidLibrary } from "../data/json";
|
||||
import { isValidLibrary } from "../data/json";
|
||||
import { Library } from "../data/library";
|
||||
import { restore } from "../data/restore";
|
||||
import {
|
||||
@@ -162,7 +155,13 @@ import Scene from "../scene/Scene";
|
||||
import { SceneState, ScrollBars } from "../scene/types";
|
||||
import { getNewZoom } from "../scene/zoom";
|
||||
import { findShapeByKey } from "../shapes";
|
||||
import { AppProps, AppState, Gesture, GestureEvent, SceneData } from "../types";
|
||||
import {
|
||||
AppState,
|
||||
ExcalidrawProps,
|
||||
Gesture,
|
||||
GestureEvent,
|
||||
SceneData,
|
||||
} from "../types";
|
||||
import {
|
||||
debounce,
|
||||
distance,
|
||||
@@ -275,31 +274,30 @@ export type ExcalidrawImperativeAPI = {
|
||||
setScrollToContent: InstanceType<typeof App>["setScrollToContent"];
|
||||
getSceneElements: InstanceType<typeof App>["getSceneElements"];
|
||||
getAppState: () => InstanceType<typeof App>["state"];
|
||||
refresh: InstanceType<typeof App>["refresh"];
|
||||
importLibrary: InstanceType<typeof App>["importLibraryFromUrl"];
|
||||
setToastMessage: InstanceType<typeof App>["setToastMessage"];
|
||||
setCanvasOffsets: InstanceType<typeof App>["setCanvasOffsets"];
|
||||
readyPromise: ResolvablePromise<ExcalidrawImperativeAPI>;
|
||||
ready: true;
|
||||
};
|
||||
|
||||
class App extends React.Component<AppProps, AppState> {
|
||||
class App extends React.Component<ExcalidrawProps, AppState> {
|
||||
canvas: HTMLCanvasElement | null = null;
|
||||
rc: RoughCanvas | null = null;
|
||||
unmounted: boolean = false;
|
||||
actionManager: ActionManager;
|
||||
private excalidrawContainerRef = React.createRef<HTMLDivElement>();
|
||||
|
||||
public static defaultProps: Partial<AppProps> = {
|
||||
// needed for tests to pass since we directly render App in many tests
|
||||
UIOptions: DEFAULT_UI_OPTIONS,
|
||||
public static defaultProps: Partial<ExcalidrawProps> = {
|
||||
width: window.innerWidth,
|
||||
height: window.innerHeight,
|
||||
};
|
||||
|
||||
private scene: Scene;
|
||||
private resizeObserver: ResizeObserver | undefined;
|
||||
constructor(props: AppProps) {
|
||||
constructor(props: ExcalidrawProps) {
|
||||
super(props);
|
||||
const defaultAppState = getDefaultAppState();
|
||||
|
||||
const {
|
||||
width = window.innerWidth,
|
||||
height = window.innerHeight,
|
||||
excalidrawRef,
|
||||
viewModeEnabled = false,
|
||||
zenModeEnabled = false,
|
||||
@@ -311,13 +309,13 @@ class App extends React.Component<AppProps, AppState> {
|
||||
...defaultAppState,
|
||||
theme,
|
||||
isLoading: true,
|
||||
width,
|
||||
height,
|
||||
...this.getCanvasOffsets(),
|
||||
viewModeEnabled,
|
||||
zenModeEnabled,
|
||||
gridSize: gridModeEnabled ? GRID_SIZE : null,
|
||||
name,
|
||||
width: window.innerWidth,
|
||||
height: window.innerHeight,
|
||||
};
|
||||
if (excalidrawRef) {
|
||||
const readyPromise =
|
||||
@@ -336,9 +334,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
setScrollToContent: this.setScrollToContent,
|
||||
getSceneElements: this.getSceneElements,
|
||||
getAppState: () => this.state,
|
||||
refresh: this.refresh,
|
||||
importLibrary: this.importLibraryFromUrl,
|
||||
setToastMessage: this.setToastMessage,
|
||||
setCanvasOffsets: this.setCanvasOffsets,
|
||||
} as const;
|
||||
if (typeof excalidrawRef === "function") {
|
||||
excalidrawRef(api);
|
||||
@@ -410,6 +406,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
onPointerUp={this.removePointer}
|
||||
onPointerCancel={this.removePointer}
|
||||
onTouchMove={this.handleTouchMove}
|
||||
onDrop={this.handleCanvasOnDrop}
|
||||
>
|
||||
{t("labels.drawingCanvas")}
|
||||
</canvas>
|
||||
@@ -424,12 +421,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
viewModeEnabled,
|
||||
} = this.state;
|
||||
|
||||
const {
|
||||
onCollabButtonClick,
|
||||
onExportToBackend,
|
||||
renderFooter,
|
||||
renderCustomStats,
|
||||
} = this.props;
|
||||
const { onCollabButtonClick, onExportToBackend, renderFooter } = this.props;
|
||||
|
||||
const DEFAULT_PASTE_X = canvasDOMWidth / 2;
|
||||
const DEFAULT_PASTE_Y = canvasDOMHeight / 2;
|
||||
@@ -440,7 +432,10 @@ class App extends React.Component<AppProps, AppState> {
|
||||
"excalidraw--view-mode": viewModeEnabled,
|
||||
})}
|
||||
ref={this.excalidrawContainerRef}
|
||||
onDrop={this.handleAppOnDrop}
|
||||
style={{
|
||||
width: canvasDOMWidth,
|
||||
height: canvasDOMHeight,
|
||||
}}
|
||||
>
|
||||
<LayerUI
|
||||
canvas={this.canvas}
|
||||
@@ -467,22 +462,16 @@ class App extends React.Component<AppProps, AppState> {
|
||||
showExitZenModeBtn={
|
||||
typeof this.props?.zenModeEnabled === "undefined" && zenModeEnabled
|
||||
}
|
||||
showThemeBtn={
|
||||
typeof this.props?.theme === "undefined" &&
|
||||
this.props.UIOptions.canvasActions.theme
|
||||
}
|
||||
showThemeBtn={typeof this.props?.theme === "undefined"}
|
||||
libraryReturnUrl={this.props.libraryReturnUrl}
|
||||
UIOptions={this.props.UIOptions}
|
||||
/>
|
||||
<div className="excalidraw-textEditorContainer" />
|
||||
<div className="excalidraw-contextMenuContainer" />
|
||||
{this.state.showStats && (
|
||||
<Stats
|
||||
appState={this.state}
|
||||
setAppState={this.setAppState}
|
||||
elements={this.scene.getElements()}
|
||||
onClose={this.toggleStats}
|
||||
renderCustomStats={renderCustomStats}
|
||||
/>
|
||||
)}
|
||||
{this.state.toastMessage !== null && (
|
||||
@@ -536,7 +525,8 @@ class App extends React.Component<AppProps, AppState> {
|
||||
let zenModeEnabled = actionResult?.appState?.zenModeEnabled || false;
|
||||
let gridSize = actionResult?.appState?.gridSize || null;
|
||||
let theme = actionResult?.appState?.theme || "light";
|
||||
let name = actionResult?.appState?.name ?? this.state.name;
|
||||
let name = actionResult?.appState?.name || this.state.name;
|
||||
|
||||
if (typeof this.props.viewModeEnabled !== "undefined") {
|
||||
viewModeEnabled = this.props.viewModeEnabled;
|
||||
}
|
||||
@@ -556,6 +546,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
if (typeof this.props.name !== "undefined") {
|
||||
name = this.props.name;
|
||||
}
|
||||
|
||||
this.setState(
|
||||
(state) => {
|
||||
// using Object.assign instead of spread to fool TS 4.2.2+ into
|
||||
@@ -564,6 +555,10 @@ class App extends React.Component<AppProps, AppState> {
|
||||
return Object.assign(actionResult.appState || {}, {
|
||||
editingElement:
|
||||
editingElement || actionResult.appState?.editingElement || null,
|
||||
width: state.width,
|
||||
height: state.height,
|
||||
offsetTop: state.offsetTop,
|
||||
offsetLeft: state.offsetLeft,
|
||||
viewModeEnabled,
|
||||
zenModeEnabled,
|
||||
gridSize,
|
||||
@@ -608,17 +603,8 @@ class App extends React.Component<AppProps, AppState> {
|
||||
this.onSceneUpdated();
|
||||
};
|
||||
|
||||
private importLibraryFromUrl = async (url: string, token?: string | null) => {
|
||||
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()}`);
|
||||
}
|
||||
|
||||
private importLibraryFromUrl = async (url: string) => {
|
||||
window.history.replaceState({}, APP_NAME, window.location.origin);
|
||||
try {
|
||||
const request = await fetch(decodeURIComponent(url));
|
||||
const blob = await request.blob();
|
||||
@@ -627,17 +613,14 @@ class App extends React.Component<AppProps, AppState> {
|
||||
throw new Error();
|
||||
}
|
||||
if (
|
||||
token === Library.csrfToken ||
|
||||
window.confirm(
|
||||
t("alerts.confirmAddLibrary", { numShapes: json.library.length }),
|
||||
)
|
||||
) {
|
||||
await Library.importLibrary(blob);
|
||||
// hack to rerender the library items after import
|
||||
if (this.state.isLibraryOpen) {
|
||||
this.setState({ isLibraryOpen: false });
|
||||
}
|
||||
this.setState({ isLibraryOpen: true });
|
||||
this.setState({
|
||||
isLibraryOpen: true,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
window.alert(t("alerts.errorLoadingLibrary"));
|
||||
@@ -696,6 +679,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
if (!this.state.isLoading) {
|
||||
this.setState({ isLoading: true });
|
||||
}
|
||||
|
||||
let initialData = null;
|
||||
try {
|
||||
initialData = (await this.props.initialData) || null;
|
||||
@@ -704,6 +688,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
}
|
||||
|
||||
const scene = restore(initialData, null);
|
||||
|
||||
scene.appState = {
|
||||
...scene.appState,
|
||||
isLoading: false,
|
||||
@@ -731,18 +716,12 @@ class App extends React.Component<AppProps, AppState> {
|
||||
commitToHistory: true,
|
||||
});
|
||||
|
||||
const libraryUrl =
|
||||
// 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,
|
||||
);
|
||||
const addToLibraryUrl = new URLSearchParams(window.location.search).get(
|
||||
"addLibrary",
|
||||
);
|
||||
|
||||
if (libraryUrl) {
|
||||
await this.importLibraryFromUrl(libraryUrl);
|
||||
if (addToLibraryUrl) {
|
||||
await this.importLibraryFromUrl(addToLibraryUrl);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -775,24 +754,19 @@ class App extends React.Component<AppProps, AppState> {
|
||||
this.scene.addCallback(this.onSceneUpdated);
|
||||
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));
|
||||
|
||||
if (searchParams.has("web-share-target")) {
|
||||
// Obtain a file that was shared via the Web Share Target API.
|
||||
this.restoreFileFromShare();
|
||||
} else {
|
||||
this.updateDOMRect(this.initializeScene);
|
||||
this.setState(this.getCanvasOffsets(), () => {
|
||||
this.initializeScene();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
this.resizeObserver?.disconnect();
|
||||
this.unmounted = true;
|
||||
this.removeEventListeners();
|
||||
this.scene.destroy();
|
||||
@@ -884,11 +858,22 @@ class App extends React.Component<AppProps, AppState> {
|
||||
window.addEventListener(EVENT.DROP, this.disableEvent, false);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: AppProps, prevState: AppState) {
|
||||
componentDidUpdate(prevProps: ExcalidrawProps, prevState: AppState) {
|
||||
if (prevProps.langCode !== this.props.langCode) {
|
||||
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) {
|
||||
this.setState(
|
||||
{ viewModeEnabled: !!this.props.viewModeEnabled },
|
||||
@@ -924,13 +909,6 @@ class App extends React.Component<AppProps, AppState> {
|
||||
.querySelector(".excalidraw")
|
||||
?.classList.toggle("theme--dark", this.state.theme === "dark");
|
||||
|
||||
if (this.state.autosave && this.state.fileHandle && supported) {
|
||||
this.autosaveLocalSceneDebounced(
|
||||
this.scene.getElementsIncludingDeleted(),
|
||||
this.state,
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
this.state.editingLinearElement &&
|
||||
!this.state.selectedElementIds[this.state.editingLinearElement.elementId]
|
||||
@@ -1063,37 +1041,6 @@ class App extends React.Component<AppProps, AppState> {
|
||||
});
|
||||
}, SCROLL_TIMEOUT);
|
||||
|
||||
private autosaveLocalSceneDebounced = debounce(
|
||||
async (elements: readonly ExcalidrawElement[], state: AppState) => {
|
||||
if (this.state.autosave && this.state.fileHandle && supported) {
|
||||
try {
|
||||
await saveAsJSON(
|
||||
elements,
|
||||
state,
|
||||
// only if fileHandle valid
|
||||
true,
|
||||
);
|
||||
} catch (error) {
|
||||
this.setState({
|
||||
autosave: false,
|
||||
toastMessage:
|
||||
error.name === "NotAllowedError"
|
||||
? t("toast.autosaveFailed_notAllowed")
|
||||
: error.name === "NotFoundError"
|
||||
? t("toast.autosaveFailed_notFound")
|
||||
: t("toast.autosaveFailed"),
|
||||
});
|
||||
|
||||
// shouldn't happen, so let's log it
|
||||
if (!["NotAllowedError", "NotFoundError"].includes(error.name)) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
AUTO_SAVE_TIMEOUT,
|
||||
);
|
||||
|
||||
// Copy/paste
|
||||
|
||||
private onCut = withBatchedUpdates((event: ClipboardEvent) => {
|
||||
@@ -1361,10 +1308,6 @@ class App extends React.Component<AppProps, AppState> {
|
||||
this.setState({ toastMessage: null });
|
||||
};
|
||||
|
||||
setToastMessage = (toastMessage: string) => {
|
||||
this.setState({ toastMessage });
|
||||
};
|
||||
|
||||
restoreFileFromShare = async () => {
|
||||
try {
|
||||
const webShareTargetCache = await caches.open("web-share-target");
|
||||
@@ -1462,7 +1405,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event[KEYS.CTRL_OR_CMD] && this.state.isBindingEnabled) {
|
||||
if (event[KEYS.CTRL_OR_CMD]) {
|
||||
this.setState({ isBindingEnabled: false });
|
||||
}
|
||||
|
||||
@@ -1938,7 +1881,8 @@ class App extends React.Component<AppProps, AppState> {
|
||||
}
|
||||
|
||||
resetCursor(this.canvas);
|
||||
if (!event[KEYS.CTRL_OR_CMD] && !this.state.viewModeEnabled) {
|
||||
|
||||
if (!event[KEYS.CTRL_OR_CMD]) {
|
||||
this.startTextEditing({
|
||||
sceneX,
|
||||
sceneY,
|
||||
@@ -2312,7 +2256,10 @@ class App extends React.Component<AppProps, AppState> {
|
||||
touchTimeout = window.setTimeout(() => {
|
||||
touchTimeout = 0;
|
||||
if (!invalidateContextMenu) {
|
||||
this.handleCanvasContextMenu(event);
|
||||
this.openContextMenu({
|
||||
clientX: event.clientX,
|
||||
clientY: event.clientY,
|
||||
});
|
||||
}
|
||||
}, TOUCH_CTX_MENU_TIMEOUT);
|
||||
}
|
||||
@@ -3623,7 +3570,9 @@ class App extends React.Component<AppProps, AppState> {
|
||||
}
|
||||
};
|
||||
|
||||
private handleAppOnDrop = async (event: React.DragEvent<HTMLDivElement>) => {
|
||||
private handleCanvasOnDrop = async (
|
||||
event: React.DragEvent<HTMLCanvasElement>,
|
||||
) => {
|
||||
try {
|
||||
const file = event.dataTransfer.files[0];
|
||||
if (file?.type === "image/png" || file?.type === "image/svg+xml") {
|
||||
@@ -3657,22 +3606,14 @@ class App extends React.Component<AppProps, AppState> {
|
||||
|
||||
const file = event.dataTransfer?.files[0];
|
||||
if (
|
||||
file?.type === MIME_TYPES.excalidrawlib ||
|
||||
file?.name?.endsWith(".excalidrawlib")
|
||||
file?.type === "application/json" ||
|
||||
file?.name.endsWith(".excalidraw")
|
||||
) {
|
||||
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 });
|
||||
if (supported) {
|
||||
if (
|
||||
"chooseFileSystemEntries" in window ||
|
||||
"showOpenFilePicker" in window
|
||||
) {
|
||||
try {
|
||||
// This will only work as of Chrome 86,
|
||||
// but can be safely ignored on older releases.
|
||||
@@ -3682,7 +3623,23 @@ class App extends React.Component<AppProps, AppState> {
|
||||
console.warn(error.name, error.message);
|
||||
}
|
||||
}
|
||||
await this.loadFileToCanvas(file);
|
||||
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"),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -3707,27 +3664,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
event: React.PointerEvent<HTMLCanvasElement>,
|
||||
) => {
|
||||
event.preventDefault();
|
||||
|
||||
const { x, y } = viewportCoordsToSceneCoords(event, this.state);
|
||||
const element = this.getElementAtPosition(x, y);
|
||||
|
||||
const type = element ? "element" : "canvas";
|
||||
|
||||
const container = this.excalidrawContainerRef.current!;
|
||||
const {
|
||||
top: offsetTop,
|
||||
left: offsetLeft,
|
||||
} = container.getBoundingClientRect();
|
||||
const left = event.clientX - offsetLeft;
|
||||
const top = event.clientY - offsetTop;
|
||||
|
||||
if (element && !this.state.selectedElementIds[element.id]) {
|
||||
this.setState({ selectedElementIds: { [element.id]: true } }, () => {
|
||||
this._openContextMenu({ top, left }, type);
|
||||
});
|
||||
} else {
|
||||
this._openContextMenu({ top, left }, type);
|
||||
}
|
||||
this.openContextMenu(event);
|
||||
};
|
||||
|
||||
private maybeDragNewGenericElement = (
|
||||
@@ -3817,17 +3754,18 @@ class App extends React.Component<AppProps, AppState> {
|
||||
return false;
|
||||
};
|
||||
|
||||
/** @private use this.handleCanvasContextMenu */
|
||||
private _openContextMenu = (
|
||||
{
|
||||
left,
|
||||
top,
|
||||
}: {
|
||||
left: number;
|
||||
top: number;
|
||||
},
|
||||
type: "canvas" | "element",
|
||||
) => {
|
||||
private openContextMenu = ({
|
||||
clientX,
|
||||
clientY,
|
||||
}: {
|
||||
clientX: number;
|
||||
clientY: number;
|
||||
}) => {
|
||||
const { x, y } = viewportCoordsToSceneCoords(
|
||||
{ clientX, clientY },
|
||||
this.state,
|
||||
);
|
||||
|
||||
const maybeGroupAction = actionGroup.contextItemPredicate!(
|
||||
this.actionManager.getElementsIncludingDeleted(),
|
||||
this.actionManager.getAppState(),
|
||||
@@ -3838,22 +3776,12 @@ class App extends React.Component<AppProps, AppState> {
|
||||
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 _isMobile = isMobile();
|
||||
|
||||
const elements = this.scene.getElements();
|
||||
|
||||
const element = this.getElementAtPosition(x, y);
|
||||
const options: ContextMenuOption[] = [];
|
||||
if (probablySupportsClipboardBlob && elements.length > 0) {
|
||||
options.push(actionCopyAsPng);
|
||||
@@ -3862,7 +3790,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
if (probablySupportsClipboardWriteText && elements.length > 0) {
|
||||
options.push(actionCopyAsSvg);
|
||||
}
|
||||
if (type === "canvas") {
|
||||
if (!element) {
|
||||
const viewModeOptions = [
|
||||
...options,
|
||||
typeof this.props.gridModeEnabled === "undefined" &&
|
||||
@@ -3875,11 +3803,10 @@ class App extends React.Component<AppProps, AppState> {
|
||||
|
||||
ContextMenu.push({
|
||||
options: viewModeOptions,
|
||||
top,
|
||||
left,
|
||||
top: clientY,
|
||||
left: clientX,
|
||||
actionManager: this.actionManager,
|
||||
appState: this.state,
|
||||
container: this.excalidrawContainerRef.current!,
|
||||
});
|
||||
|
||||
if (this.state.viewModeEnabled) {
|
||||
@@ -3919,23 +3846,25 @@ class App extends React.Component<AppProps, AppState> {
|
||||
actionToggleViewMode,
|
||||
actionToggleStats,
|
||||
],
|
||||
top,
|
||||
left,
|
||||
top: clientY,
|
||||
left: clientX,
|
||||
actionManager: this.actionManager,
|
||||
appState: this.state,
|
||||
container: this.excalidrawContainerRef.current!,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.state.selectedElementIds[element.id]) {
|
||||
this.setState({ selectedElementIds: { [element.id]: true } });
|
||||
}
|
||||
|
||||
if (this.state.viewModeEnabled) {
|
||||
ContextMenu.push({
|
||||
options: [navigator.clipboard && actionCopy, ...options],
|
||||
top,
|
||||
left,
|
||||
top: clientY,
|
||||
left: clientX,
|
||||
actionManager: this.actionManager,
|
||||
appState: this.state,
|
||||
container: this.excalidrawContainerRef.current!,
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -3971,17 +3900,13 @@ class App extends React.Component<AppProps, AppState> {
|
||||
actionSendToBack,
|
||||
actionBringToFront,
|
||||
separator,
|
||||
maybeFlipHorizontal && actionFlipHorizontal,
|
||||
maybeFlipVertical && actionFlipVertical,
|
||||
(maybeFlipHorizontal || maybeFlipVertical) && separator,
|
||||
actionDuplicateSelection,
|
||||
actionDeleteSelected,
|
||||
],
|
||||
top,
|
||||
left,
|
||||
top: clientY,
|
||||
left: clientX,
|
||||
actionManager: this.actionManager,
|
||||
appState: this.state,
|
||||
container: this.excalidrawContainerRef.current!,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -4115,56 +4040,14 @@ class App extends React.Component<AppProps, AppState> {
|
||||
}
|
||||
}, 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 refresh = () => {
|
||||
public setCanvasOffsets = () => {
|
||||
this.setState({ ...this.getCanvasOffsets() });
|
||||
};
|
||||
|
||||
private getCanvasOffsets(): Pick<AppState, "offsetTop" | "offsetLeft"> {
|
||||
if (this.excalidrawContainerRef?.current) {
|
||||
const excalidrawContainer = this.excalidrawContainerRef.current;
|
||||
const { left, top } = excalidrawContainer.getBoundingClientRect();
|
||||
if (this.excalidrawContainerRef?.current?.parentElement) {
|
||||
const parentElement = this.excalidrawContainerRef.current.parentElement;
|
||||
const { left, top } = parentElement.getBoundingClientRect();
|
||||
return {
|
||||
offsetLeft: left,
|
||||
offsetTop: top,
|
||||
@@ -4198,6 +4081,9 @@ declare global {
|
||||
history: SceneHistory;
|
||||
app: InstanceType<typeof App>;
|
||||
library: typeof Library;
|
||||
collab: InstanceType<
|
||||
typeof import("../excalidraw-app/collab/CollabWrapper").default
|
||||
>;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import React from "react";
|
||||
import clsx from "clsx";
|
||||
|
||||
export const ButtonIconCycle = <T extends any>({
|
||||
@@ -13,11 +14,11 @@ export const ButtonIconCycle = <T extends any>({
|
||||
}) => {
|
||||
const current = options.find((op) => op.value === value);
|
||||
|
||||
const cycle = () => {
|
||||
function cycle() {
|
||||
const index = options.indexOf(current!);
|
||||
const next = (index + 1) % options.length;
|
||||
onChange(options[next].value);
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<label key={group} className={clsx({ active: current!.value !== null })}>
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from "react";
|
||||
import clsx from "clsx";
|
||||
import { ToolButton } from "./ToolButton";
|
||||
import { t } from "../i18n";
|
||||
import { useIsMobile } from "../is-mobile";
|
||||
import useIsMobile from "../is-mobile";
|
||||
import { users } from "./icons";
|
||||
|
||||
import "./CollabButton.scss";
|
||||
|
||||
@@ -32,63 +32,67 @@ const ContextMenu = ({
|
||||
actionManager,
|
||||
appState,
|
||||
}: ContextMenuProps) => {
|
||||
const isDarkTheme = !!document
|
||||
.querySelector(".excalidraw")
|
||||
?.classList.contains("theme--dark");
|
||||
return (
|
||||
<Popover
|
||||
onCloseRequest={onCloseRequest}
|
||||
top={top}
|
||||
left={left}
|
||||
fitInViewport={true}
|
||||
<div
|
||||
className={clsx("excalidraw", {
|
||||
"theme--dark theme--dark-background-none": isDarkTheme,
|
||||
})}
|
||||
>
|
||||
<ul
|
||||
className="context-menu"
|
||||
onContextMenu={(event) => event.preventDefault()}
|
||||
<Popover
|
||||
onCloseRequest={onCloseRequest}
|
||||
top={top}
|
||||
left={left}
|
||||
fitInViewport={true}
|
||||
>
|
||||
{options.map((option, idx) => {
|
||||
if (option === "separator") {
|
||||
return <hr key={idx} className="context-menu-option-separator" />;
|
||||
}
|
||||
<ul
|
||||
className="context-menu"
|
||||
onContextMenu={(event) => event.preventDefault()}
|
||||
>
|
||||
{options.map((option, idx) => {
|
||||
if (option === "separator") {
|
||||
return <hr key={idx} className="context-menu-option-separator" />;
|
||||
}
|
||||
|
||||
const actionName = option.name;
|
||||
const label = option.contextItemLabel
|
||||
? t(option.contextItemLabel)
|
||||
: "";
|
||||
return (
|
||||
<li key={idx} data-testid={actionName} onClick={onCloseRequest}>
|
||||
<button
|
||||
className={clsx("context-menu-option", {
|
||||
dangerous: actionName === "deleteSelectedElements",
|
||||
checkmark: option.checked?.(appState),
|
||||
})}
|
||||
onClick={() => actionManager.executeAction(option)}
|
||||
>
|
||||
<div className="context-menu-option__label">{label}</div>
|
||||
<kbd className="context-menu-option__shortcut">
|
||||
{actionName
|
||||
? getShortcutFromShortcutName(actionName as ShortcutName)
|
||||
: ""}
|
||||
</kbd>
|
||||
</button>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</Popover>
|
||||
const actionName = option.name;
|
||||
const label = option.contextItemLabel
|
||||
? t(option.contextItemLabel)
|
||||
: "";
|
||||
return (
|
||||
<li key={idx} data-testid={actionName} onClick={onCloseRequest}>
|
||||
<button
|
||||
className={clsx("context-menu-option", {
|
||||
dangerous: actionName === "deleteSelectedElements",
|
||||
checkmark: option.checked?.(appState),
|
||||
})}
|
||||
onClick={() => actionManager.executeAction(option)}
|
||||
>
|
||||
<div className="context-menu-option__label">{label}</div>
|
||||
<kbd className="context-menu-option__shortcut">
|
||||
{actionName
|
||||
? getShortcutFromShortcutName(actionName as ShortcutName)
|
||||
: ""}
|
||||
</kbd>
|
||||
</button>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</Popover>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const contextMenuNodeByContainer = new WeakMap<HTMLElement, HTMLDivElement>();
|
||||
|
||||
const getContextMenuNode = (container: HTMLElement): HTMLDivElement => {
|
||||
let contextMenuNode = contextMenuNodeByContainer.get(container);
|
||||
let contextMenuNode: HTMLDivElement;
|
||||
const getContextMenuNode = (): HTMLDivElement => {
|
||||
if (contextMenuNode) {
|
||||
return contextMenuNode;
|
||||
}
|
||||
contextMenuNode = document.createElement("div");
|
||||
container
|
||||
.querySelector(".excalidraw-contextMenuContainer")!
|
||||
.appendChild(contextMenuNode);
|
||||
contextMenuNodeByContainer.set(container, contextMenuNode);
|
||||
return contextMenuNode;
|
||||
const div = document.createElement("div");
|
||||
document.body.appendChild(div);
|
||||
return (contextMenuNode = div);
|
||||
};
|
||||
|
||||
type ContextMenuParams = {
|
||||
@@ -97,16 +101,10 @@ type ContextMenuParams = {
|
||||
left: ContextMenuProps["left"];
|
||||
actionManager: ContextMenuProps["actionManager"];
|
||||
appState: Readonly<AppState>;
|
||||
container: HTMLElement;
|
||||
};
|
||||
|
||||
const handleClose = (container: HTMLElement) => {
|
||||
const contextMenuNode = contextMenuNodeByContainer.get(container);
|
||||
if (contextMenuNode) {
|
||||
unmountComponentAtNode(contextMenuNode);
|
||||
contextMenuNode.remove();
|
||||
contextMenuNodeByContainer.delete(container);
|
||||
}
|
||||
const handleClose = () => {
|
||||
unmountComponentAtNode(getContextMenuNode());
|
||||
};
|
||||
|
||||
export default {
|
||||
@@ -123,11 +121,11 @@ export default {
|
||||
top={params.top}
|
||||
left={params.left}
|
||||
options={options}
|
||||
onCloseRequest={() => handleClose(params.container)}
|
||||
onCloseRequest={handleClose}
|
||||
actionManager={params.actionManager}
|
||||
appState={params.appState}
|
||||
/>,
|
||||
getContextMenuNode(params.container),
|
||||
getContextMenuNode(),
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -2,7 +2,7 @@ import clsx from "clsx";
|
||||
import React, { useEffect } from "react";
|
||||
import { useCallbackRefState } from "../hooks/useCallbackRefState";
|
||||
import { t } from "../i18n";
|
||||
import { useIsMobile } from "../is-mobile";
|
||||
import useIsMobile from "../is-mobile";
|
||||
import { KEYS } from "../keys";
|
||||
import "./Dialog.scss";
|
||||
import { back, close } from "./icons";
|
||||
|
||||
@@ -28,7 +28,14 @@ export const ErrorDialog = ({
|
||||
onCloseRequest={handleClose}
|
||||
title={t("errorDialog.title")}
|
||||
>
|
||||
<div style={{ whiteSpace: "pre-wrap" }}>{message}</div>
|
||||
<div>
|
||||
{message.split("\n").map((line) => (
|
||||
<>
|
||||
{line}
|
||||
<br />
|
||||
</>
|
||||
))}
|
||||
</div>
|
||||
</Dialog>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -31,16 +31,9 @@
|
||||
.ExportDialog__name {
|
||||
grid-column: project-name;
|
||||
margin: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.TextInput {
|
||||
height: calc(1rem - 3px);
|
||||
width: 200px;
|
||||
overflow: hidden;
|
||||
text-align: center;
|
||||
margin-left: 8px;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
&--readonly {
|
||||
background: none;
|
||||
@@ -48,9 +41,6 @@
|
||||
&:hover {
|
||||
background: none;
|
||||
}
|
||||
width: auto;
|
||||
max-width: 200px;
|
||||
padding-left: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { canvasToBlob } from "../data/blob";
|
||||
import { NonDeletedExcalidrawElement } from "../element/types";
|
||||
import { CanvasError } from "../errors";
|
||||
import { t } from "../i18n";
|
||||
import { useIsMobile } from "../is-mobile";
|
||||
import useIsMobile from "../is-mobile";
|
||||
import { getSelectedElements, isSomeElementSelected } from "../scene";
|
||||
import { exportToCanvas, getExportSize } from "../scene/export";
|
||||
import { AppState } from "../types";
|
||||
@@ -202,7 +202,6 @@ const ExportModal = ({
|
||||
})}
|
||||
</Stack.Row>
|
||||
</div>
|
||||
{actionManager.renderAction("toggleAutosave")}
|
||||
{actionManager.renderAction("changeExportBackground")}
|
||||
{someElementIsSelected && (
|
||||
<div>
|
||||
|
||||
@@ -349,14 +349,6 @@ export const HelpDialog = ({ onClose }: { onClose?: () => void }) => {
|
||||
label={t("labels.ungroup")}
|
||||
shortcuts={[getShortcutKey("CtrlOrCmd+Shift+G")]}
|
||||
/>
|
||||
<Shortcut
|
||||
label={t("labels.flipHorizontal")}
|
||||
shortcuts={[getShortcutKey("Shift+H")]}
|
||||
/>
|
||||
<Shortcut
|
||||
label={t("labels.flipVertical")}
|
||||
shortcuts={[getShortcutKey("Shift+V")]}
|
||||
/>
|
||||
</ShortcutIsland>
|
||||
</Column>
|
||||
</Columns>
|
||||
|
||||
@@ -9,13 +9,7 @@ type HelpIconProps = {
|
||||
};
|
||||
|
||||
export const HelpIcon = (props: HelpIconProps) => (
|
||||
<button
|
||||
className="help-icon"
|
||||
onClick={props.onClick}
|
||||
type="button"
|
||||
title={`${props.title} — ?`}
|
||||
aria-label={props.title}
|
||||
>
|
||||
{questionCircle}
|
||||
</button>
|
||||
<label title={`${props.title} — ?`} className="help-icon">
|
||||
<div onClick={props.onClick}>{questionCircle}</div>
|
||||
</label>
|
||||
);
|
||||
|
||||
@@ -14,16 +14,10 @@ import { Library } from "../data/library";
|
||||
import { isTextElement, showSelectedShapeActions } from "../element";
|
||||
import { NonDeletedExcalidrawElement } from "../element/types";
|
||||
import { Language, t } from "../i18n";
|
||||
import { useIsMobile } from "../is-mobile";
|
||||
import useIsMobile from "../is-mobile";
|
||||
import { calculateScrollCenter, getSelectedElements } from "../scene";
|
||||
import { ExportType } from "../scene/types";
|
||||
import {
|
||||
AppProps,
|
||||
AppState,
|
||||
ExcalidrawProps,
|
||||
LibraryItem,
|
||||
LibraryItems,
|
||||
} from "../types";
|
||||
import { AppState, ExcalidrawProps, LibraryItem, LibraryItems } from "../types";
|
||||
import { muteFSAbortError } from "../utils";
|
||||
import { SelectedShapeActions, ShapesSwitcher, ZoomActions } from "./Actions";
|
||||
import { BackgroundPickerAndDarkModeToggle } from "./BackgroundPickerAndDarkModeToggle";
|
||||
@@ -71,7 +65,6 @@ interface LayerUIProps {
|
||||
renderCustomFooter?: (isMobile: boolean) => JSX.Element;
|
||||
viewModeEnabled: boolean;
|
||||
libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"];
|
||||
UIOptions: AppProps["UIOptions"];
|
||||
}
|
||||
|
||||
const useOnClickOutside = (
|
||||
@@ -128,11 +121,10 @@ const LibraryMenuItems = ({
|
||||
const rows = [];
|
||||
let addedPendingElements = false;
|
||||
|
||||
const referrer =
|
||||
libraryReturnUrl || window.location.origin + window.location.pathname;
|
||||
const referrer = libraryReturnUrl || window.location.origin;
|
||||
|
||||
rows.push(
|
||||
<div className="layer-ui__library-header" key="library-header">
|
||||
<div className="layer-ui__library-header">
|
||||
<ToolButton
|
||||
key="import"
|
||||
type="button"
|
||||
@@ -142,9 +134,9 @@ const LibraryMenuItems = ({
|
||||
onClick={() => {
|
||||
importLibraryFromJSON()
|
||||
.then(() => {
|
||||
// Close and then open to get the libraries updated
|
||||
// Maybe we should close and open the menu so that the items get updated.
|
||||
// But for now we just close the menu.
|
||||
setAppState({ isLibraryOpen: false });
|
||||
setAppState({ isLibraryOpen: true });
|
||||
})
|
||||
.catch(muteFSAbortError)
|
||||
.catch((error) => {
|
||||
@@ -152,41 +144,36 @@ const LibraryMenuItems = ({
|
||||
});
|
||||
}}
|
||||
/>
|
||||
{!!library.length && (
|
||||
<>
|
||||
<ToolButton
|
||||
key="export"
|
||||
type="button"
|
||||
title={t("buttons.export")}
|
||||
aria-label={t("buttons.export")}
|
||||
icon={exportFile}
|
||||
onClick={() => {
|
||||
saveLibraryAsJSON()
|
||||
.catch(muteFSAbortError)
|
||||
.catch((error) => {
|
||||
setAppState({ errorMessage: error.message });
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<ToolButton
|
||||
key="reset"
|
||||
type="button"
|
||||
title={t("buttons.resetLibrary")}
|
||||
aria-label={t("buttons.resetLibrary")}
|
||||
icon={trash}
|
||||
onClick={() => {
|
||||
if (window.confirm(t("alerts.resetLibrary"))) {
|
||||
Library.resetLibrary();
|
||||
setLibraryItems([]);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<ToolButton
|
||||
key="export"
|
||||
type="button"
|
||||
title={t("buttons.export")}
|
||||
aria-label={t("buttons.export")}
|
||||
icon={exportFile}
|
||||
onClick={() => {
|
||||
saveLibraryAsJSON()
|
||||
.catch(muteFSAbortError)
|
||||
.catch((error) => {
|
||||
setAppState({ errorMessage: error.message });
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<ToolButton
|
||||
key="reset"
|
||||
type="button"
|
||||
title={t("buttons.resetLibrary")}
|
||||
aria-label={t("buttons.resetLibrary")}
|
||||
icon={trash}
|
||||
onClick={() => {
|
||||
if (window.confirm(t("alerts.resetLibrary"))) {
|
||||
Library.resetLibrary();
|
||||
setLibraryItems([]);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<a
|
||||
href={`https://libraries.excalidraw.com?target=${
|
||||
window.name || "_blank"
|
||||
}&referrer=${referrer}&useHash=true&token=${Library.csrfToken}`}
|
||||
href={`https://libraries.excalidraw.com?referrer=${referrer}`}
|
||||
target="_excalidraw_libraries"
|
||||
>
|
||||
{t("labels.libraries")}
|
||||
@@ -346,7 +333,6 @@ const LayerUI = ({
|
||||
renderCustomFooter,
|
||||
viewModeEnabled,
|
||||
libraryReturnUrl,
|
||||
UIOptions,
|
||||
}: LayerUIProps) => {
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
@@ -358,7 +344,6 @@ const LayerUI = ({
|
||||
href="https://blog.excalidraw.com/end-to-end-encryption/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label={t("encrypted.link")}
|
||||
>
|
||||
<Tooltip label={t("encrypted.tooltip")} position="above" long={true}>
|
||||
{shield}
|
||||
@@ -367,10 +352,6 @@ const LayerUI = ({
|
||||
);
|
||||
|
||||
const renderExportDialog = () => {
|
||||
if (!UIOptions.canvasActions.export) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const createExporter = (type: ExportType): ExportCB => async (
|
||||
exportedElements,
|
||||
scale,
|
||||
|
||||
@@ -4,7 +4,7 @@ import React, { useEffect, useRef, useState } from "react";
|
||||
import { close } from "../components/icons";
|
||||
import { MIME_TYPES } from "../constants";
|
||||
import { t } from "../i18n";
|
||||
import { useIsMobile } from "../is-mobile";
|
||||
import useIsMobile from "../is-mobile";
|
||||
import { exportToSvg } from "../scene/export";
|
||||
import { LibraryItem } from "../types";
|
||||
import "./LibraryUnit.scss";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.excalidraw {
|
||||
.popover {
|
||||
position: absolute;
|
||||
position: fixed;
|
||||
z-index: 10;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import "./TextInput.scss";
|
||||
|
||||
import React, { Component } from "react";
|
||||
import { selectNode, removeSelection } from "../utils";
|
||||
|
||||
type Props = {
|
||||
value: string;
|
||||
@@ -9,18 +10,17 @@ type Props = {
|
||||
isNameEditable: boolean;
|
||||
};
|
||||
|
||||
type State = {
|
||||
fileName: string;
|
||||
};
|
||||
export class ProjectName extends Component<Props, State> {
|
||||
state = {
|
||||
fileName: this.props.value,
|
||||
export class ProjectName extends Component<Props> {
|
||||
private handleFocus = (event: React.FocusEvent<HTMLElement>) => {
|
||||
selectNode(event.currentTarget);
|
||||
};
|
||||
private handleBlur = (event: any) => {
|
||||
const value = event.target.value;
|
||||
|
||||
private handleBlur = (event: React.FocusEvent<HTMLElement>) => {
|
||||
const value = event.currentTarget.innerText.trim();
|
||||
if (value !== this.props.value) {
|
||||
this.props.onChange(value);
|
||||
}
|
||||
removeSelection();
|
||||
};
|
||||
|
||||
private handleKeyDown = (event: React.KeyboardEvent<HTMLElement>) => {
|
||||
@@ -32,30 +32,39 @@ export class ProjectName extends Component<Props, State> {
|
||||
event.currentTarget.blur();
|
||||
}
|
||||
};
|
||||
private makeEditable = (editable: HTMLSpanElement | null) => {
|
||||
if (!editable) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
editable.contentEditable = "plaintext-only";
|
||||
} catch {
|
||||
editable.contentEditable = "true";
|
||||
}
|
||||
};
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<>
|
||||
<label htmlFor="file-name">
|
||||
{`${this.props.label}${this.props.isNameEditable ? "" : ":"}`}
|
||||
</label>
|
||||
{this.props.isNameEditable ? (
|
||||
<input
|
||||
className="TextInput"
|
||||
onBlur={this.handleBlur}
|
||||
onKeyDown={this.handleKeyDown}
|
||||
id="file-name"
|
||||
value={this.state.fileName}
|
||||
onChange={(event) =>
|
||||
this.setState({ fileName: event.target.value })
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<span className="TextInput TextInput--readonly" id="file-name">
|
||||
{this.props.value}
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
return this.props.isNameEditable ? (
|
||||
<span
|
||||
suppressContentEditableWarning
|
||||
ref={this.makeEditable}
|
||||
data-type="wysiwyg"
|
||||
className="TextInput"
|
||||
role="textbox"
|
||||
aria-label={this.props.label}
|
||||
onBlur={this.handleBlur}
|
||||
onKeyDown={this.handleKeyDown}
|
||||
onFocus={this.handleFocus}
|
||||
>
|
||||
{this.props.value}
|
||||
</span>
|
||||
) : (
|
||||
<span
|
||||
className="TextInput TextInput--readonly"
|
||||
aria-label={this.props.label}
|
||||
>
|
||||
{this.props.value}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +1,49 @@
|
||||
import React from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { copyTextToSystemClipboard } from "../clipboard";
|
||||
import { DEFAULT_VERSION } from "../constants";
|
||||
import { getCommonBounds } from "../element/bounds";
|
||||
import { NonDeletedExcalidrawElement } from "../element/types";
|
||||
import {
|
||||
getElementsStorageSize,
|
||||
getTotalStorageSize,
|
||||
} from "../excalidraw-app/data/localStorage";
|
||||
import { t } from "../i18n";
|
||||
import { useIsMobile } from "../is-mobile";
|
||||
import useIsMobile from "../is-mobile";
|
||||
import { getTargetElements } from "../scene";
|
||||
import { AppState, ExcalidrawProps } from "../types";
|
||||
import { AppState } from "../types";
|
||||
import { debounce, getVersion, nFormatter } from "../utils";
|
||||
import { close } from "./icons";
|
||||
import { Island } from "./Island";
|
||||
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: {
|
||||
appState: AppState;
|
||||
setAppState: React.Component<any, AppState>["setState"];
|
||||
elements: readonly NonDeletedExcalidrawElement[];
|
||||
onClose: () => void;
|
||||
renderCustomStats: ExcalidrawProps["renderCustomStats"];
|
||||
}) => {
|
||||
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 selectedElements = getTargetElements(props.elements, props.appState);
|
||||
@@ -26,6 +53,17 @@ export const Stats = (props: {
|
||||
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 (
|
||||
<div className="Stats">
|
||||
<Island padding={2}>
|
||||
@@ -50,7 +88,17 @@ export const Stats = (props: {
|
||||
<td>{t("stats.height")}</td>
|
||||
<td>{Math.round(boundingBox[3]) - Math.round(boundingBox[1])}</td>
|
||||
</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 && (
|
||||
<tr>
|
||||
<th colSpan={2}>{t("stats.element")}</th>
|
||||
@@ -72,17 +120,31 @@ export const Stats = (props: {
|
||||
<>
|
||||
<tr>
|
||||
<td>{"x"}</td>
|
||||
<td>{Math.round(selectedBoundingBox[0])}</td>
|
||||
<td>
|
||||
{Math.round(
|
||||
selectedElements.length === 1
|
||||
? selectedElements[0].x
|
||||
: selectedBoundingBox[0],
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{"y"}</td>
|
||||
<td>{Math.round(selectedBoundingBox[1])}</td>
|
||||
<td>
|
||||
{Math.round(
|
||||
selectedElements.length === 1
|
||||
? selectedElements[0].y
|
||||
: selectedBoundingBox[1],
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{t("stats.width")}</td>
|
||||
<td>
|
||||
{Math.round(
|
||||
selectedBoundingBox[2] - selectedBoundingBox[0],
|
||||
selectedElements.length === 1
|
||||
? selectedElements[0].width
|
||||
: selectedBoundingBox[2] - selectedBoundingBox[0],
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
@@ -90,7 +152,9 @@ export const Stats = (props: {
|
||||
<td>{t("stats.height")}</td>
|
||||
<td>
|
||||
{Math.round(
|
||||
selectedBoundingBox[3] - selectedBoundingBox[1],
|
||||
selectedElements.length === 1
|
||||
? selectedElements[0].height
|
||||
: selectedBoundingBox[3] - selectedBoundingBox[1],
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
@@ -106,7 +170,28 @@ export const Stats = (props: {
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
{props.renderCustomStats?.(props.elements, props.appState)}
|
||||
<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.setAppState({
|
||||
toastMessage: t("toast.copyToClipboard"),
|
||||
});
|
||||
} catch {}
|
||||
}}
|
||||
title={t("stats.versionCopy")}
|
||||
>
|
||||
{timestamp}
|
||||
<br />
|
||||
{hash}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</Island>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
cursor: default;
|
||||
left: 50%;
|
||||
margin-left: -150px;
|
||||
padding: 8px;
|
||||
padding: 4px 0;
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
width: 300px;
|
||||
|
||||
@@ -123,22 +123,6 @@ export const shareIOS = createIcon(
|
||||
{ width: 24, height: 24 },
|
||||
);
|
||||
|
||||
export const shareWindows = createIcon(
|
||||
<>
|
||||
<path
|
||||
stroke="currentColor"
|
||||
fill="currentColor"
|
||||
d="M40 5.6v6.1l-4.1.7c-8.9 1.4-16.5 6.9-20.6 15C13 32 10.9 43 12.4 43c.4 0 2.4-1.3 4.4-3 5-3.9 12.1-7 18.2-7.7l5-.6v12.8l11.2-11.3L62.5 22 51.2 10.8 40-.5v6.1zm10.2 22.6L44 34.5v-6.8l-6.9.6c-3.9.3-9.8 1.7-13.2 3.1-3.5 1.4-6.5 2.4-6.7 2.2-.9-1 3-7.5 6.4-10.8C28 18.6 34.4 16 40.1 16c3.7 0 3.9-.1 3.9-3.2V9.5l6.2 6.3 6.3 6.2-6.3 6.2z"
|
||||
/>
|
||||
<path
|
||||
stroke="currentColor"
|
||||
fill="currentColor"
|
||||
d="M0 36v20h48v-6.2c0-6 0-6.1-2-4.3-1.1 1-2 2.9-2 4.2V52H4V34c0-17.3-.1-18-2-18s-2 .7-2 20z"
|
||||
/>
|
||||
</>,
|
||||
{ width: 64, height: 64 },
|
||||
);
|
||||
|
||||
// Icon imported form Storybook
|
||||
// Storybook is licensed under MIT https://github.com/storybookjs/storybook/blob/next/LICENSE
|
||||
export const resetZoom = createIcon(
|
||||
@@ -810,121 +794,3 @@ export const ArrowheadBarIcon = React.memo(
|
||||
{ width: 40, height: 20 },
|
||||
),
|
||||
);
|
||||
|
||||
export const FontSizeSmallIcon = React.memo(
|
||||
({ theme }: { theme: "light" | "dark" }) =>
|
||||
createIcon(
|
||||
<path
|
||||
fill={iconFillColor(theme)}
|
||||
d="M 0 69.092 L 0 55.03 A 124.24 124.24 0 0 0 4.706 57.02 Q 6.826 57.863 8.708 58.5 A 53.466 53.466 0 0 0 12.231 59.571 Q 17.236 60.889 21.387 60.889 A 20.909 20.909 0 0 0 24.265 60.704 Q 25.719 60.502 26.903 60.077 A 8.649 8.649 0 0 0 29.028 58.985 Q 31.689 57.08 31.689 53.321 Q 31.689 51.221 30.518 49.585 A 10.126 10.126 0 0 0 29.282 48.177 Q 28.352 47.287 27.075 46.436 A 23.719 23.719 0 0 0 25.752 45.627 Q 23.774 44.492 20.176 42.735 A 254.44 254.44 0 0 0 17.822 41.602 Q 11.503 38.631 8.236 35.888 A 19.742 19.742 0 0 1 8.008 35.694 A 22.18 22.18 0 0 1 2.783 29.102 Q 0.83 25.342 0.83 20.313 A 22.471 22.471 0 0 1 1.733 13.778 A 17.283 17.283 0 0 1 7.251 5.42 A 21.486 21.486 0 0 1 15.177 1.272 Q 18.361 0.338 22.166 0.09 A 43.573 43.573 0 0 1 25 0 A 42.399 42.399 0 0 1 34.349 1.01 A 39.075 39.075 0 0 1 35.62 1.319 A 67.407 67.407 0 0 1 42.108 3.382 A 83.357 83.357 0 0 1 46.191 5.03 L 41.309 16.797 Q 35.596 14.453 31.86 13.526 A 30.762 30.762 0 0 0 25.417 12.612 A 28.337 28.337 0 0 0 24.512 12.598 A 14.846 14.846 0 0 0 22.022 12.793 Q 19.498 13.224 17.92 14.6 Q 15.625 16.602 15.625 19.824 Q 15.625 21.826 16.553 23.316 Q 17.48 24.805 19.507 26.197 A 18.343 18.343 0 0 0 20.659 26.912 Q 22.596 28.035 26.516 29.953 A 299.99 299.99 0 0 0 29.102 31.201 Q 37.91 35.412 41.841 39.642 A 16.553 16.553 0 0 1 42.822 40.796 A 17.675 17.675 0 0 1 46.301 49.233 A 23.517 23.517 0 0 1 46.533 52.588 A 21.581 21.581 0 0 1 45.471 59.515 A 17.733 17.733 0 0 1 39.575 67.823 Q 33.745 72.486 24.094 73.243 A 49.683 49.683 0 0 1 20.215 73.389 A 51.712 51.712 0 0 1 9.448 72.315 A 40.672 40.672 0 0 1 0 69.092 Z"
|
||||
/>,
|
||||
{ width: 47, height: 77 },
|
||||
),
|
||||
);
|
||||
|
||||
export const FontSizeMediumIcon = React.memo(
|
||||
({ theme }: { theme: "light" | "dark" }) =>
|
||||
createIcon(
|
||||
<path
|
||||
fill={iconFillColor(theme)}
|
||||
d="M 44.092 71.387 L 30.225 71.387 L 13.037 15.381 L 12.598 15.381 A 1505.093 1505.093 0 0 1 12.959 22.313 Q 13.426 31.715 13.508 36.4 A 102.991 102.991 0 0 1 13.525 38.184 L 13.525 71.387 L 0 71.387 L 0 0 L 20.605 0 L 37.5 54.59 L 37.793 54.59 L 55.713 0 L 76.318 0 L 76.318 71.387 L 62.207 71.387 L 62.207 37.598 Q 62.207 35.205 62.28 32.08 A 160.703 160.703 0 0 1 62.326 30.544 Q 62.452 26.754 62.866 17.168 A 5390.536 5390.536 0 0 1 62.939 15.479 L 62.5 15.479 L 44.092 71.387 Z"
|
||||
/>,
|
||||
{ width: 77, height: 75 },
|
||||
),
|
||||
);
|
||||
|
||||
export const FontSizeLargeIcon = React.memo(
|
||||
({ theme }: { theme: "light" | "dark" }) =>
|
||||
createIcon(
|
||||
<path
|
||||
fill={iconFillColor(theme)}
|
||||
d="M 44.092 71.387 L 0 71.387 L 0 0 L 15.137 0 L 15.137 58.887 L 44.092 58.887 L 44.092 71.387 Z"
|
||||
/>,
|
||||
{ width: 45, height: 75 },
|
||||
),
|
||||
);
|
||||
|
||||
export const FontSizeExtraLargeIcon = React.memo(
|
||||
({ theme }: { theme: "light" | "dark" }) =>
|
||||
createIcon(
|
||||
<path
|
||||
fill={iconFillColor(theme)}
|
||||
d="M 42.578 35.4 L 66.699 71.387 L 49.414 71.387 L 32.813 44.385 L 16.211 71.387 L 0 71.387 L 23.682 34.57 L 1.514 0 L 18.213 0 L 33.594 25.684 L 48.682 0 L 64.99 0 L 42.578 35.4 Z M 119.775 71.387 L 75.684 71.387 L 75.684 0 L 90.82 0 L 90.82 58.887 L 119.775 58.887 L 119.775 71.387 Z"
|
||||
/>,
|
||||
{ width: 120, height: 75 },
|
||||
),
|
||||
);
|
||||
|
||||
export const FontFamilyHandDrawnIcon = React.memo(
|
||||
({ theme }: { theme: "light" | "dark" }) =>
|
||||
createIcon(
|
||||
<path
|
||||
fill={iconFillColor(theme)}
|
||||
d="M290.74 93.24l128.02 128.02-277.99 277.99-114.14 12.6C11.35 513.54-1.56 500.62.14 485.34l12.7-114.22 277.9-277.88zm207.2-19.06l-60.11-60.11c-18.75-18.75-49.16-18.75-67.91 0l-56.55 56.55 128.02 128.02 56.55-56.55c18.75-18.76 18.75-49.16 0-67.91z"
|
||||
/>,
|
||||
{ width: 448, height: 512 },
|
||||
),
|
||||
);
|
||||
|
||||
export const FontFamilyNormalIcon = React.memo(
|
||||
({ theme }: { theme: "light" | "dark" }) =>
|
||||
createIcon(
|
||||
<>
|
||||
<path
|
||||
fill={iconFillColor(theme)}
|
||||
d="M 63.818 71.68 L 54.492 71.68 L 45.898 49.561 L 17.578 49.561 L 9.082 71.68 L 0 71.68 L 27.881 0 L 35.986 0 L 63.818 71.68 Z M 20.605 41.602 L 43.213 41.602 L 35.205 19.971 L 31.787 9.277 Q 30.322 15.137 28.711 19.971 L 20.605 41.602 Z"
|
||||
/>
|
||||
<path
|
||||
fill={iconFillColor(theme)}
|
||||
d="M 68.994 71.68 L 52.686 71.68 L 47.51 54.688 L 21.484 54.688 L 16.309 71.68 L 0 71.68 L 25.195 0 L 43.701 0 L 68.994 71.68 Z M 25.293 41.992 L 43.896 41.992 A 27590.463 27590.463 0 0 1 42.2 36.532 Q 36.965 19.676 35.937 16.273 A 120.932 120.932 0 0 1 35.815 15.869 A 131.65 131.65 0 0 1 35.396 14.435 Q 34.951 12.879 34.675 11.741 A 34.866 34.866 0 0 1 34.521 11.084 A 141.762 141.762 0 0 1 33.706 14.075 Q 31.482 21.957 25.293 41.992 Z"
|
||||
/>
|
||||
</>,
|
||||
{ width: 70, height: 78 },
|
||||
),
|
||||
);
|
||||
|
||||
export const FontFamilyCodeIcon = React.memo(
|
||||
({ theme }: { theme: "light" | "dark" }) =>
|
||||
createIcon(
|
||||
<>
|
||||
<path
|
||||
fill={iconFillColor(theme)}
|
||||
d="M278.9 511.5l-61-17.7c-6.4-1.8-10-8.5-8.2-14.9L346.2 8.7c1.8-6.4 8.5-10 14.9-8.2l61 17.7c6.4 1.8 10 8.5 8.2 14.9L293.8 503.3c-1.9 6.4-8.5 10.1-14.9 8.2zm-114-112.2l43.5-46.4c4.6-4.9 4.3-12.7-.8-17.2L117 256l90.6-79.7c5.1-4.5 5.5-12.3.8-17.2l-43.5-46.4c-4.5-4.8-12.1-5.1-17-.5L3.8 247.2c-5.1 4.7-5.1 12.8 0 17.5l144.1 135.1c4.9 4.6 12.5 4.4 17-.5zm327.2.6l144.1-135.1c5.1-4.7 5.1-12.8 0-17.5L492.1 112.1c-4.8-4.5-12.4-4.3-17 .5L431.6 159c-4.6 4.9-4.3 12.7.8 17.2L523 256l-90.6 79.7c-5.1 4.5-5.5 12.3-.8 17.2l43.5 46.4c4.5 4.9 12.1 5.1 17 .6z"
|
||||
/>
|
||||
</>,
|
||||
{ width: 640, height: 512 },
|
||||
),
|
||||
);
|
||||
|
||||
export const TextAlignLeftIcon = React.memo(
|
||||
({ theme }: { theme: "light" | "dark" }) =>
|
||||
createIcon(
|
||||
<path
|
||||
d="M12.83 352h262.34A12.82 12.82 0 00288 339.17v-38.34A12.82 12.82 0 00275.17 288H12.83A12.82 12.82 0 000 300.83v38.34A12.82 12.82 0 0012.83 352zm0-256h262.34A12.82 12.82 0 00288 83.17V44.83A12.82 12.82 0 00275.17 32H12.83A12.82 12.82 0 000 44.83v38.34A12.82 12.82 0 0012.83 96zM432 160H16a16 16 0 00-16 16v32a16 16 0 0016 16h416a16 16 0 0016-16v-32a16 16 0 00-16-16zm0 256H16a16 16 0 00-16 16v32a16 16 0 0016 16h416a16 16 0 0016-16v-32a16 16 0 00-16-16z"
|
||||
fill={iconFillColor(theme)}
|
||||
/>,
|
||||
{ width: 448, height: 512 },
|
||||
),
|
||||
);
|
||||
|
||||
export const TextAlignCenterIcon = React.memo(
|
||||
({ theme }: { theme: "light" | "dark" }) =>
|
||||
createIcon(
|
||||
<path
|
||||
d="M432 160H16a16 16 0 00-16 16v32a16 16 0 0016 16h416a16 16 0 0016-16v-32a16 16 0 00-16-16zm0 256H16a16 16 0 00-16 16v32a16 16 0 0016 16h416a16 16 0 0016-16v-32a16 16 0 00-16-16zM108.1 96h231.81A12.09 12.09 0 00352 83.9V44.09A12.09 12.09 0 00339.91 32H108.1A12.09 12.09 0 0096 44.09V83.9A12.1 12.1 0 00108.1 96zm231.81 256A12.09 12.09 0 00352 339.9v-39.81A12.09 12.09 0 00339.91 288H108.1A12.09 12.09 0 0096 300.09v39.81a12.1 12.1 0 0012.1 12.1z"
|
||||
fill={iconFillColor(theme)}
|
||||
/>,
|
||||
{ width: 448, height: 512 },
|
||||
),
|
||||
);
|
||||
|
||||
export const TextAlignRightIcon = React.memo(
|
||||
({ theme }: { theme: "light" | "dark" }) =>
|
||||
createIcon(
|
||||
<path
|
||||
d="M16 224h416a16 16 0 0016-16v-32a16 16 0 00-16-16H16a16 16 0 00-16 16v32a16 16 0 0016 16zm416 192H16a16 16 0 00-16 16v32a16 16 0 0016 16h416a16 16 0 0016-16v-32a16 16 0 00-16-16zm3.17-384H172.83A12.82 12.82 0 00160 44.83v38.34A12.82 12.82 0 00172.83 96h262.34A12.82 12.82 0 00448 83.17V44.83A12.82 12.82 0 00435.17 32zm0 256H172.83A12.82 12.82 0 00160 300.83v38.34A12.82 12.82 0 00172.83 352h262.34A12.82 12.82 0 00448 339.17v-38.34A12.82 12.82 0 00435.17 288z"
|
||||
fill={iconFillColor(theme)}
|
||||
/>,
|
||||
{ width: 448, height: 512 },
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { FontFamily } from "./element/types";
|
||||
import cssVariables from "./css/variables.module.scss";
|
||||
import { AppProps } from "./types";
|
||||
|
||||
export const APP_NAME = "Excalidraw";
|
||||
|
||||
@@ -85,15 +84,9 @@ export const MIME_TYPES = {
|
||||
excalidrawlib: "application/vnd.excalidrawlib+json",
|
||||
};
|
||||
|
||||
export const EXPORT_DATA_TYPES = {
|
||||
excalidraw: "excalidraw",
|
||||
excalidrawClipboard: "excalidraw/clipboard",
|
||||
excalidrawLibrary: "excalidrawlib",
|
||||
} as const;
|
||||
|
||||
export const STORAGE_KEYS = {
|
||||
LOCAL_STORAGE_LIBRARY: "excalidraw-library",
|
||||
} as const;
|
||||
};
|
||||
|
||||
// time in milliseconds
|
||||
export const TAP_TWICE_TIMEOUT = 300;
|
||||
@@ -101,7 +94,6 @@ export const TOUCH_CTX_MENU_TIMEOUT = 500;
|
||||
export const TITLE_TIMEOUT = 10000;
|
||||
export const TOAST_TIMEOUT = 5000;
|
||||
export const VERSION_TIMEOUT = 30000;
|
||||
export const AUTO_SAVE_TIMEOUT = 500;
|
||||
export const SCROLL_TIMEOUT = 100;
|
||||
|
||||
export const ZOOM_STEP = 0.1;
|
||||
@@ -118,23 +110,3 @@ export const MODES = {
|
||||
};
|
||||
|
||||
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,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -16,8 +16,6 @@
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
// serves 2 purposes:
|
||||
// 1. prevent selecting text outside the component when double-clicking or
|
||||
@@ -49,12 +47,6 @@
|
||||
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 {
|
||||
// The percentage is inspired by
|
||||
// https://material.io/design/color/dark-theme.html#properties, which
|
||||
@@ -230,8 +222,7 @@
|
||||
align-items: center;
|
||||
svg {
|
||||
width: 36px;
|
||||
height: 14px;
|
||||
padding: 2px;
|
||||
height: 18px;
|
||||
opacity: 0.6;
|
||||
}
|
||||
&.active svg {
|
||||
@@ -462,14 +453,6 @@
|
||||
fill: $oc-gray-6;
|
||||
bottom: 14px;
|
||||
width: 1.5rem;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
background: none;
|
||||
color: var(--icon-fill-color);
|
||||
|
||||
&:hover {
|
||||
background: none;
|
||||
}
|
||||
|
||||
:root[dir="ltr"] & {
|
||||
right: 14px;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { cleanAppStateForExport } from "../appState";
|
||||
import { EXPORT_DATA_TYPES } from "../constants";
|
||||
import { MIME_TYPES } from "../constants";
|
||||
import { clearElementsForExport } from "../element";
|
||||
import { CanvasError } from "../errors";
|
||||
import { t } from "../i18n";
|
||||
@@ -95,7 +95,13 @@ export const loadFromBlob = async (
|
||||
elements: clearElementsForExport(data.elements || []),
|
||||
appState: {
|
||||
theme: localAppState?.theme,
|
||||
fileHandle: (!blob.type.startsWith("image/") && blob.handle) || null,
|
||||
fileHandle:
|
||||
blob.handle &&
|
||||
["application/json", MIME_TYPES.excalidraw].includes(
|
||||
getMimeType(blob),
|
||||
)
|
||||
? blob.handle
|
||||
: null,
|
||||
...cleanAppStateForExport(data.appState || {}),
|
||||
...(localAppState
|
||||
? calculateScrollCenter(data.elements || [], localAppState, null)
|
||||
@@ -115,7 +121,7 @@ export const loadFromBlob = async (
|
||||
export const loadLibraryFromBlob = async (blob: Blob) => {
|
||||
const contents = await parseFileContents(blob);
|
||||
const data: LibraryData = JSON.parse(contents);
|
||||
if (data.type !== EXPORT_DATA_TYPES.excalidrawLibrary) {
|
||||
if (data.type !== "excalidrawlib") {
|
||||
throw new Error(t("alerts.couldNotLoadInvalidFile"));
|
||||
}
|
||||
return data;
|
||||
|
||||
@@ -2,7 +2,7 @@ import decodePng from "png-chunks-extract";
|
||||
import tEXt from "png-chunk-text";
|
||||
import encodePng from "png-chunks-encode";
|
||||
import { stringToBase64, encode, decode, base64ToString } from "./encode";
|
||||
import { EXPORT_DATA_TYPES, MIME_TYPES } from "../constants";
|
||||
import { MIME_TYPES } from "../constants";
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// PNG
|
||||
@@ -67,10 +67,7 @@ export const decodePngMetadata = async (blob: Blob) => {
|
||||
const encodedData = JSON.parse(metadata.text);
|
||||
if (!("encoded" in encodedData)) {
|
||||
// legacy, un-encoded scene JSON
|
||||
if (
|
||||
"type" in encodedData &&
|
||||
encodedData.type === EXPORT_DATA_TYPES.excalidraw
|
||||
) {
|
||||
if ("type" in encodedData && encodedData.type === "excalidraw") {
|
||||
return metadata.text;
|
||||
}
|
||||
throw new Error("FAILED");
|
||||
@@ -118,10 +115,7 @@ export const decodeSvgMetadata = async ({ svg }: { svg: string }) => {
|
||||
const encodedData = JSON.parse(json);
|
||||
if (!("encoded" in encodedData)) {
|
||||
// legacy, un-encoded scene JSON
|
||||
if (
|
||||
"type" in encodedData &&
|
||||
encodedData.type === EXPORT_DATA_TYPES.excalidraw
|
||||
) {
|
||||
if ("type" in encodedData && encodedData.type === "excalidraw") {
|
||||
return json;
|
||||
}
|
||||
throw new Error("FAILED");
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { fileOpen, fileSave } from "browser-fs-access";
|
||||
import { cleanAppStateForExport } from "../appState";
|
||||
import { EXPORT_DATA_TYPES, MIME_TYPES } from "../constants";
|
||||
import { MIME_TYPES } from "../constants";
|
||||
import { clearElementsForExport } from "../element";
|
||||
import { ExcalidrawElement } from "../element/types";
|
||||
import { AppState } from "../types";
|
||||
@@ -14,7 +14,7 @@ export const serializeAsJSON = (
|
||||
): string =>
|
||||
JSON.stringify(
|
||||
{
|
||||
type: EXPORT_DATA_TYPES.excalidraw,
|
||||
type: "excalidraw",
|
||||
version: 2,
|
||||
source: window.location.origin,
|
||||
elements: clearElementsForExport(elements),
|
||||
@@ -27,7 +27,6 @@ export const serializeAsJSON = (
|
||||
export const saveAsJSON = async (
|
||||
elements: readonly ExcalidrawElement[],
|
||||
appState: AppState,
|
||||
onlyIfFileHandleValid = false,
|
||||
) => {
|
||||
const serialized = serializeAsJSON(elements, appState);
|
||||
const blob = new Blob([serialized], {
|
||||
@@ -42,7 +41,6 @@ export const saveAsJSON = async (
|
||||
extensions: [".excalidraw"],
|
||||
},
|
||||
appState.fileHandle,
|
||||
onlyIfFileHandleValid,
|
||||
);
|
||||
return { fileHandle };
|
||||
};
|
||||
@@ -71,7 +69,7 @@ export const isValidExcalidrawData = (data?: {
|
||||
appState?: any;
|
||||
}): data is ImportedDataState => {
|
||||
return (
|
||||
data?.type === EXPORT_DATA_TYPES.excalidraw &&
|
||||
data?.type === "excalidraw" &&
|
||||
(!data.elements ||
|
||||
(Array.isArray(data.elements) &&
|
||||
(!data.appState || typeof data.appState === "object")))
|
||||
@@ -82,7 +80,7 @@ export const isValidLibrary = (json: any) => {
|
||||
return (
|
||||
typeof json === "object" &&
|
||||
json &&
|
||||
json.type === EXPORT_DATA_TYPES.excalidrawLibrary &&
|
||||
json.type === "excalidrawlib" &&
|
||||
json.version === 1
|
||||
);
|
||||
};
|
||||
@@ -91,7 +89,7 @@ export const saveLibraryAsJSON = async () => {
|
||||
const library = await Library.loadLibrary();
|
||||
const serialized = JSON.stringify(
|
||||
{
|
||||
type: EXPORT_DATA_TYPES.excalidrawLibrary,
|
||||
type: "excalidrawlib",
|
||||
version: 1,
|
||||
library,
|
||||
},
|
||||
@@ -118,5 +116,5 @@ export const importLibraryFromJSON = async () => {
|
||||
extensions: [".json", ".excalidrawlib"],
|
||||
*/
|
||||
});
|
||||
await Library.importLibrary(blob);
|
||||
Library.importLibrary(blob);
|
||||
};
|
||||
|
||||
@@ -4,11 +4,9 @@ import { restoreElements } from "./restore";
|
||||
import { STORAGE_KEYS } from "../constants";
|
||||
import { getNonDeletedElements } from "../element";
|
||||
import { NonDeleted, ExcalidrawElement } from "../element/types";
|
||||
import { nanoid } from "nanoid";
|
||||
|
||||
export class Library {
|
||||
private static libraryCache: LibraryItems | null = null;
|
||||
public static csrfToken = nanoid();
|
||||
|
||||
static resetLibrary = () => {
|
||||
Library.libraryCache = null;
|
||||
|
||||
@@ -144,7 +144,7 @@ export const restoreElements = (
|
||||
export const restoreAppState = (
|
||||
appState: ImportedDataState["appState"],
|
||||
localAppState: Partial<AppState> | null,
|
||||
): DataState["appState"] => {
|
||||
): AppState => {
|
||||
appState = appState || {};
|
||||
|
||||
const defaultAppState = getDefaultAppState();
|
||||
@@ -166,6 +166,8 @@ export const restoreAppState = (
|
||||
|
||||
return {
|
||||
...nextAppState,
|
||||
offsetLeft: appState.offsetLeft || 0,
|
||||
offsetTop: appState.offsetTop || 0,
|
||||
// Migrates from previous version where appState.zoom was a number
|
||||
zoom:
|
||||
typeof appState.zoom === "number"
|
||||
|
||||
@@ -6,7 +6,7 @@ export interface DataState {
|
||||
version?: string;
|
||||
source?: string;
|
||||
elements: readonly ExcalidrawElement[];
|
||||
appState: Omit<AppState, "offsetTop" | "offsetLeft" | "width" | "height">;
|
||||
appState: MarkOptional<AppState, "offsetTop" | "offsetLeft">;
|
||||
}
|
||||
|
||||
export interface ImportedDataState {
|
||||
|
||||
@@ -87,30 +87,9 @@ export const mutateElement = <TElement extends Mutable<ExcalidrawElement>>(
|
||||
export const newElementWith = <TElement extends ExcalidrawElement>(
|
||||
element: TElement,
|
||||
updates: ElementUpdate<TElement>,
|
||||
): TElement => {
|
||||
let didChange = false;
|
||||
for (const key in updates) {
|
||||
const value = (updates as any)[key];
|
||||
if (typeof value !== "undefined") {
|
||||
if (
|
||||
(element as any)[key] === value &&
|
||||
// if object, always update in case its deep prop was mutated
|
||||
(typeof value !== "object" || value === null || key === "groupIds")
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
didChange = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!didChange) {
|
||||
return element;
|
||||
}
|
||||
|
||||
return {
|
||||
...element,
|
||||
...updates,
|
||||
version: element.version + 1,
|
||||
versionNonce: randomInteger(),
|
||||
};
|
||||
};
|
||||
): TElement => ({
|
||||
...element,
|
||||
...updates,
|
||||
version: element.version + 1,
|
||||
versionNonce: randomInteger(),
|
||||
});
|
||||
|
||||
@@ -31,7 +31,7 @@ import {
|
||||
import { PointerDownState } from "../components/App";
|
||||
import { Point } from "../types";
|
||||
|
||||
export const normalizeAngle = (angle: number): number => {
|
||||
const normalizeAngle = (angle: number): number => {
|
||||
if (angle >= 2 * Math.PI) {
|
||||
return angle - 2 * Math.PI;
|
||||
}
|
||||
@@ -181,7 +181,7 @@ const getPerfectElementSizeWithRotation = (
|
||||
return rotate(size.width, size.height, 0, 0, -angle);
|
||||
};
|
||||
|
||||
export const reshapeSingleTwoPointElement = (
|
||||
const reshapeSingleTwoPointElement = (
|
||||
element: NonDeleted<ExcalidrawLinearElement>,
|
||||
resizeArrowDirection: "origin" | "end",
|
||||
isRotateWithDiscreteAngle: boolean,
|
||||
@@ -378,7 +378,7 @@ const resizeSingleTextElement = (
|
||||
}
|
||||
};
|
||||
|
||||
export const resizeSingleElement = (
|
||||
const resizeSingleElement = (
|
||||
stateAtResizeStart: NonDeletedExcalidrawElement,
|
||||
shouldKeepSidesRatio: boolean,
|
||||
element: NonDeletedExcalidrawElement,
|
||||
|
||||
@@ -207,8 +207,7 @@ export const textWysiwyg = ({
|
||||
// prevent blur when changing properties from the menu
|
||||
const onPointerDown = (event: MouseEvent) => {
|
||||
if (
|
||||
(event.target instanceof HTMLElement ||
|
||||
event.target instanceof SVGElement) &&
|
||||
event.target instanceof HTMLElement &&
|
||||
event.target.closest(`.${CLASSES.SHAPE_ACTIONS_MENU}`) &&
|
||||
!isWritableElement(event.target)
|
||||
) {
|
||||
|
||||
@@ -1,85 +0,0 @@
|
||||
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;
|
||||
@@ -38,7 +38,7 @@ import Portal from "./Portal";
|
||||
import RoomDialog from "./RoomDialog";
|
||||
import { createInverseContext } from "../../createInverseContext";
|
||||
import { t } from "../../i18n";
|
||||
import { UserIdleState } from "../../types";
|
||||
import { UserIdleState } from "./types";
|
||||
import { IDLE_THRESHOLD, ACTIVE_THRESHOLD } from "../../constants";
|
||||
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.DEVELOPMENT
|
||||
) {
|
||||
window.collab = window.collab || ({} as Window["collab"]);
|
||||
Object.defineProperties(window, {
|
||||
window.h = window.h || ({} as Window["h"]);
|
||||
Object.defineProperties(window.h, {
|
||||
collab: {
|
||||
configurable: true,
|
||||
value: this,
|
||||
@@ -448,8 +448,15 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
|
||||
|
||||
private handleRemoteSceneUpdate = (
|
||||
elements: ReconciledElements,
|
||||
{ init = false }: { init?: boolean } = {},
|
||||
{
|
||||
init = false,
|
||||
initFromSnapshot = false,
|
||||
}: { init?: boolean; initFromSnapshot?: boolean } = {},
|
||||
) => {
|
||||
if (init || initFromSnapshot) {
|
||||
this.excalidrawAPI.setScrollToContent(elements);
|
||||
}
|
||||
|
||||
this.excalidrawAPI.updateScene({
|
||||
elements,
|
||||
commitToHistory: !!init,
|
||||
@@ -658,17 +665,4 @@ 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;
|
||||
|
||||
@@ -9,7 +9,7 @@ import CollabWrapper from "./CollabWrapper";
|
||||
import { getSyncableElements } from "../../packages/excalidraw/index";
|
||||
import { ExcalidrawElement } from "../../element/types";
|
||||
import { BROADCAST, SCENE } from "../app_constants";
|
||||
import { UserIdleState } from "../../types";
|
||||
import { UserIdleState } from "./types";
|
||||
import { trackEvent } from "../../analytics";
|
||||
|
||||
class Portal {
|
||||
|
||||
@@ -7,27 +7,12 @@ import {
|
||||
stop,
|
||||
share,
|
||||
shareIOS,
|
||||
shareWindows,
|
||||
} from "../../components/icons";
|
||||
import { ToolButton } from "../../components/ToolButton";
|
||||
import { t } from "../../i18n";
|
||||
import "./RoomDialog.scss";
|
||||
import Stack from "../../components/Stack";
|
||||
|
||||
const getShareIcon = () => {
|
||||
const navigator = window.navigator as any;
|
||||
const isAppleBrowser = /Apple/.test(navigator.vendor);
|
||||
const isWindowsBrowser = navigator.appVersion.indexOf("Win") !== -1;
|
||||
|
||||
if (isAppleBrowser) {
|
||||
return shareIOS;
|
||||
} else if (isWindowsBrowser) {
|
||||
return shareWindows;
|
||||
}
|
||||
|
||||
return share;
|
||||
};
|
||||
|
||||
const RoomDialog = ({
|
||||
handleClose,
|
||||
activeRoomLink,
|
||||
@@ -46,6 +31,8 @@ const RoomDialog = ({
|
||||
setErrorMessage: (message: string) => void;
|
||||
}) => {
|
||||
const roomLinkInput = useRef<HTMLInputElement>(null);
|
||||
const navigator = window.navigator as any;
|
||||
const isAppleBrowser = /Apple/.test(navigator.vendor);
|
||||
|
||||
const copyRoomLink = async () => {
|
||||
try {
|
||||
@@ -106,7 +93,7 @@ const RoomDialog = ({
|
||||
{"share" in navigator ? (
|
||||
<ToolButton
|
||||
type="button"
|
||||
icon={getShareIcon()}
|
||||
icon={isAppleBrowser ? shareIOS : share}
|
||||
title={t("labels.share")}
|
||||
aria-label={t("labels.share")}
|
||||
onClick={shareRoomLink}
|
||||
|
||||
5
src/excalidraw-app/collab/types.ts
Normal file
5
src/excalidraw-app/collab/types.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export enum UserIdleState {
|
||||
ACTIVE = "active",
|
||||
AWAY = "away",
|
||||
IDLE = "idle",
|
||||
}
|
||||
@@ -3,7 +3,8 @@ import { restore } from "../../data/restore";
|
||||
import { ImportedDataState } from "../../data/types";
|
||||
import { ExcalidrawElement } from "../../element/types";
|
||||
import { t } from "../../i18n";
|
||||
import { AppState, UserIdleState } from "../../types";
|
||||
import { AppState } from "../../types";
|
||||
import { UserIdleState } from "../collab/types";
|
||||
|
||||
const byteToHex = (byte: number): string => `0${byte.toString(16)}`.slice(-2);
|
||||
|
||||
@@ -79,10 +80,8 @@ export type SocketUpdateData = SocketUpdateDataSource[keyof SocketUpdateDataSour
|
||||
_brand: "socketUpdateData";
|
||||
};
|
||||
|
||||
const IV_LENGTH_BYTES = 12; // 96 bits
|
||||
|
||||
export const createIV = () => {
|
||||
const arr = new Uint8Array(IV_LENGTH_BYTES);
|
||||
const arr = new Uint8Array(12);
|
||||
return window.crypto.getRandomValues(arr);
|
||||
};
|
||||
|
||||
@@ -176,22 +175,6 @@ export const getImportedKey = (key: string, usage: KeyUsage) =>
|
||||
[usage],
|
||||
);
|
||||
|
||||
const decryptImported = async (
|
||||
iv: ArrayBuffer,
|
||||
encrypted: ArrayBuffer,
|
||||
privateKey: string,
|
||||
): Promise<ArrayBuffer> => {
|
||||
const key = await getImportedKey(privateKey, "decrypt");
|
||||
return window.crypto.subtle.decrypt(
|
||||
{
|
||||
name: "AES-GCM",
|
||||
iv,
|
||||
},
|
||||
key,
|
||||
encrypted,
|
||||
);
|
||||
};
|
||||
|
||||
const importFromBackend = async (
|
||||
id: string | null,
|
||||
privateKey?: string | null,
|
||||
@@ -200,7 +183,6 @@ const importFromBackend = async (
|
||||
const response = await fetch(
|
||||
privateKey ? `${BACKEND_V2_GET}${id}` : `${BACKEND_GET}${id}.json`,
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
window.alert(t("alerts.importBackendFailed"));
|
||||
return {};
|
||||
@@ -208,19 +190,16 @@ const importFromBackend = async (
|
||||
let data: ImportedDataState;
|
||||
if (privateKey) {
|
||||
const buffer = await response.arrayBuffer();
|
||||
|
||||
let decrypted: ArrayBuffer;
|
||||
try {
|
||||
// Buffer should contain both the IV (fixed length) and encrypted data
|
||||
const iv = buffer.slice(0, IV_LENGTH_BYTES);
|
||||
const encrypted = buffer.slice(IV_LENGTH_BYTES, buffer.byteLength);
|
||||
decrypted = await decryptImported(iv, encrypted, privateKey);
|
||||
} catch (error) {
|
||||
// Fixed IV (old format, backward compatibility)
|
||||
const fixedIv = new Uint8Array(IV_LENGTH_BYTES);
|
||||
decrypted = await decryptImported(fixedIv, buffer, privateKey);
|
||||
}
|
||||
|
||||
const key = await getImportedKey(privateKey, "decrypt");
|
||||
const iv = new Uint8Array(12);
|
||||
const decrypted = await window.crypto.subtle.decrypt(
|
||||
{
|
||||
name: "AES-GCM",
|
||||
iv,
|
||||
},
|
||||
key,
|
||||
buffer,
|
||||
);
|
||||
// We need to convert the decrypted array buffer to a string
|
||||
const string = new window.TextDecoder("utf-8").decode(
|
||||
new Uint8Array(decrypted) as any,
|
||||
@@ -284,8 +263,9 @@ export const exportToBackend = async (
|
||||
true, // extractable
|
||||
["encrypt", "decrypt"],
|
||||
);
|
||||
|
||||
const iv = createIV();
|
||||
// The iv is set to 0. We are never going to reuse the same key so we don't
|
||||
// need to have an iv. (I hope that's correct...)
|
||||
const iv = new Uint8Array(12);
|
||||
// We use symmetric encryption. AES-GCM is the recommended algorithm and
|
||||
// includes checks that the ciphertext has not been modified by an attacker.
|
||||
const encrypted = await window.crypto.subtle.encrypt(
|
||||
@@ -296,11 +276,6 @@ export const exportToBackend = async (
|
||||
key,
|
||||
encoded,
|
||||
);
|
||||
|
||||
// Concatenate IV with encrypted data (IV does not have to be secret).
|
||||
const payloadBlob = new Blob([iv.buffer, encrypted]);
|
||||
const payload = await new Response(payloadBlob).arrayBuffer();
|
||||
|
||||
// We use jwk encoding to be able to extract just the base64 encoded key.
|
||||
// We will hardcode the rest of the attributes when importing back the key.
|
||||
const exportedKey = await window.crypto.subtle.exportKey("jwk", key);
|
||||
@@ -308,7 +283,7 @@ export const exportToBackend = async (
|
||||
try {
|
||||
const response = await fetch(BACKEND_V2_POST, {
|
||||
method: "POST",
|
||||
body: payload,
|
||||
body: encrypted,
|
||||
});
|
||||
const json = await response.json();
|
||||
if (json.id) {
|
||||
|
||||
@@ -101,7 +101,7 @@ export const importFromLocalStorage = () => {
|
||||
export const getElementsStorageSize = () => {
|
||||
try {
|
||||
const elements = localStorage.getItem(STORAGE_KEYS.LOCAL_STORAGE_ELEMENTS);
|
||||
const elementsSize = elements?.length || 0;
|
||||
const elementsSize = elements ? JSON.stringify(elements).length : 0;
|
||||
return elementsSize;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
@@ -117,9 +117,9 @@ export const getTotalStorageSize = () => {
|
||||
APP_STORAGE_KEYS.LOCAL_STORAGE_LIBRARY,
|
||||
);
|
||||
|
||||
const appStateSize = appState?.length || 0;
|
||||
const collabSize = collab?.length || 0;
|
||||
const librarySize = library?.length || 0;
|
||||
const appStateSize = appState ? JSON.stringify(appState).length : 0;
|
||||
const collabSize = collab ? JSON.stringify(collab).length : 0;
|
||||
const librarySize = library ? JSON.stringify(library).length : 0;
|
||||
|
||||
return appStateSize + collabSize + librarySize + getElementsStorageSize();
|
||||
} catch (error) {
|
||||
|
||||
@@ -3,6 +3,7 @@ import React, {
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
@@ -11,13 +12,7 @@ import { getDefaultAppState } from "../appState";
|
||||
import { ExcalidrawImperativeAPI } from "../components/App";
|
||||
import { ErrorDialog } from "../components/ErrorDialog";
|
||||
import { TopErrorBoundary } from "../components/TopErrorBoundary";
|
||||
import {
|
||||
APP_NAME,
|
||||
EVENT,
|
||||
TITLE_TIMEOUT,
|
||||
URL_HASH_KEYS,
|
||||
VERSION_TIMEOUT,
|
||||
} from "../constants";
|
||||
import { APP_NAME, EVENT, TITLE_TIMEOUT, VERSION_TIMEOUT } from "../constants";
|
||||
import { loadFromBlob } from "../data/blob";
|
||||
import { DataState, ImportedDataState } from "../data/types";
|
||||
import {
|
||||
@@ -49,7 +44,6 @@ import {
|
||||
importFromLocalStorage,
|
||||
saveToLocalStorage,
|
||||
} from "./data/localStorage";
|
||||
import CustomStats from "./CustomStats";
|
||||
|
||||
const languageDetector = new LanguageDetector();
|
||||
languageDetector.init({
|
||||
@@ -161,11 +155,31 @@ const initializeScene = async (opts: {
|
||||
return null;
|
||||
};
|
||||
|
||||
const ExcalidrawWrapper = () => {
|
||||
function ExcalidrawWrapper() {
|
||||
// dimensions
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const [dimensions, setDimensions] = useState({
|
||||
width: window.innerWidth,
|
||||
height: window.innerHeight,
|
||||
});
|
||||
const [errorMessage, setErrorMessage] = useState("");
|
||||
const currentLangCode = languageDetector.detect() || defaultLang.code;
|
||||
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
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -199,24 +213,12 @@ const ExcalidrawWrapper = () => {
|
||||
initialStatePromiseRef.current.promise.resolve(scene);
|
||||
});
|
||||
|
||||
const onHashChange = (event: HashChangeEvent) => {
|
||||
event.preventDefault();
|
||||
const hash = new URLSearchParams(window.location.hash.slice(1));
|
||||
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 onHashChange = (_: HashChangeEvent) => {
|
||||
initializeScene({ collabAPI }).then((scene) => {
|
||||
if (scene) {
|
||||
excalidrawAPI.updateScene(scene);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const titleTimeout = setTimeout(
|
||||
@@ -303,19 +305,13 @@ const ExcalidrawWrapper = () => {
|
||||
[langCode],
|
||||
);
|
||||
|
||||
const renderCustomStats = () => {
|
||||
return (
|
||||
<CustomStats
|
||||
setToastMessage={(message) => excalidrawAPI!.setToastMessage(message)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Excalidraw
|
||||
ref={excalidrawRefCallback}
|
||||
onChange={onChange}
|
||||
width={dimensions.width}
|
||||
height={dimensions.height}
|
||||
initialData={initialStatePromiseRef.current.promise}
|
||||
onCollabButtonClick={collabAPI?.onCollabButtonClick}
|
||||
isCollaborating={collabAPI?.isCollaborating()}
|
||||
@@ -323,7 +319,6 @@ const ExcalidrawWrapper = () => {
|
||||
onExportToBackend={onExportToBackend}
|
||||
renderFooter={renderFooter}
|
||||
langCode={langCode}
|
||||
renderCustomStats={renderCustomStats}
|
||||
/>
|
||||
{excalidrawAPI && <CollabWrapper excalidrawAPI={excalidrawAPI} />}
|
||||
{errorMessage && (
|
||||
@@ -334,9 +329,9 @@ const ExcalidrawWrapper = () => {
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
const ExcalidrawApp = () => {
|
||||
export default function ExcalidrawApp() {
|
||||
return (
|
||||
<TopErrorBoundary>
|
||||
<CollabContextConsumer>
|
||||
@@ -344,6 +339,4 @@ const ExcalidrawApp = () => {
|
||||
</CollabContextConsumer>
|
||||
</TopErrorBoundary>
|
||||
);
|
||||
};
|
||||
|
||||
export default ExcalidrawApp;
|
||||
}
|
||||
|
||||
2
src/global.d.ts
vendored
2
src/global.d.ts
vendored
@@ -89,5 +89,3 @@ interface Blob {
|
||||
handle?: import("browser-fs-acces").FileSystemHandle;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
declare module "*.scss";
|
||||
|
||||
@@ -63,8 +63,6 @@ const canvas = exportToCanvas(
|
||||
...getDefaultAppState(),
|
||||
offsetTop: 0,
|
||||
offsetLeft: 0,
|
||||
width: 0,
|
||||
height: 0,
|
||||
},
|
||||
{
|
||||
exportBackground: true,
|
||||
|
||||
@@ -34,4 +34,7 @@ export const IsMobileProvider = ({
|
||||
};
|
||||
|
||||
export const isMobile = () => getIsMobileMatcher().matches;
|
||||
export const useIsMobile = () => useContext(context);
|
||||
|
||||
export default function useIsMobile() {
|
||||
return useContext(context);
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
"architect": "معماري",
|
||||
"artist": "رسام",
|
||||
"cartoonist": "كرتوني",
|
||||
"fileTitle": "",
|
||||
"fileTitle": "عنوان الملف",
|
||||
"colorPicker": "اختيار الألوان",
|
||||
"canvasBackground": "خلفية اللوحة",
|
||||
"drawingCanvas": "لوحة الرسم",
|
||||
@@ -77,7 +77,7 @@
|
||||
"group": "تحديد مجموعة",
|
||||
"ungroup": "إلغاء تحديد مجموعة",
|
||||
"collaborators": "المتعاونون",
|
||||
"showGrid": "إظهار الشبكة",
|
||||
"showGrid": "",
|
||||
"addToLibrary": "أضف إلى المكتبة",
|
||||
"removeFromLibrary": "حذف من المكتبة",
|
||||
"libraryLoadingMessage": "جارٍ تحميل المكتبة…",
|
||||
@@ -92,11 +92,9 @@
|
||||
"centerHorizontally": "توسيط أفقي",
|
||||
"distributeHorizontally": "التوزيع الأفقي",
|
||||
"distributeVertically": "التوزيع عمودياً",
|
||||
"flipHorizontal": "",
|
||||
"flipVertical": "",
|
||||
"viewMode": "نمط العرض",
|
||||
"viewMode": "",
|
||||
"toggleExportColorScheme": "",
|
||||
"share": "مشاركة"
|
||||
"share": ""
|
||||
},
|
||||
"buttons": {
|
||||
"clearReset": "إعادة تعيين اللوحة",
|
||||
@@ -121,7 +119,7 @@
|
||||
"edit": "تعديل",
|
||||
"undo": "تراجع",
|
||||
"redo": "إعادة تنفيذ",
|
||||
"resetLibrary": "إعادة ضبط المكتبة",
|
||||
"resetLibrary": "",
|
||||
"createNewRoom": "إنشاء غرفة جديدة",
|
||||
"fullScreen": "شاشة كاملة",
|
||||
"darkMode": "الوضع المظلم",
|
||||
@@ -140,7 +138,7 @@
|
||||
"decryptFailed": "تعذر فك تشفير البيانات.",
|
||||
"uploadedSecurly": "تم تأمين التحميل بتشفير النهاية إلى النهاية، مما يعني أن خادوم Excalidraw والأطراف الثالثة لا يمكنها قراءة المحتوى.",
|
||||
"loadSceneOverridePrompt": "تحميل الرسم الخارجي سيحل محل المحتوى الموجود لديك. هل ترغب في المتابعة؟",
|
||||
"collabStopOverridePrompt": "إيقاف الجلسة سيؤدي إلى الكتابة فوق رسومك السابقة المخزنة داخليا. هل أنت متأكد؟\n\n(إذا كنت ترغب في الاحتفاظ برسمك المخزن داخليا، ببساطة أغلق علامة تبويب المتصفح بدلاً من ذلك.)",
|
||||
"collabStopOverridePrompt": "",
|
||||
"errorLoadingLibrary": "حصل خطأ أثناء تحميل مكتبة الطرف الثالث.",
|
||||
"confirmAddLibrary": "هذا سيضيف {{numShapes}} شكل إلى مكتبتك. هل أنت متأكد؟",
|
||||
"imageDoesNotContainScene": "استيراد الصور غير مدعوم في الوقت الراهن.\n\nهل تريد استيراد مشهد؟ لا يبدو أن هذه الصورة تحتوي على أي بيانات مشهد. هل قمت بسماح هذا أثناء التصدير؟",
|
||||
@@ -214,9 +212,9 @@
|
||||
"curvedArrow": "",
|
||||
"curvedLine": "",
|
||||
"documentation": "",
|
||||
"drag": "اسحب",
|
||||
"editor": "المحرر",
|
||||
"github": "عثرت على مشكلة؟ إرسال",
|
||||
"drag": "",
|
||||
"editor": "",
|
||||
"github": "",
|
||||
"howto": "",
|
||||
"or": "",
|
||||
"preventBinding": "",
|
||||
@@ -230,8 +228,7 @@
|
||||
"zoomToSelection": ""
|
||||
},
|
||||
"encrypted": {
|
||||
"tooltip": "رسوماتك مشفرة من النهاية إلى النهاية حتى أن خوادم Excalidraw لن تراها أبدا.",
|
||||
"link": ""
|
||||
"tooltip": "رسوماتك مشفرة من النهاية إلى النهاية حتى أن خوادم Excalidraw لن تراها أبدا."
|
||||
},
|
||||
"stats": {
|
||||
"angle": "الزاوية",
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
"architect": "Архитект",
|
||||
"artist": "Художник",
|
||||
"cartoonist": "Карикатурист",
|
||||
"fileTitle": "",
|
||||
"fileTitle": "Заглавие на файл",
|
||||
"colorPicker": "Избор на цвят",
|
||||
"canvasBackground": "Фон на платно",
|
||||
"drawingCanvas": "Платно за рисуване",
|
||||
@@ -92,8 +92,6 @@
|
||||
"centerHorizontally": "Центрирай хоризонтално",
|
||||
"distributeHorizontally": "Разпредели хоризонтално",
|
||||
"distributeVertically": "Разпредели вертикално",
|
||||
"flipHorizontal": "",
|
||||
"flipVertical": "",
|
||||
"viewMode": "Изглед",
|
||||
"toggleExportColorScheme": "",
|
||||
"share": ""
|
||||
@@ -230,8 +228,7 @@
|
||||
"zoomToSelection": "Приближи селекцията"
|
||||
},
|
||||
"encrypted": {
|
||||
"tooltip": "Вашите рисунки са криптирани от край до край, така че сървърите на Excalidraw няма да могат да ги виждат.",
|
||||
"link": ""
|
||||
"tooltip": "Вашите рисунки са криптирани от край до край, така че сървърите на Excalidraw няма да могат да ги виждат."
|
||||
},
|
||||
"stats": {
|
||||
"angle": "Ъгъл",
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
"architect": "Arquitecte",
|
||||
"artist": "Artista",
|
||||
"cartoonist": "Dibuixant",
|
||||
"fileTitle": "",
|
||||
"fileTitle": "Títol del fitxer",
|
||||
"colorPicker": "Selector de colors",
|
||||
"canvasBackground": "Fons del llenç",
|
||||
"drawingCanvas": "Llenç de dibuix",
|
||||
@@ -92,8 +92,6 @@
|
||||
"centerHorizontally": "Centrar horitzontalment",
|
||||
"distributeHorizontally": "Distribuir horitzontalment",
|
||||
"distributeVertically": "Distribuir verticalment",
|
||||
"flipHorizontal": "",
|
||||
"flipVertical": "",
|
||||
"viewMode": "Mode de visualització",
|
||||
"toggleExportColorScheme": "Canvia l'esquema de colors de l'exportació",
|
||||
"share": "Compartir"
|
||||
@@ -230,8 +228,7 @@
|
||||
"zoomToSelection": "Zoom per veure la selecció"
|
||||
},
|
||||
"encrypted": {
|
||||
"tooltip": "Els vostres dibuixos estan xifrats de punta a punta de manera que els servidors d’Excalidraw no els veuran mai.",
|
||||
"link": ""
|
||||
"tooltip": "Els vostres dibuixos estan xifrats de punta a punta de manera que els servidors d’Excalidraw no els veuran mai."
|
||||
},
|
||||
"stats": {
|
||||
"angle": "Angle",
|
||||
|
||||
@@ -92,8 +92,6 @@
|
||||
"centerHorizontally": "Horizontal zentrieren",
|
||||
"distributeHorizontally": "Horizontal verteilen",
|
||||
"distributeVertically": "Vertikal verteilen",
|
||||
"flipHorizontal": "Horizontal spiegeln",
|
||||
"flipVertical": "Vertikal spiegeln",
|
||||
"viewMode": "Ansichtsmodus",
|
||||
"toggleExportColorScheme": "Farbschema für Export umschalten",
|
||||
"share": "Teilen"
|
||||
@@ -230,8 +228,7 @@
|
||||
"zoomToSelection": "Auf Auswahl zoomen"
|
||||
},
|
||||
"encrypted": {
|
||||
"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"
|
||||
"tooltip": "Da deine Zeichnungen Ende-zu-Ende verschlüsselt werden, sehen auch unsere Excalidraw-Server sie niemals."
|
||||
},
|
||||
"stats": {
|
||||
"angle": "Winkel",
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
"architect": "Αρχιτέκτονας",
|
||||
"artist": "Καλλιτέχνης",
|
||||
"cartoonist": "Σκιτσογράφος",
|
||||
"fileTitle": "Όνομα αρχείου",
|
||||
"fileTitle": "Τίτλος αρχείου",
|
||||
"colorPicker": "Επιλογή Χρώματος",
|
||||
"canvasBackground": "Φόντο καμβά",
|
||||
"drawingCanvas": "Σχεδίαση καμβά",
|
||||
@@ -92,11 +92,9 @@
|
||||
"centerHorizontally": "Κέντρο οριζόντια",
|
||||
"distributeHorizontally": "Οριζόντια κατανομή",
|
||||
"distributeVertically": "Κατακόρυφη κατανομή",
|
||||
"flipHorizontal": "Οριζόντια αναστροφή",
|
||||
"flipVertical": "Κατακόρυφη αναστροφή",
|
||||
"viewMode": "Λειτουργία προβολής",
|
||||
"toggleExportColorScheme": "Εναλλαγή εξαγωγής θέματος χρωμάτων",
|
||||
"share": "Κοινοποίηση"
|
||||
"share": ""
|
||||
},
|
||||
"buttons": {
|
||||
"clearReset": "Επαναφορά του καμβά",
|
||||
@@ -230,8 +228,7 @@
|
||||
"zoomToSelection": "Ζουμ στην επιλογή"
|
||||
},
|
||||
"encrypted": {
|
||||
"tooltip": "Τα σχέδιά σου είναι κρυπτογραφημένα από άκρο σε άκρο, έτσι δεν θα είναι ποτέ ορατά μέσα από τους διακομιστές του Excalidraw.",
|
||||
"link": ""
|
||||
"tooltip": "Τα σχέδιά σου είναι κρυπτογραφημένα από άκρο σε άκρο, έτσι δεν θα είναι ποτέ ορατά μέσα από τους διακομιστές του Excalidraw."
|
||||
},
|
||||
"stats": {
|
||||
"angle": "Γωνία",
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
"architect": "Architect",
|
||||
"artist": "Artist",
|
||||
"cartoonist": "Cartoonist",
|
||||
"fileTitle": "File name",
|
||||
"fileTitle": "File title",
|
||||
"colorPicker": "Color picker",
|
||||
"canvasBackground": "Canvas background",
|
||||
"drawingCanvas": "Drawing canvas",
|
||||
@@ -78,8 +78,6 @@
|
||||
"ungroup": "Ungroup selection",
|
||||
"collaborators": "Collaborators",
|
||||
"showGrid": "Show grid",
|
||||
"toggleAutosave": "Autosave to current file",
|
||||
"toggleAutosave_details": "Automatically save changes when working on an existing file.",
|
||||
"addToLibrary": "Add to library",
|
||||
"removeFromLibrary": "Remove from library",
|
||||
"libraryLoadingMessage": "Loading library…",
|
||||
@@ -94,8 +92,6 @@
|
||||
"centerHorizontally": "Center horizontally",
|
||||
"distributeHorizontally": "Distribute horizontally",
|
||||
"distributeVertically": "Distribute vertically",
|
||||
"flipHorizontal": "Flip horizontal",
|
||||
"flipVertical": "Flip vertical",
|
||||
"viewMode": "View mode",
|
||||
"toggleExportColorScheme": "Toggle export color scheme",
|
||||
"share": "Share"
|
||||
@@ -232,8 +228,7 @@
|
||||
"zoomToSelection": "Zoom to selection"
|
||||
},
|
||||
"encrypted": {
|
||||
"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"
|
||||
"tooltip": "Your drawings are end-to-end encrypted so Excalidraw's servers will never see them."
|
||||
},
|
||||
"stats": {
|
||||
"angle": "Angle",
|
||||
@@ -251,9 +246,6 @@
|
||||
"width": "Width"
|
||||
},
|
||||
"toast": {
|
||||
"autosaveFailed_notAllowed": "Autosave was disabled.",
|
||||
"autosaveFailed_notFound": "Autosave failed.\nIt seems the file no longer exists.",
|
||||
"autosaveFailed": "Autosave failed.",
|
||||
"copyStyles": "Copied styles.",
|
||||
"copyToClipboard": "Copied to clipboard.",
|
||||
"copyToClipboardAsPng": "Copied {{exportSelection}} to clipboard as PNG\n({{exportColorScheme}})",
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
"architect": "Arquitecto",
|
||||
"artist": "Artista",
|
||||
"cartoonist": "Caricatura",
|
||||
"fileTitle": "Nombre del archivo",
|
||||
"fileTitle": "Título del archivo",
|
||||
"colorPicker": "Selector de color",
|
||||
"canvasBackground": "Fondo del lienzo",
|
||||
"drawingCanvas": "Lienzo de dibujo",
|
||||
@@ -92,8 +92,6 @@
|
||||
"centerHorizontally": "Centrar horizontalmente",
|
||||
"distributeHorizontally": "Distribuir horizontalmente",
|
||||
"distributeVertically": "Distribuir verticalmente",
|
||||
"flipHorizontal": "Girar horizontalmente",
|
||||
"flipVertical": "Girar verticalmente",
|
||||
"viewMode": "Modo presentación",
|
||||
"toggleExportColorScheme": "Cambiar el esquema de colores de exportación",
|
||||
"share": "Compartir"
|
||||
@@ -230,8 +228,7 @@
|
||||
"zoomToSelection": "Hacer zoom a la selección"
|
||||
},
|
||||
"encrypted": {
|
||||
"tooltip": "Tus dibujos están cifrados de punto a punto, por lo que los servidores de Excalidraw nunca los verán.",
|
||||
"link": ""
|
||||
"tooltip": "Tus dibujos están cifrados de punto a punto, por lo que los servidores de Excalidraw nunca los verán."
|
||||
},
|
||||
"stats": {
|
||||
"angle": "Ángulo",
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
"architect": "معمار",
|
||||
"artist": "هنرمند",
|
||||
"cartoonist": "کارتونیست",
|
||||
"fileTitle": "",
|
||||
"fileTitle": "عنوان فایل",
|
||||
"colorPicker": "انتخابگر رنگ",
|
||||
"canvasBackground": "بوم",
|
||||
"drawingCanvas": "بوم نقاشی",
|
||||
@@ -92,8 +92,6 @@
|
||||
"centerHorizontally": "وسط قرار دادن به صورت افقی",
|
||||
"distributeHorizontally": "توزیع کردن به صورت افقی",
|
||||
"distributeVertically": "توزیع کردن به صورت عمودی",
|
||||
"flipHorizontal": "",
|
||||
"flipVertical": "",
|
||||
"viewMode": "",
|
||||
"toggleExportColorScheme": "",
|
||||
"share": ""
|
||||
@@ -230,8 +228,7 @@
|
||||
"zoomToSelection": "بزرگنمایی قسمت انتخاب شده"
|
||||
},
|
||||
"encrypted": {
|
||||
"tooltip": "شما در یک محیط رمزگزاری شده دو طرفه در حال طراحی هستید پس Excalidraw هرگز طرح های شما را نمیبند.",
|
||||
"link": ""
|
||||
"tooltip": "شما در یک محیط رمزگزاری شده دو طرفه در حال طراحی هستید پس Excalidraw هرگز طرح های شما را نمیبند."
|
||||
},
|
||||
"stats": {
|
||||
"angle": "زاویه",
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
"architect": "Arkkitehti",
|
||||
"artist": "Taiteilija",
|
||||
"cartoonist": "Sarjakuva",
|
||||
"fileTitle": "Tiedostonimi",
|
||||
"fileTitle": "Tiedoston otsikko",
|
||||
"colorPicker": "Värin valinta",
|
||||
"canvasBackground": "Piirtoalueen tausta",
|
||||
"drawingCanvas": "Piirtoalue",
|
||||
@@ -92,8 +92,6 @@
|
||||
"centerHorizontally": "Keskitä vaakasuunnassa",
|
||||
"distributeHorizontally": "Jaa vaakasuunnassa",
|
||||
"distributeVertically": "Jaa pystysuunnassa",
|
||||
"flipHorizontal": "Käännä vaakasuunnassa",
|
||||
"flipVertical": "Käännä pystysuunnassa",
|
||||
"viewMode": "Katselutila",
|
||||
"toggleExportColorScheme": "Vaihda viennin väriteema",
|
||||
"share": "Jaa"
|
||||
@@ -230,8 +228,7 @@
|
||||
"zoomToSelection": "Näytä valinta"
|
||||
},
|
||||
"encrypted": {
|
||||
"tooltip": "Piirroksesi ovat päästä-päähän-salattuja, joten Excalidrawin palvelimet eivät koskaan näe niitä.",
|
||||
"link": ""
|
||||
"tooltip": "Piirroksesi ovat päästä-päähän-salattuja, joten Excalidrawin palvelimet eivät koskaan näe niitä."
|
||||
},
|
||||
"stats": {
|
||||
"angle": "Kulma",
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
"architect": "Architecte",
|
||||
"artist": "Artiste",
|
||||
"cartoonist": "Caricaturiste",
|
||||
"fileTitle": "Nom du fichier",
|
||||
"fileTitle": "Titre du fichier",
|
||||
"colorPicker": "Sélecteur de couleur",
|
||||
"canvasBackground": "Arrière-plan du canevas",
|
||||
"drawingCanvas": "Zone de dessin",
|
||||
@@ -92,8 +92,6 @@
|
||||
"centerHorizontally": "Centrer horizontalement",
|
||||
"distributeHorizontally": "Distribuer horizontalement",
|
||||
"distributeVertically": "Distribuer verticalement",
|
||||
"flipHorizontal": "Retourner horizontalement",
|
||||
"flipVertical": "Retourner verticalement",
|
||||
"viewMode": "Mode présentation",
|
||||
"toggleExportColorScheme": "Activer/Désactiver l'export du thème de couleur",
|
||||
"share": "Partager"
|
||||
@@ -230,8 +228,7 @@
|
||||
"zoomToSelection": "Zoomer sur la sélection"
|
||||
},
|
||||
"encrypted": {
|
||||
"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"
|
||||
"tooltip": "Vos dessins sont chiffrés de bout en bout, les serveurs d'Excalidraw ne les verront jamais."
|
||||
},
|
||||
"stats": {
|
||||
"angle": "Angle",
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
"architect": "ארכיטקט",
|
||||
"artist": "אמן",
|
||||
"cartoonist": "קריקטוריסט",
|
||||
"fileTitle": "",
|
||||
"fileTitle": "כותרת הקובץ",
|
||||
"colorPicker": "בחירת צבע",
|
||||
"canvasBackground": "רקע הלוח",
|
||||
"drawingCanvas": "לוח ציור",
|
||||
@@ -92,8 +92,6 @@
|
||||
"centerHorizontally": "מרכז אופקית",
|
||||
"distributeHorizontally": "חלוקה אופקית",
|
||||
"distributeVertically": "חלוקה אנכית",
|
||||
"flipHorizontal": "",
|
||||
"flipVertical": "",
|
||||
"viewMode": "מצב תצוגה",
|
||||
"toggleExportColorScheme": "",
|
||||
"share": ""
|
||||
@@ -230,8 +228,7 @@
|
||||
"zoomToSelection": "התמקד בבחירה"
|
||||
},
|
||||
"encrypted": {
|
||||
"tooltip": "הרישומים שלך מוצפנים מקצה לקצה כך שהשרתים של Excalidraw לא יראו אותם לעולם.",
|
||||
"link": ""
|
||||
"tooltip": "הרישומים שלך מוצפנים מקצה לקצה כך שהשרתים של Excalidraw לא יראו אותם לעולם."
|
||||
},
|
||||
"stats": {
|
||||
"angle": "זווית",
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
"architect": "वास्तुकार",
|
||||
"artist": "कलाकार",
|
||||
"cartoonist": "व्यंग्य चित्रकार",
|
||||
"fileTitle": "फ़ाइल का नाम",
|
||||
"fileTitle": "फ़ाइल का शीर्षक",
|
||||
"colorPicker": "रंग चयन",
|
||||
"canvasBackground": "कैनवास बैकग्राउंड",
|
||||
"drawingCanvas": "कैनवास बना रहे हैं",
|
||||
@@ -92,8 +92,6 @@
|
||||
"centerHorizontally": "क्षैतिज केन्द्रित",
|
||||
"distributeHorizontally": "क्षैतिज रूप से वितरित करें",
|
||||
"distributeVertically": "खड़ी रूप से वितरित करें",
|
||||
"flipHorizontal": "",
|
||||
"flipVertical": "",
|
||||
"viewMode": "",
|
||||
"toggleExportColorScheme": "",
|
||||
"share": ""
|
||||
@@ -230,8 +228,7 @@
|
||||
"zoomToSelection": "चयन तक ज़ूम करे"
|
||||
},
|
||||
"encrypted": {
|
||||
"tooltip": "आपके चित्र अंत-से-अंत एन्क्रिप्टेड हैं, इसलिए एक्सक्लूसिव्रॉव के सर्वर उन्हें कभी नहीं देखेंगे।",
|
||||
"link": ""
|
||||
"tooltip": "आपके चित्र अंत-से-अंत एन्क्रिप्टेड हैं, इसलिए एक्सक्लूसिव्रॉव के सर्वर उन्हें कभी नहीं देखेंगे।"
|
||||
},
|
||||
"stats": {
|
||||
"angle": "कोण",
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
"architect": "Tervezői",
|
||||
"artist": "Művészi",
|
||||
"cartoonist": "Karikatúrás",
|
||||
"fileTitle": "",
|
||||
"fileTitle": "Fájl címe",
|
||||
"colorPicker": "Színválasztó",
|
||||
"canvasBackground": "Vászon háttérszíne",
|
||||
"drawingCanvas": "Rajzvászon",
|
||||
@@ -92,8 +92,6 @@
|
||||
"centerHorizontally": "Vízszintesen középre igazított",
|
||||
"distributeHorizontally": "Vízszintes elosztás",
|
||||
"distributeVertically": "Függőleges elosztás",
|
||||
"flipHorizontal": "",
|
||||
"flipVertical": "",
|
||||
"viewMode": "",
|
||||
"toggleExportColorScheme": "",
|
||||
"share": ""
|
||||
@@ -230,8 +228,7 @@
|
||||
"zoomToSelection": ""
|
||||
},
|
||||
"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.",
|
||||
"link": ""
|
||||
"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."
|
||||
},
|
||||
"stats": {
|
||||
"angle": "Szög",
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
"architect": "Arsitek",
|
||||
"artist": "Artis",
|
||||
"cartoonist": "Kartunis",
|
||||
"fileTitle": "Nama file",
|
||||
"fileTitle": "Judul file",
|
||||
"colorPicker": "Pilihan Warna",
|
||||
"canvasBackground": "Latar Kanvas",
|
||||
"drawingCanvas": "Kanvas",
|
||||
@@ -92,8 +92,6 @@
|
||||
"centerHorizontally": "Pusatkan secara horizontal",
|
||||
"distributeHorizontally": "Distribusikan horizontal",
|
||||
"distributeVertically": "Distribusikan vertikal",
|
||||
"flipHorizontal": "Balikkan horizontal",
|
||||
"flipVertical": "Balikkan vertikal",
|
||||
"viewMode": "Mode tampilan",
|
||||
"toggleExportColorScheme": "Ubah skema warna ekspor",
|
||||
"share": "Bagikan"
|
||||
@@ -145,7 +143,7 @@
|
||||
"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?",
|
||||
"cannotRestoreFromImage": "Pemandangan tidak dapat dipulihkan dari file gambar ini",
|
||||
"invalidSceneUrl": "Tidak dapat impor pemandangan dari URL. Kemungkinan URL itu rusak atau tidak berisi data JSON Excalidraw yang valid.",
|
||||
"invalidSceneUrl": "",
|
||||
"resetLibrary": "Ini akan menghapus pustaka Anda. Anda yakin?"
|
||||
},
|
||||
"toolBar": {
|
||||
@@ -230,8 +228,7 @@
|
||||
"zoomToSelection": "Perbesar ke seleksi"
|
||||
},
|
||||
"encrypted": {
|
||||
"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"
|
||||
"tooltip": "Gambar anda terenkripsi end-to-end sehingga server Excalidraw tidak akan pernah dapat melihatnya."
|
||||
},
|
||||
"stats": {
|
||||
"angle": "Sudut",
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
"architect": "Architetto",
|
||||
"artist": "Artista",
|
||||
"cartoonist": "Fumettista",
|
||||
"fileTitle": "Nome del file",
|
||||
"fileTitle": "Titolo del file",
|
||||
"colorPicker": "Selettore colore",
|
||||
"canvasBackground": "Sfondo tela",
|
||||
"drawingCanvas": "Area di disegno",
|
||||
@@ -92,8 +92,6 @@
|
||||
"centerHorizontally": "Centra orizzontalmente",
|
||||
"distributeHorizontally": "Distribuisci orizzontalmente",
|
||||
"distributeVertically": "Distribuisci verticalmente",
|
||||
"flipHorizontal": "Capovolgi orizzontalmente",
|
||||
"flipVertical": "Capovolgi verticalmente",
|
||||
"viewMode": "Modalità visualizzazione",
|
||||
"toggleExportColorScheme": "Cambia lo schema di colori in esportazione",
|
||||
"share": "Condividi"
|
||||
@@ -230,8 +228,7 @@
|
||||
"zoomToSelection": "Zoom alla selezione"
|
||||
},
|
||||
"encrypted": {
|
||||
"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"
|
||||
"tooltip": "I tuoi disegni sono crittografati end-to-end in modo che i server di Excalidraw non li possano mai vedere."
|
||||
},
|
||||
"stats": {
|
||||
"angle": "Angolo",
|
||||
|
||||
@@ -92,8 +92,6 @@
|
||||
"centerHorizontally": "横方向に中央揃え",
|
||||
"distributeHorizontally": "水平方向に分散配置",
|
||||
"distributeVertically": "垂直方向に分散配置",
|
||||
"flipHorizontal": "水平方向に反転",
|
||||
"flipVertical": "垂直方向に反転",
|
||||
"viewMode": "閲覧モード",
|
||||
"toggleExportColorScheme": "",
|
||||
"share": ""
|
||||
@@ -203,7 +201,7 @@
|
||||
"desc_inProgressIntro": "共同編集セッションが有効になっています。",
|
||||
"desc_shareLink": "下記URLを共同編集したい人に共有してください:",
|
||||
"desc_exitSession": "セッションを終了すると部屋から切断されますが、手元の環境で編集を続けることができます。変更内容は他の人には反映されません。",
|
||||
"shareTitle": "Excalidrawのライブコラボレーションセッションに参加する"
|
||||
"shareTitle": ""
|
||||
},
|
||||
"errorDialog": {
|
||||
"title": "エラー"
|
||||
@@ -230,8 +228,7 @@
|
||||
"zoomToSelection": "選択要素にズーム"
|
||||
},
|
||||
"encrypted": {
|
||||
"tooltip": "描画内容はエンドツーエンド暗号化が施されており、Excalidrawサーバーが内容を見ることはできません。",
|
||||
"link": "Excalidrawのエンドツーエンド暗号化に関するブログ記事"
|
||||
"tooltip": "描画内容はエンドツーエンド暗号化が施されており、Excalidrawサーバーが内容を見ることはできません。"
|
||||
},
|
||||
"stats": {
|
||||
"angle": "角度",
|
||||
@@ -254,7 +251,7 @@
|
||||
"copyToClipboardAsPng": "",
|
||||
"fileSaved": "ファイルを保存しました",
|
||||
"fileSavedToFilename": "{filename} に保存しました",
|
||||
"canvas": "キャンバス",
|
||||
"canvas": "",
|
||||
"selection": ""
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
"architect": "Amasdag",
|
||||
"artist": "Anaẓur",
|
||||
"cartoonist": "",
|
||||
"fileTitle": "Isem n ufaylu",
|
||||
"fileTitle": "Azwel n ufaylu",
|
||||
"colorPicker": "Amafran n yini",
|
||||
"canvasBackground": "Agilal n teɣzut n usuneɣ",
|
||||
"drawingCanvas": "Taɣzut n usuneɣ",
|
||||
@@ -92,8 +92,6 @@
|
||||
"centerHorizontally": "Di tlemmast s uglawi",
|
||||
"distributeHorizontally": "Freq s uglawi",
|
||||
"distributeVertically": "Freq s yibeddi",
|
||||
"flipHorizontal": "Tuttya taglawant",
|
||||
"flipVertical": "Tuttya tubdidt",
|
||||
"viewMode": "Askar n tmuɣli",
|
||||
"toggleExportColorScheme": "Sermed/sens asifeḍ usentel n yini",
|
||||
"share": "Bḍu"
|
||||
@@ -230,8 +228,7 @@
|
||||
"zoomToSelection": "Simɣur ɣer tefrayt"
|
||||
},
|
||||
"encrypted": {
|
||||
"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"
|
||||
"tooltip": "Unuɣen-inek (m) ttuwgelhnen seg yixef s ixef dɣa iqeddacen n Excalidraw werǧin ad ten-walin. "
|
||||
},
|
||||
"stats": {
|
||||
"angle": "Tiɣmeṛt",
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
"architect": "건축가",
|
||||
"artist": "예술가",
|
||||
"cartoonist": "만화가",
|
||||
"fileTitle": "",
|
||||
"fileTitle": "파일명",
|
||||
"colorPicker": "색상 선택기",
|
||||
"canvasBackground": "캔버스 배경",
|
||||
"drawingCanvas": "캔버스 그리기",
|
||||
@@ -92,8 +92,6 @@
|
||||
"centerHorizontally": "수평으로 중앙 정렬",
|
||||
"distributeHorizontally": "수평으로 분배",
|
||||
"distributeVertically": "수직으로 분배",
|
||||
"flipHorizontal": "",
|
||||
"flipVertical": "",
|
||||
"viewMode": "보기 모드",
|
||||
"toggleExportColorScheme": "",
|
||||
"share": ""
|
||||
@@ -230,8 +228,7 @@
|
||||
"zoomToSelection": "선택 영역으로 확대/축소"
|
||||
},
|
||||
"encrypted": {
|
||||
"tooltip": "그림은 종단 간 암호화되므로 Excalidraw의 서버는 절대로 내용을 알 수 없습니다.",
|
||||
"link": ""
|
||||
"tooltip": "그림은 종단 간 암호화되므로 Excalidraw의 서버는 절대로 내용을 알 수 없습니다."
|
||||
},
|
||||
"stats": {
|
||||
"angle": "각도",
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
"architect": "ဗိသုကာ",
|
||||
"artist": "ပန်းချီ",
|
||||
"cartoonist": "ကာတွန်း",
|
||||
"fileTitle": "",
|
||||
"fileTitle": "ခေါင်းစဉ်",
|
||||
"colorPicker": "အရောင်ရွေး",
|
||||
"canvasBackground": "ကားချပ်နောက်ခံ",
|
||||
"drawingCanvas": "ပုံဆွဲကားချပ်",
|
||||
@@ -92,8 +92,6 @@
|
||||
"centerHorizontally": "အလျားလိုက်အလယ်ညှိ",
|
||||
"distributeHorizontally": "အလျားလိုက်",
|
||||
"distributeVertically": "ထောင်လိုက်",
|
||||
"flipHorizontal": "",
|
||||
"flipVertical": "",
|
||||
"viewMode": "",
|
||||
"toggleExportColorScheme": "",
|
||||
"share": ""
|
||||
@@ -230,8 +228,7 @@
|
||||
"zoomToSelection": ""
|
||||
},
|
||||
"encrypted": {
|
||||
"tooltip": "ရေးဆွဲထားသောပုံများအား နှစ်ဘက်စွန်းတိုင်လျှို့ဝှက်ထားသဖြင့် Excalidraw ၏ဆာဗာများပင်လျှင်မြင်တွေ့ရမည်မဟုတ်ပါ။",
|
||||
"link": ""
|
||||
"tooltip": "ရေးဆွဲထားသောပုံများအား နှစ်ဘက်စွန်းတိုင်လျှို့ဝှက်ထားသဖြင့် Excalidraw ၏ဆာဗာများပင်လျှင်မြင်တွေ့ရမည်မဟုတ်ပါ။"
|
||||
},
|
||||
"stats": {
|
||||
"angle": "ထောင့်",
|
||||
|
||||
@@ -92,8 +92,6 @@
|
||||
"centerHorizontally": "Midtstill horisontalt",
|
||||
"distributeHorizontally": "Distribuer horisontalt",
|
||||
"distributeVertically": "Distribuer vertikalt",
|
||||
"flipHorizontal": "Snu horisontalt",
|
||||
"flipVertical": "Snu vertikalt",
|
||||
"viewMode": "Visningsmodus",
|
||||
"toggleExportColorScheme": "Veksle eksport av fargepalett",
|
||||
"share": "Del"
|
||||
@@ -230,8 +228,7 @@
|
||||
"zoomToSelection": "Zoom til utvalg"
|
||||
},
|
||||
"encrypted": {
|
||||
"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"
|
||||
"tooltip": "Dine tegninger er ende-til-ende-krypterte slik at Excalidraw sine servere aldri vil se dem."
|
||||
},
|
||||
"stats": {
|
||||
"angle": "Vinkel",
|
||||
|
||||
@@ -92,8 +92,6 @@
|
||||
"centerHorizontally": "Horizontaal Centreren",
|
||||
"distributeHorizontally": "Horizontaal verspreiden",
|
||||
"distributeVertically": "Verticaal distribueren",
|
||||
"flipHorizontal": "Horizontaal spiegelen",
|
||||
"flipVertical": "Verticaal spiegelen",
|
||||
"viewMode": "Weergavemodus",
|
||||
"toggleExportColorScheme": "Kleurenschema exporteren aan/uit",
|
||||
"share": "Deel"
|
||||
@@ -140,7 +138,7 @@
|
||||
"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.",
|
||||
"loadSceneOverridePrompt": "Het laden van externe tekening zal uw bestaande inhoud vervangen. Wil je doorgaan?",
|
||||
"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)",
|
||||
"collabStopOverridePrompt": "",
|
||||
"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?",
|
||||
"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?",
|
||||
@@ -230,8 +228,7 @@
|
||||
"zoomToSelection": "Inzoomen op selectie"
|
||||
},
|
||||
"encrypted": {
|
||||
"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"
|
||||
"tooltip": "Je tekeningen zijn beveiligd met end-to-end encryptie, dus Excalidraw's servers zullen nooit zien wat je tekent."
|
||||
},
|
||||
"stats": {
|
||||
"angle": "Hoek",
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
"architect": "Arkitekt",
|
||||
"artist": "Kunstnar",
|
||||
"cartoonist": "Teiknar",
|
||||
"fileTitle": "",
|
||||
"fileTitle": "Filnamn",
|
||||
"colorPicker": "Fargeveljar",
|
||||
"canvasBackground": "Lerretsbakgrunn",
|
||||
"drawingCanvas": "Lerret",
|
||||
@@ -92,8 +92,6 @@
|
||||
"centerHorizontally": "Midtstill horisontalt",
|
||||
"distributeHorizontally": "Sprei horisontalt",
|
||||
"distributeVertically": "Sprei vertikalt",
|
||||
"flipHorizontal": "",
|
||||
"flipVertical": "",
|
||||
"viewMode": "",
|
||||
"toggleExportColorScheme": "",
|
||||
"share": ""
|
||||
@@ -230,8 +228,7 @@
|
||||
"zoomToSelection": ""
|
||||
},
|
||||
"encrypted": {
|
||||
"tooltip": "Teikningane dine er ende-til-ende-krypterte slik at Excalidraw sine serverar aldri får sjå dei.",
|
||||
"link": ""
|
||||
"tooltip": "Teikningane dine er ende-til-ende-krypterte slik at Excalidraw sine serverar aldri får sjå dei."
|
||||
},
|
||||
"stats": {
|
||||
"angle": "Vinkel",
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
"fontSize": "Talha poliça",
|
||||
"fontFamily": "Familha de poliça",
|
||||
"onlySelected": "Seleccion sonque",
|
||||
"withBackground": "Inclure lo rèireplan",
|
||||
"withBackground": "Inclure rèireplan",
|
||||
"exportEmbedScene": "Integrar la scèna al fichièr d’expo",
|
||||
"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 d’aqueste fichièr.\nAumentarà la talha del fichièr exportat.",
|
||||
"addWatermark": "Apondre « Fabricat amb Excalidraw »",
|
||||
@@ -61,7 +61,7 @@
|
||||
"architect": "Arquitècte",
|
||||
"artist": "Artista",
|
||||
"cartoonist": "Dessenhaire",
|
||||
"fileTitle": "Nom del fichièr",
|
||||
"fileTitle": "Títol del fichièr",
|
||||
"colorPicker": "Selector de color",
|
||||
"canvasBackground": "Rèireplan del canabàs",
|
||||
"drawingCanvas": "Zòna de dessenh",
|
||||
@@ -92,8 +92,6 @@
|
||||
"centerHorizontally": "Centrar orizontalament",
|
||||
"distributeHorizontally": "Distribuir orizontalament",
|
||||
"distributeVertically": "Distribuir verticalament",
|
||||
"flipHorizontal": "Virar orizontalament",
|
||||
"flipVertical": "Virar verticalament",
|
||||
"viewMode": "Mòde de vista",
|
||||
"toggleExportColorScheme": "Alternar l’esquèma de color d’expòrt",
|
||||
"share": "Partejar"
|
||||
@@ -101,8 +99,8 @@
|
||||
"buttons": {
|
||||
"clearReset": "Reïnicializar lo canabàs",
|
||||
"export": "Exportar",
|
||||
"exportToPng": "Exportar en PNG",
|
||||
"exportToSvg": "Exportar en SVG",
|
||||
"exportToPng": "Export en PNG",
|
||||
"exportToSvg": "Export en SVG",
|
||||
"copyToClipboard": "Copiar al quichapapièrs",
|
||||
"copyPngToClipboard": "Copiar PNG al quichapapièrs",
|
||||
"scale": "Escala",
|
||||
@@ -230,8 +228,7 @@
|
||||
"zoomToSelection": "Zoomar la seleccion"
|
||||
},
|
||||
"encrypted": {
|
||||
"tooltip": "Vòstres dessenhs son chifrats del cap a la fin en consequéncia los servidors d’Excalidraw los veiràn pas jamai.",
|
||||
"link": "Article de blòg sul chiframent del cap a la fin dins Excalidraw"
|
||||
"tooltip": "Vòstres dessenhs son chifrats del cap a la fin en consequéncia los servidors d’Excalidraw los veiràn pas jamai."
|
||||
},
|
||||
"stats": {
|
||||
"angle": "Angle",
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
"architect": "ਭਵਨ ਨਿਰਮਾਣਕਾਰੀ",
|
||||
"artist": "ਕਲਾਕਾਰ",
|
||||
"cartoonist": "ਕਾਰਟੂਨਿਸਟ",
|
||||
"fileTitle": "",
|
||||
"fileTitle": "ਫਾਈਲ ਦਾ ਸਿਰਨਾਵਾਂ",
|
||||
"colorPicker": "ਰੰਗ ਚੋਣਕਾਰ",
|
||||
"canvasBackground": "ਕੈਨਵਸ ਦਾ ਬੈਕਗਰਾਉਂਡ",
|
||||
"drawingCanvas": "ਡਰਾਇੰਗ ਕੈਨਵਸ",
|
||||
@@ -92,8 +92,6 @@
|
||||
"centerHorizontally": "ਖੜ੍ਹਵੇਂ ਵਿਚਕਾਰ ਕਰੋ",
|
||||
"distributeHorizontally": "ਖੜ੍ਹਵੇਂ ਇਕਸਾਰ ਵੰਡੋ",
|
||||
"distributeVertically": "ਲੇਟਵੇਂ ਇਕਸਾਰ ਵੰਡੋ",
|
||||
"flipHorizontal": "",
|
||||
"flipVertical": "",
|
||||
"viewMode": "ਦੇਖੋ ਮੋਡ",
|
||||
"toggleExportColorScheme": "",
|
||||
"share": ""
|
||||
@@ -230,8 +228,7 @@
|
||||
"zoomToSelection": "ਚੋਣ ਤੱਕ ਜ਼ੂਮ ਕਰੋ"
|
||||
},
|
||||
"encrypted": {
|
||||
"tooltip": "ਤੁਹਾਡੀ ਡਰਾਇੰਗਾਂ ਸਿਰੇ-ਤੋਂ-ਸਿਰੇ ਤੱਕ ਇਨਕਰਿਪਟ ਕੀਤੀਆਂ ਹੋਈਆਂ ਹਨ, ਇਸ ਲਈ Excalidraw ਦੇ ਸਰਵਰ ਉਹਨਾਂ ਨੂੰ ਕਦੇ ਵੀ ਨਹੀਂ ਦੇਖਣਗੇ।",
|
||||
"link": ""
|
||||
"tooltip": "ਤੁਹਾਡੀ ਡਰਾਇੰਗਾਂ ਸਿਰੇ-ਤੋਂ-ਸਿਰੇ ਤੱਕ ਇਨਕਰਿਪਟ ਕੀਤੀਆਂ ਹੋਈਆਂ ਹਨ, ਇਸ ਲਈ Excalidraw ਦੇ ਸਰਵਰ ਉਹਨਾਂ ਨੂੰ ਕਦੇ ਵੀ ਨਹੀਂ ਦੇਖਣਗੇ।"
|
||||
},
|
||||
"stats": {
|
||||
"angle": "ਕੋਣ",
|
||||
|
||||
@@ -1,37 +1,37 @@
|
||||
{
|
||||
"ar-SA": 85,
|
||||
"bg-BG": 93,
|
||||
"ca-ES": 98,
|
||||
"ar-SA": 83,
|
||||
"bg-BG": 94,
|
||||
"ca-ES": 100,
|
||||
"de-DE": 100,
|
||||
"el-GR": 98,
|
||||
"en": 100,
|
||||
"es-ES": 99,
|
||||
"fa-IR": 88,
|
||||
"fi-FI": 99,
|
||||
"es-ES": 100,
|
||||
"fa-IR": 90,
|
||||
"fi-FI": 100,
|
||||
"fr-FR": 100,
|
||||
"he-IL": 89,
|
||||
"hi-IN": 91,
|
||||
"hu-HU": 81,
|
||||
"id-ID": 100,
|
||||
"he-IL": 91,
|
||||
"hi-IN": 93,
|
||||
"hu-HU": 83,
|
||||
"id-ID": 99,
|
||||
"it-IT": 100,
|
||||
"ja-JP": 97,
|
||||
"ja-JP": 96,
|
||||
"kab-KAB": 99,
|
||||
"ko-KR": 92,
|
||||
"my-MM": 76,
|
||||
"ko-KR": 94,
|
||||
"my-MM": 77,
|
||||
"nb-NO": 100,
|
||||
"nl-NL": 100,
|
||||
"nn-NO": 83,
|
||||
"nl-NL": 99,
|
||||
"nn-NO": 85,
|
||||
"oc-FR": 100,
|
||||
"pa-IN": 94,
|
||||
"pl-PL": 95,
|
||||
"pa-IN": 95,
|
||||
"pl-PL": 96,
|
||||
"pt-BR": 100,
|
||||
"pt-PT": 95,
|
||||
"pt-PT": 97,
|
||||
"ro-RO": 100,
|
||||
"ru-RU": 98,
|
||||
"sk-SK": 99,
|
||||
"ru-RU": 100,
|
||||
"sk-SK": 100,
|
||||
"sv-SE": 100,
|
||||
"tr-TR": 99,
|
||||
"uk-UA": 99,
|
||||
"zh-CN": 99,
|
||||
"zh-TW": 99
|
||||
"tr-TR": 83,
|
||||
"uk-UA": 95,
|
||||
"zh-CN": 100,
|
||||
"zh-TW": 100
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
"architect": "Dokładny",
|
||||
"artist": "Artystyczny",
|
||||
"cartoonist": "Rysunkowy",
|
||||
"fileTitle": "",
|
||||
"fileTitle": "Tytuł pliku",
|
||||
"colorPicker": "Paleta kolorów",
|
||||
"canvasBackground": "Kolor dokumentu",
|
||||
"drawingCanvas": "Obszar roboczy",
|
||||
@@ -92,8 +92,6 @@
|
||||
"centerHorizontally": "Wyśrodkuj w poziomie",
|
||||
"distributeHorizontally": "Rozłóż poziomo",
|
||||
"distributeVertically": "Rozłóż pionowo",
|
||||
"flipHorizontal": "",
|
||||
"flipVertical": "",
|
||||
"viewMode": "Tryb widoku",
|
||||
"toggleExportColorScheme": "",
|
||||
"share": ""
|
||||
@@ -230,8 +228,7 @@
|
||||
"zoomToSelection": "Przybliż do zaznaczenia"
|
||||
},
|
||||
"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.",
|
||||
"link": ""
|
||||
"tooltip": "Twoje rysunki są zabezpieczone szyfrowaniem end-to-end, tak więc nawet w Excalidraw nie jesteśmy w stanie zobaczyć tego co tworzysz."
|
||||
},
|
||||
"stats": {
|
||||
"angle": "Kąt",
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
"architect": "Arquiteto",
|
||||
"artist": "Artista",
|
||||
"cartoonist": "Cartunista",
|
||||
"fileTitle": "Nome do arquivo",
|
||||
"fileTitle": "Título do arquivo",
|
||||
"colorPicker": "Seletor de cores",
|
||||
"canvasBackground": "Fundo da tela",
|
||||
"drawingCanvas": "Tela de desenho",
|
||||
@@ -92,8 +92,6 @@
|
||||
"centerHorizontally": "Centralizar horizontalmente",
|
||||
"distributeHorizontally": "Distribuir horizontalmente",
|
||||
"distributeVertically": "Distribuir verticalmente",
|
||||
"flipHorizontal": "Inverter horizontalmente",
|
||||
"flipVertical": "Inverter verticalmente",
|
||||
"viewMode": "Modo de visualização",
|
||||
"toggleExportColorScheme": "Alternar esquema de cores de exportação",
|
||||
"share": "Compartilhar"
|
||||
@@ -230,8 +228,7 @@
|
||||
"zoomToSelection": "Ampliar a seleção"
|
||||
},
|
||||
"encrypted": {
|
||||
"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"
|
||||
"tooltip": "Seus desenhos são criptografados de ponta a ponta, então os servidores do Excalidraw nunca os verão."
|
||||
},
|
||||
"stats": {
|
||||
"angle": "Ângulo",
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
"architect": "Arquitecto",
|
||||
"artist": "Artista",
|
||||
"cartoonist": "Caricaturista",
|
||||
"fileTitle": "",
|
||||
"fileTitle": "Título do ficheiro",
|
||||
"colorPicker": "Seletor de cores",
|
||||
"canvasBackground": "Fundo da tela",
|
||||
"drawingCanvas": "Tela de desenho",
|
||||
@@ -92,8 +92,6 @@
|
||||
"centerHorizontally": "Centralizar horizontalmente",
|
||||
"distributeHorizontally": "Distribuir horizontalmente",
|
||||
"distributeVertically": "Distribuir verticalmente",
|
||||
"flipHorizontal": "",
|
||||
"flipVertical": "",
|
||||
"viewMode": "Modo de visualização",
|
||||
"toggleExportColorScheme": "Alternar esquema de cores de exportação",
|
||||
"share": ""
|
||||
@@ -230,8 +228,7 @@
|
||||
"zoomToSelection": "Ampliar a seleção"
|
||||
},
|
||||
"encrypted": {
|
||||
"tooltip": "Seus desenhos são criptografados de ponta a ponta, então os servidores do Excalidraw nunca os verão.",
|
||||
"link": ""
|
||||
"tooltip": "Seus desenhos são criptografados de ponta a ponta, então os servidores do Excalidraw nunca os verão."
|
||||
},
|
||||
"stats": {
|
||||
"angle": "Ângulo",
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
"architect": "Arhitect",
|
||||
"artist": "Artist",
|
||||
"cartoonist": "Caricaturist",
|
||||
"fileTitle": "Nume de fișier",
|
||||
"fileTitle": "Denumirea fișierului",
|
||||
"colorPicker": "Selector de culoare",
|
||||
"canvasBackground": "Fundalul pânzei",
|
||||
"drawingCanvas": "Pânză pentru desenat",
|
||||
@@ -92,8 +92,6 @@
|
||||
"centerHorizontally": "Centrare orizontală",
|
||||
"distributeHorizontally": "Distribuie orizontal",
|
||||
"distributeVertically": "Distribuie vertical",
|
||||
"flipHorizontal": "Răsturnare orizontală",
|
||||
"flipVertical": "Răsturnare verticală",
|
||||
"viewMode": "Mod de vizualizare",
|
||||
"toggleExportColorScheme": "Comutare schemă de culori de export",
|
||||
"share": "Distribuie"
|
||||
@@ -230,8 +228,7 @@
|
||||
"zoomToSelection": "Panoramare la selecție"
|
||||
},
|
||||
"encrypted": {
|
||||
"tooltip": "Desenele tale sunt criptate integral, astfel că serverele Excalidraw nu le vor vedea niciodată.",
|
||||
"link": "Articol de blog pe criptarea integrală din Excalidraw"
|
||||
"tooltip": "Desenele tale sunt criptate integral, astfel că serverele Excalidraw nu le vor vedea niciodată."
|
||||
},
|
||||
"stats": {
|
||||
"angle": "Unghi",
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
"architect": "Архитектор",
|
||||
"artist": "Художник",
|
||||
"cartoonist": "Карикатурист",
|
||||
"fileTitle": "Имя файла",
|
||||
"fileTitle": "Название файла",
|
||||
"colorPicker": "Выбор цвета",
|
||||
"canvasBackground": "Фон холста",
|
||||
"drawingCanvas": "Полотно",
|
||||
@@ -92,8 +92,6 @@
|
||||
"centerHorizontally": "Центрировать по горизонтали",
|
||||
"distributeHorizontally": "Распределить по горизонтали",
|
||||
"distributeVertically": "Распределить по вертикали",
|
||||
"flipHorizontal": "",
|
||||
"flipVertical": "",
|
||||
"viewMode": "Вид",
|
||||
"toggleExportColorScheme": "Экспортировать цветовую схему",
|
||||
"share": "Поделиться"
|
||||
@@ -230,8 +228,7 @@
|
||||
"zoomToSelection": "Увеличить до выделенного"
|
||||
},
|
||||
"encrypted": {
|
||||
"tooltip": "Ваши данные защищены сквозным (End-to-end) шифрованием. Серверы Excalidraw никогда не получат доступ к ним.",
|
||||
"link": ""
|
||||
"tooltip": "Ваши данные защищены сквозным (End-to-end) шифрованием. Серверы Excalidraw никогда не получат доступ к ним."
|
||||
},
|
||||
"stats": {
|
||||
"angle": "Угол",
|
||||
|
||||
@@ -92,8 +92,6 @@
|
||||
"centerHorizontally": "Zarovnať vodorovne na stred",
|
||||
"distributeHorizontally": "Rozmiestniť vodorovne",
|
||||
"distributeVertically": "Rozmiestniť zvisle",
|
||||
"flipHorizontal": "Prevrátiť vodorovne",
|
||||
"flipVertical": "Prevrátiť zvislo",
|
||||
"viewMode": "Režim zobrazenia",
|
||||
"toggleExportColorScheme": "Prepnúť exportovanie farebnej schémy",
|
||||
"share": "Zdieľať"
|
||||
@@ -230,8 +228,7 @@
|
||||
"zoomToSelection": "Priblížiť na výber"
|
||||
},
|
||||
"encrypted": {
|
||||
"tooltip": "Vaše kresby používajú end-to-end šifrovanie, takže ich Excalidraw server nedokáže prečítať.",
|
||||
"link": ""
|
||||
"tooltip": "Vaše kresby používajú end-to-end šifrovanie, takže ich Excalidraw server nedokáže prečítať."
|
||||
},
|
||||
"stats": {
|
||||
"angle": "Uhol",
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
"architect": "Arkitekt",
|
||||
"artist": "Artist",
|
||||
"cartoonist": "Serietecknare",
|
||||
"fileTitle": "Filnamn",
|
||||
"fileTitle": "Filtitel",
|
||||
"colorPicker": "Färgväljare",
|
||||
"canvasBackground": "Canvas-bakgrund",
|
||||
"drawingCanvas": "Ritar canvas",
|
||||
@@ -92,8 +92,6 @@
|
||||
"centerHorizontally": "Centrera horisontellt",
|
||||
"distributeHorizontally": "Fördela horisontellt",
|
||||
"distributeVertically": "Fördela vertikalt",
|
||||
"flipHorizontal": "Vänd horisontellt",
|
||||
"flipVertical": "Vänd vertikalt",
|
||||
"viewMode": "Visningsläge",
|
||||
"toggleExportColorScheme": "Växla färgschema för export",
|
||||
"share": "Dela"
|
||||
@@ -230,8 +228,7 @@
|
||||
"zoomToSelection": "Zooma till markering"
|
||||
},
|
||||
"encrypted": {
|
||||
"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"
|
||||
"tooltip": "Dina skisser är krypterade från ände till ände så Excalidraws servrar kommer aldrig att se dem."
|
||||
},
|
||||
"stats": {
|
||||
"angle": "Vinkel",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"labels": {
|
||||
"paste": "Yapıştır",
|
||||
"pasteCharts": "Grafikleri yapıştır",
|
||||
"pasteCharts": "Dairesel grafik",
|
||||
"selectAll": "Tümünü seç",
|
||||
"multiSelect": "Seçime öge ekle",
|
||||
"moveCanvas": "Tuvali taşı",
|
||||
@@ -68,7 +68,7 @@
|
||||
"layers": "Katmanlar",
|
||||
"actions": "Eylemler",
|
||||
"language": "Dil",
|
||||
"liveCollaboration": "Canlı ortak çalışma alanı",
|
||||
"liveCollaboration": "",
|
||||
"duplicateSelection": "Çoğalt",
|
||||
"untitled": "Adsız",
|
||||
"name": "İsim",
|
||||
@@ -77,7 +77,7 @@
|
||||
"group": "Seçimi grup yap",
|
||||
"ungroup": "Seçilen grubu dağıt",
|
||||
"collaborators": "Ortaklar",
|
||||
"showGrid": "Izgarayı göster",
|
||||
"showGrid": "",
|
||||
"addToLibrary": "Kütüphaneye ekle",
|
||||
"removeFromLibrary": "Kütüphaneden kaldır",
|
||||
"libraryLoadingMessage": "Kütüphane yükleniyor…",
|
||||
@@ -92,11 +92,9 @@
|
||||
"centerHorizontally": "Yatayda ortala",
|
||||
"distributeHorizontally": "Yatay dağıt",
|
||||
"distributeVertically": "Dikey dağıt",
|
||||
"flipHorizontal": "Yatay döndür",
|
||||
"flipVertical": "Dikey döndür",
|
||||
"viewMode": "Görünüm modu",
|
||||
"toggleExportColorScheme": "Renk şemasını dışa aktar/aktarma",
|
||||
"share": "Paylaş"
|
||||
"viewMode": "",
|
||||
"toggleExportColorScheme": "",
|
||||
"share": ""
|
||||
},
|
||||
"buttons": {
|
||||
"clearReset": "Tuvali sıfırla",
|
||||
@@ -121,7 +119,7 @@
|
||||
"edit": "Düzenle",
|
||||
"undo": "Geri Al",
|
||||
"redo": "Yeniden yap",
|
||||
"resetLibrary": "Kütüphaneyi sıfırla",
|
||||
"resetLibrary": "",
|
||||
"createNewRoom": "Yeni oda oluştur",
|
||||
"fullScreen": "Tam ekran",
|
||||
"darkMode": "Koyu tema",
|
||||
@@ -140,13 +138,13 @@
|
||||
"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.",
|
||||
"loadSceneOverridePrompt": "Harici çizimler yüklemek mevcut olan içeriği değiştirecektir. Devam etmek istiyor musunuz?",
|
||||
"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.)",
|
||||
"collabStopOverridePrompt": "",
|
||||
"errorLoadingLibrary": "Üçüncü taraf kitaplığı yüklerken bir hata oluştu.",
|
||||
"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?",
|
||||
"cannotRestoreFromImage": "Sahne bu dosyadan oluşturulamıyor",
|
||||
"invalidSceneUrl": "Verilen URL'den çalışma alanı yüklenemedi. Dosya bozuk olabilir veya geçerli bir Excalidraw JSON verisi bulundurmuyor olabilir.",
|
||||
"resetLibrary": "Bu işlem kütüphanenizi sıfırlayacak. Emin misiniz?"
|
||||
"invalidSceneUrl": "",
|
||||
"resetLibrary": ""
|
||||
},
|
||||
"toolBar": {
|
||||
"selection": "Seçme",
|
||||
@@ -203,35 +201,34 @@
|
||||
"desc_inProgressIntro": "Ortak çalışma ortamı oluşturuldu.",
|
||||
"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.",
|
||||
"shareTitle": "Excalidraw'da canlı ortak calışma oturumuna katıl"
|
||||
"shareTitle": ""
|
||||
},
|
||||
"errorDialog": {
|
||||
"title": "Hata"
|
||||
},
|
||||
"helpDialog": {
|
||||
"blog": "Blog'umuzu okuyun",
|
||||
"click": "tıkla",
|
||||
"curvedArrow": "Eğri ok",
|
||||
"curvedLine": "Eğri çizgi",
|
||||
"documentation": "Dokümantasyon",
|
||||
"drag": "sürükle",
|
||||
"editor": "Düzenleyici",
|
||||
"github": "Bir hata mı buldun? Bildir",
|
||||
"howto": "Rehberlerimizi takip edin",
|
||||
"or": "veya",
|
||||
"preventBinding": "Ok bağlamayı önleyin",
|
||||
"shapes": "Şekiller",
|
||||
"shortcuts": "Klavye kısayolları",
|
||||
"textFinish": "(Metin) düzenlemeyi bitir",
|
||||
"textNewLine": "Yeni satır ekle (metin)",
|
||||
"title": "Yardım",
|
||||
"view": "Görünüm",
|
||||
"zoomToFit": "Tüm öğeleri sığdırmak için yakınlaştır",
|
||||
"zoomToSelection": "Seçime yakınlaş"
|
||||
"blog": "",
|
||||
"click": "",
|
||||
"curvedArrow": "",
|
||||
"curvedLine": "",
|
||||
"documentation": "",
|
||||
"drag": "",
|
||||
"editor": "",
|
||||
"github": "",
|
||||
"howto": "",
|
||||
"or": "",
|
||||
"preventBinding": "",
|
||||
"shapes": "",
|
||||
"shortcuts": "",
|
||||
"textFinish": "",
|
||||
"textNewLine": "",
|
||||
"title": "",
|
||||
"view": "",
|
||||
"zoomToFit": "",
|
||||
"zoomToSelection": ""
|
||||
},
|
||||
"encrypted": {
|
||||
"tooltip": "Çizimleriniz uçtan-uca şifrelenmiştir, Excalidraw'ın sunucuları bile onları göremez.",
|
||||
"link": ""
|
||||
"tooltip": "Çizimleriniz uçtan-uca şifrelenmiştir, Excalidraw'ın sunucuları bile onları göremez."
|
||||
},
|
||||
"stats": {
|
||||
"angle": "Açı",
|
||||
@@ -243,18 +240,18 @@
|
||||
"storage": "Depolama",
|
||||
"title": "İnekler için istatistikler",
|
||||
"total": "Toplam",
|
||||
"version": "Sürüm",
|
||||
"versionCopy": "Kopyalamak için tıkla",
|
||||
"versionNotAvailable": "Sürüm mevcut değil",
|
||||
"version": "",
|
||||
"versionCopy": "",
|
||||
"versionNotAvailable": "",
|
||||
"width": "Genişlik"
|
||||
},
|
||||
"toast": {
|
||||
"copyStyles": "Stiller kopyalandı.",
|
||||
"copyToClipboard": "Panoya kopyalandı.",
|
||||
"copyToClipboardAsPng": "{{exportSelection}} panoya PNG olarak\n({{exportColorScheme}}) kopyalandı",
|
||||
"fileSaved": "Dosya kaydedildi.",
|
||||
"fileSavedToFilename": "{filename} kaydedildi",
|
||||
"canvas": "tuval",
|
||||
"selection": "seçim"
|
||||
"copyStyles": "",
|
||||
"copyToClipboard": "",
|
||||
"copyToClipboardAsPng": "",
|
||||
"fileSaved": "",
|
||||
"fileSavedToFilename": "",
|
||||
"canvas": "",
|
||||
"selection": ""
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
"layers": "Шари",
|
||||
"actions": "Дії",
|
||||
"language": "Мова",
|
||||
"liveCollaboration": "Спільна співпраця",
|
||||
"liveCollaboration": "",
|
||||
"duplicateSelection": "Дублювати",
|
||||
"untitled": "Без назви",
|
||||
"name": "Ім’я",
|
||||
@@ -92,11 +92,9 @@
|
||||
"centerHorizontally": "Центрувати по горизонталі",
|
||||
"distributeHorizontally": "Розподілити по горизонталі",
|
||||
"distributeVertically": "Розподілити вертикально",
|
||||
"flipHorizontal": "Віддзеркалити горизонтально",
|
||||
"flipVertical": "Віддзеркалити вертикально",
|
||||
"viewMode": "Режим перегляду",
|
||||
"toggleExportColorScheme": "Переключити колірну схему експорту",
|
||||
"share": "Поділитися"
|
||||
"toggleExportColorScheme": "",
|
||||
"share": ""
|
||||
},
|
||||
"buttons": {
|
||||
"clearReset": "Очистити полотно",
|
||||
@@ -121,7 +119,7 @@
|
||||
"edit": "Редагувати",
|
||||
"undo": "Відмінити",
|
||||
"redo": "Повторити",
|
||||
"resetLibrary": "Очистити бібліотеку",
|
||||
"resetLibrary": "",
|
||||
"createNewRoom": "Створити нову кімнату",
|
||||
"fullScreen": "Повноекранний режим",
|
||||
"darkMode": "Темний режим",
|
||||
@@ -145,8 +143,8 @@
|
||||
"confirmAddLibrary": "Це призведе до додавання {{numShapes}} фігур до вашої бібліотеки. Ви впевнені?",
|
||||
"imageDoesNotContainScene": "Імпортування зображень на даний момент не підтримується.\n\nЧи хочете ви імпортувати сцену? Це зображення не містить ніяких даних сцен. Ви увімкнули це під час експорту?",
|
||||
"cannotRestoreFromImage": "Сцена не може бути відновлена з цього файлу зображення",
|
||||
"invalidSceneUrl": "Не вдалося імпортувати сцену з наданого URL. Він або недоформований, або не містить дійсних даних Excalidraw JSON.",
|
||||
"resetLibrary": "Це призведе до очищення бібліотеки. Ви впевнені?"
|
||||
"invalidSceneUrl": "",
|
||||
"resetLibrary": ""
|
||||
},
|
||||
"toolBar": {
|
||||
"selection": "Виділення",
|
||||
@@ -203,7 +201,7 @@
|
||||
"desc_inProgressIntro": "Сесія спільної роботи над кресленням триває.",
|
||||
"desc_shareLink": "Поділіться цим посиланням з будь-ким для спільної роботи:",
|
||||
"desc_exitSession": "Зупинка сесії відключить вас від кімнати, але ви зможете продовжити роботу з полотном локально. Зверніть увагу, що це не вплине на інших людей, і вони все одно зможуть працювати над їх версією.",
|
||||
"shareTitle": "Приєднатися до сеансу спільної роботи на Excalidraw"
|
||||
"shareTitle": ""
|
||||
},
|
||||
"errorDialog": {
|
||||
"title": "Помилка"
|
||||
@@ -230,8 +228,7 @@
|
||||
"zoomToSelection": "Наблизити вибране"
|
||||
},
|
||||
"encrypted": {
|
||||
"tooltip": "Ваші креслення захищені наскрізним шифруванням — сервери Excalidraw ніколи їх не побачать.",
|
||||
"link": ""
|
||||
"tooltip": "Ваші креслення захищені наскрізним шифруванням — сервери Excalidraw ніколи їх не побачать."
|
||||
},
|
||||
"stats": {
|
||||
"angle": "Кут",
|
||||
@@ -251,10 +248,10 @@
|
||||
"toast": {
|
||||
"copyStyles": "Скопійовані стилі.",
|
||||
"copyToClipboard": "Скопіювати до буферу обміну.",
|
||||
"copyToClipboardAsPng": "Скопійовано {{exportSelection}} до буфера обміну як PNG\n({{exportColorScheme}})",
|
||||
"copyToClipboardAsPng": "",
|
||||
"fileSaved": "Файл збережено.",
|
||||
"fileSavedToFilename": "Збережено в {filename}",
|
||||
"canvas": "полотно",
|
||||
"selection": "виділення"
|
||||
"canvas": "",
|
||||
"selection": ""
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
"strokeStyle_dashed": "虚线",
|
||||
"strokeStyle_dotted": "点虚线",
|
||||
"sloppiness": "边框",
|
||||
"opacity": "不透明度",
|
||||
"opacity": "透明度",
|
||||
"textAlign": "文本对齐",
|
||||
"edges": "边角",
|
||||
"sharp": "尖锐",
|
||||
@@ -38,7 +38,7 @@
|
||||
"fontSize": "字体大小",
|
||||
"fontFamily": "字体",
|
||||
"onlySelected": "仅被选中",
|
||||
"withBackground": "包括背景",
|
||||
"withBackground": "使用背景",
|
||||
"exportEmbedScene": "将画布数据嵌入到导出的文件",
|
||||
"exportEmbedScene_details": "画布数据将被保存到导出的 PNG/SVG 文件,以便恢复。\n将会增加导出的文件大小。",
|
||||
"addWatermark": "添加 “使用 Excalidraw 创建” 水印",
|
||||
@@ -61,7 +61,7 @@
|
||||
"architect": "朴素",
|
||||
"artist": "艺术",
|
||||
"cartoonist": "漫画家",
|
||||
"fileTitle": "文件名",
|
||||
"fileTitle": "文件标题",
|
||||
"colorPicker": "调色盘",
|
||||
"canvasBackground": "画布背景",
|
||||
"drawingCanvas": "绘制 Canvas",
|
||||
@@ -92,8 +92,6 @@
|
||||
"centerHorizontally": "水平居中",
|
||||
"distributeHorizontally": "水平等距分布",
|
||||
"distributeVertically": "垂直等距分布",
|
||||
"flipHorizontal": "水平翻转",
|
||||
"flipVertical": "垂直翻转",
|
||||
"viewMode": "查看模式",
|
||||
"toggleExportColorScheme": "切换导出配色方案",
|
||||
"share": "分享"
|
||||
@@ -130,7 +128,7 @@
|
||||
"exitZenMode": "退出禅模式"
|
||||
},
|
||||
"alerts": {
|
||||
"clearReset": "这将会清除整个画布。您是否要继续?",
|
||||
"clearReset": "这将会清除整个 画板。您是否要继续?",
|
||||
"couldNotCreateShareableLink": "无法创建共享链接",
|
||||
"couldNotCreateShareableLinkTooBig": "无法创建可共享链接:画布过大",
|
||||
"couldNotLoadInvalidFile": "无法加载无效的文件",
|
||||
@@ -230,8 +228,7 @@
|
||||
"zoomToSelection": "缩放到选区"
|
||||
},
|
||||
"encrypted": {
|
||||
"tooltip": "您的绘图采用的端到端加密,其内容对于Excalidraw服务器是不可见的。",
|
||||
"link": ""
|
||||
"tooltip": "您的绘图采用的端到端加密,其内容对于Excalidraw服务器是不可见的。"
|
||||
},
|
||||
"stats": {
|
||||
"angle": "角度",
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
"architect": "精確",
|
||||
"artist": "藝術",
|
||||
"cartoonist": "卡通",
|
||||
"fileTitle": "檔案名稱",
|
||||
"fileTitle": "檔案標題",
|
||||
"colorPicker": "色彩選擇工具",
|
||||
"canvasBackground": "Canvas 背景",
|
||||
"drawingCanvas": "繪圖 canvas",
|
||||
@@ -92,8 +92,6 @@
|
||||
"centerHorizontally": "水平置中",
|
||||
"distributeHorizontally": "水平分布",
|
||||
"distributeVertically": "垂直分布",
|
||||
"flipHorizontal": "水平翻轉",
|
||||
"flipVertical": "垂直翻轉",
|
||||
"viewMode": "檢視模式",
|
||||
"toggleExportColorScheme": "切換輸出配色",
|
||||
"share": "共享"
|
||||
@@ -230,8 +228,7 @@
|
||||
"zoomToSelection": "縮放至選取區"
|
||||
},
|
||||
"encrypted": {
|
||||
"tooltip": "你的作品已使用 end-to-end 方式加密,Excalidraw 的伺服器也無法取得其內容。",
|
||||
"link": ""
|
||||
"tooltip": "你的作品已使用 end-to-end 方式加密,Excalidraw 的伺服器也無法取得其內容。"
|
||||
},
|
||||
"stats": {
|
||||
"angle": "角度",
|
||||
|
||||
@@ -6,78 +6,19 @@ The change should be grouped under one of the below section and must contain PR
|
||||
- Features: For new features.
|
||||
- Fixes: For bug fixes.
|
||||
- Chore: Changes for non src files example package.json.
|
||||
- Improvements: For any improvements.
|
||||
- Refactor: For any refactoring.
|
||||
|
||||
Please add the latest change on the top under the correct section.
|
||||
-->
|
||||
|
||||
## 0.6.0 (2021-04-04)
|
||||
## 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). Check the [readme](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#uioptions) for more details.
|
||||
- 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/excalidraw/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). Check the [readme](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#importlibrary) for more details.
|
||||
- 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). Check the [readme](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#installation) for more details.
|
||||
#### 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`
|
||||
|
||||
### Refactor
|
||||
|
||||
#### BREAKING CHANGE
|
||||
|
||||
- Rename the API `setCanvasOffsets` exposed via [`ref`](https://github.com/excalidraw/excalidraw/blob/master/src/components/App.tsx#L265) to `refresh` [#3398](https://github.com/excalidraw/excalidraw/pull/3398).
|
||||
|
||||
## Excalidraw Library
|
||||
|
||||
### Features
|
||||
|
||||
- Reopen library menu on import from file [#3383](https://github.com/excalidraw/excalidraw/pull/3383).
|
||||
- Don't unnecessarily prompt when installing libraries [#3329](https://github.com/excalidraw/excalidraw/pull/3329).
|
||||
- Add option to flip single element on the context menu [#2520](https://github.com/excalidraw/excalidraw/pull/2520).
|
||||
- Replace fontSize and fontFamily text with icons [#2857](https://github.com/excalidraw/excalidraw/pull/2857).
|
||||
|
||||
### Fixes
|
||||
|
||||
- Export dialog canvas positioning [#3397](https://github.com/excalidraw/excalidraw/pull/3397).
|
||||
- Don't share collab types with core [#3353](https://github.com/excalidraw/excalidraw/pull/3353).
|
||||
- Support d&d of files without extension [#3168](https://github.com/excalidraw/excalidraw/pull/3168).
|
||||
- Positions stats for linear elements [#3331](https://github.com/excalidraw/excalidraw/pull/3331).
|
||||
- Debounce.flush invokes func even if never queued before [#3326](https://github.com/excalidraw/excalidraw/pull/3326).
|
||||
- State selection state on opening contextMenu [#3333](https://github.com/excalidraw/excalidraw/pull/3333).
|
||||
- Add unique key for library header to resolve dev warnings [#3316](https://github.com/excalidraw/excalidraw/pull/3316).
|
||||
- disallow create text in viewMode on mobile [#3219](https://github.com/excalidraw/excalidraw/pull/3219).
|
||||
- Make help toggle tabbable [#3310](https://github.com/excalidraw/excalidraw/pull/3310)
|
||||
- Show Windows share icon for Windows users [#3306](https://github.com/excalidraw/excalidraw/pull/3306).
|
||||
- Don't scroll to content on INIT websocket message [#3291](https://github.com/excalidraw/excalidraw/pull/3291).
|
||||
|
||||
### Refactor
|
||||
|
||||
- Use arrow function where possible [#3315](https://github.com/excalidraw/excalidraw/pull/3315).
|
||||
|
||||
---
|
||||
|
||||
## 0.5.0 (2021-03-21)
|
||||
|
||||
## Excalidraw API
|
||||
|
||||
### Features
|
||||
|
||||
- Set the target to `window.name` if present during excalidraw libraries installation so it opens in same tab for the host. If `window.name` is not set it will open in a new tab [#3299](https://github.com/excalidraw/excalidraw/pull/3299).
|
||||
- Add `name` prop to indicate 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 [#3273](https://github.com/excalidraw/excalidraw/pull/3273).
|
||||
- Add `name` prop which indicates 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 [#3273](https://github.com/excalidraw/excalidraw/pull/3273).
|
||||
- Export API `setCanvasOffsets` via `ref` to set the offsets for Excalidraw[#3265](https://github.com/excalidraw/excalidraw/pull/3265).
|
||||
#### BREAKING CHANGE
|
||||
- `offsetLeft` and `offsetTop` props have been removed now so you have to use the `setCanvasOffsets` via `ref` to achieve the same.
|
||||
@@ -95,24 +36,6 @@ Please add the latest change on the top under the correct section.
|
||||
- The class `Appearance_dark` is renamed to `theme--dark`.
|
||||
- The class `Appearance_dark-background-none` is renamed to `theme--dark-background-none`.
|
||||
|
||||
## Excalidraw Library
|
||||
|
||||
### Features
|
||||
|
||||
- Support pasting file contents & always prefer system clip [#3257](https://github.com/excalidraw/excalidraw/pull/3257)
|
||||
- Add label for name field and use input when editable in export dialog [#3286](https://github.com/excalidraw/excalidraw/pull/3286)
|
||||
- Implement the Web Share Target API [#3230](https://github.com/excalidraw/excalidraw/pull/3230).
|
||||
|
||||
### Fixes
|
||||
|
||||
- Don't show export and delete when library is empty [#3288](https://github.com/excalidraw/excalidraw/pull/3288)
|
||||
- Overflow in textinput in export dialog [#3284](https://github.com/excalidraw/excalidraw/pull/3284).
|
||||
- Bail on noop updates for newElementWith [#3279](https://github.com/excalidraw/excalidraw/pull/3279).
|
||||
- Prevent State continuously updated when holding ctrl/cmd #3283
|
||||
- Debounce flush not invoked if lastArgs not defined [#3281](https://github.com/excalidraw/excalidraw/pull/3281).
|
||||
- Stop preventing canvas pointerdown/tapend events [#3207](https://github.com/excalidraw/excalidraw/pull/3207).
|
||||
- Double scrollbar on modals [#3226](https://github.com/excalidraw/excalidraw/pull/3226).
|
||||
|
||||
---
|
||||
|
||||
## 0.4.3 (2021-03-12)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
### Excalidraw
|
||||
|
||||
Excalidraw exported as a component to directly embed in your projects.
|
||||
Excalidraw exported as a component to directly embed in your projects
|
||||
|
||||
### Installation
|
||||
|
||||
@@ -16,13 +16,13 @@ or via yarn
|
||||
yarn add react react-dom @excalidraw/excalidraw
|
||||
```
|
||||
|
||||
After installation you will see a folder `excalidraw-assets` and `excalidraw-assets-dev` in `dist` directory which contains the assets needed for this app in prod and dev mode respectively.
|
||||
After installation you will see a folder `excalidraw-assets` in `dist` directory which contains the assets needed for this app.
|
||||
|
||||
Move the folder `excalidraw-assets` and `excalidraw-assets-dev` to the path where your assets are served.
|
||||
Move the folder `excalidraw-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.
|
||||
If you want to load assets from a different path you can set a variable `window.EXCALIDRAW_ASSET_PATH` to the url from where you want to load the assets.
|
||||
|
||||
### Demo
|
||||
|
||||
@@ -30,11 +30,7 @@ If you want to load assets from a different path you can set a variable `window.
|
||||
|
||||
### 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>
|
||||
1. If you are using a Web bundler (for instance, Webpack), you can import it as an ES6 module as shown below
|
||||
|
||||
```js
|
||||
import React, { useEffect, useState, useRef } from "react";
|
||||
@@ -45,11 +41,33 @@ import "./styles.scss";
|
||||
|
||||
export default function App() {
|
||||
const excalidrawRef = useRef(null);
|
||||
const excalidrawWrapperRef = useRef(null);
|
||||
const [dimensions, setDimensions] = useState({
|
||||
width: undefined,
|
||||
height: undefined,
|
||||
});
|
||||
|
||||
const [viewModeEnabled, setViewModeEnabled] = useState(false);
|
||||
const [zenModeEnabled, setZenModeEnabled] = useState(false);
|
||||
const [gridModeEnabled, setGridModeEnabled] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setDimensions({
|
||||
width: excalidrawWrapperRef.current.getBoundingClientRect().width,
|
||||
height: excalidrawWrapperRef.current.getBoundingClientRect().height,
|
||||
});
|
||||
const onResize = () => {
|
||||
setDimensions({
|
||||
width: excalidrawWrapperRef.current.getBoundingClientRect().width,
|
||||
height: excalidrawWrapperRef.current.getBoundingClientRect().height,
|
||||
});
|
||||
};
|
||||
|
||||
window.addEventListener("resize", onResize);
|
||||
|
||||
return () => window.removeEventListener("resize", onResize);
|
||||
}, [excalidrawWrapperRef]);
|
||||
|
||||
const updateScene = () => {
|
||||
const sceneData = {
|
||||
elements: [
|
||||
@@ -122,11 +140,13 @@ export default function App() {
|
||||
Grid mode
|
||||
</label>
|
||||
</div>
|
||||
<div className="excalidraw-wrapper">
|
||||
<div className="excalidraw-wrapper" ref={excalidrawWrapperRef}>
|
||||
<Excalidraw
|
||||
ref={excalidrawRef}
|
||||
width={dimensions.width}
|
||||
height={dimensions.height}
|
||||
initialData={InitialData}
|
||||
onChange={(elements, state) => {
|
||||
onChange={(elements, state) =>
|
||||
console.log("Elements :", elements, "State : ", state)
|
||||
}
|
||||
onPointerUpdate={(payload) => console.log(payload)}
|
||||
@@ -143,52 +163,11 @@ export default function App() {
|
||||
}
|
||||
```
|
||||
|
||||
To view the full example visit :point_down:
|
||||
|
||||
[](https://codesandbox.io/s/excalidraw-ehlz3?fontsize=14&hidenavigation=1&theme=dark)
|
||||
|
||||
</details>
|
||||
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 />}</>;
|
||||
}
|
||||
```
|
||||
|
||||
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>
|
||||
You will need to make sure `react`, `react-dom` is available as shown below.
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
@@ -201,7 +180,7 @@ You will need to make sure `react`, `react-dom` is available as shown in the bel
|
||||
|
||||
<script
|
||||
type="text/javascript"
|
||||
src="https://unpkg.com/@excalidraw/excalidraw@0.6.0/dist/excalidraw.development.js"
|
||||
src="https://unpkg.com/@excalidraw/excalidraw@0.4.1/dist/excalidraw.min.js"
|
||||
></script>
|
||||
</head>
|
||||
|
||||
@@ -222,11 +201,33 @@ import InitialData from "./initialData";
|
||||
|
||||
const App = () => {
|
||||
const excalidrawRef = React.useRef(null);
|
||||
const excalidrawWrapperRef = React.useRef(null);
|
||||
const [dimensions, setDimensions] = React.useState({
|
||||
width: undefined,
|
||||
height: undefined,
|
||||
});
|
||||
|
||||
const [viewModeEnabled, setViewModeEnabled] = React.useState(false);
|
||||
const [zenModeEnabled, setZenModeEnabled] = React.useState(false);
|
||||
const [gridModeEnabled, setGridModeEnabled] = React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
setDimensions({
|
||||
width: excalidrawWrapperRef.current.getBoundingClientRect().width,
|
||||
height: excalidrawWrapperRef.current.getBoundingClientRect().height,
|
||||
});
|
||||
const onResize = () => {
|
||||
setDimensions({
|
||||
width: excalidrawWrapperRef.current.getBoundingClientRect().width,
|
||||
height: excalidrawWrapperRef.current.getBoundingClientRect().height,
|
||||
});
|
||||
};
|
||||
|
||||
window.addEventListener("resize", onResize);
|
||||
|
||||
return () => window.removeEventListener("resize", onResize);
|
||||
}, [excalidrawWrapperRef]);
|
||||
|
||||
const updateScene = () => {
|
||||
const sceneData = {
|
||||
elements: [
|
||||
@@ -319,6 +320,9 @@ const App = () => {
|
||||
ref: excalidrawWrapperRef,
|
||||
},
|
||||
React.createElement(Excalidraw.default, {
|
||||
ref: excalidrawRef,
|
||||
width: dimensions.width,
|
||||
height: dimensions.height,
|
||||
initialData: InitialData,
|
||||
onChange: (elements, state) =>
|
||||
console.log("Elements :", elements, "State : ", state),
|
||||
@@ -337,16 +341,27 @@ const excalidrawWrapper = document.getElementById("app");
|
||||
ReactDOM.render(React.createElement(App), excalidrawWrapper);
|
||||
```
|
||||
|
||||
To view the full example visit :point_down:
|
||||
|
||||
[](https://codesandbox.io/s/excalidraw-in-browser-tlqom?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 />}</>;
|
||||
}
|
||||
```
|
||||
|
||||
### Props
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| [`width`](#width) | Number | `window.innerWidth` | The width of Excalidraw component |
|
||||
| [`height`](#height) | Number | `window.innerHeight` | The height of Excalidraw component |
|
||||
| [`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 |
|
||||
@@ -356,18 +371,20 @@ To view the full example visit :point_down:
|
||||
| [`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
|
||||
#### `width`
|
||||
|
||||
Excalidraw takes `100%` of `width` and `height` of the containing block so make sure the container in which you render Excalidraw has non zero dimensions.
|
||||
This props defines the `width` of the Excalidraw component. Defaults to `window.innerWidth` if not passed.
|
||||
|
||||
#### `height`
|
||||
|
||||
This props defines the `height` of the Excalidraw component. Defaults to `window.innerHeight` if not passed.
|
||||
|
||||
#### `onChange`
|
||||
|
||||
@@ -439,9 +456,7 @@ You can pass a `ref` when you want to access some excalidraw APIs. We expose the
|
||||
| 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 |
|
||||
| refresh | `() => 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](#importlibrary) | `(url: string, token?: string) => void` | Imports library from given URL |
|
||||
| setToastMessage | `(message: string) => void` | This API can be used to show the toast with custom message. |
|
||||
| setCanvasOffsets | `() => void` | Updates the offsets for the Excalidraw component so that the coordinates are computed correctly (for example the cursor position). You should call this API when your app changes the dimensions/position of the Excalidraw container, such as when toggling a sidebar. You don't have to call this when the position is changed on page scroll (we handled that ourselves). |
|
||||
|
||||
#### `readyPromise`
|
||||
|
||||
@@ -500,10 +515,6 @@ import { defaultLang, languages } from "@excalidraw/excalidraw";
|
||||
|
||||
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.
|
||||
@@ -516,64 +527,18 @@ This prop indicates whether the app is in `zen mode`. When supplied, the value t
|
||||
|
||||
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`
|
||||
### `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.
|
||||
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`.
|
||||
|
||||
#### `theme`
|
||||
### `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`
|
||||
### `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).
|
||||
|
||||
### importLibrary
|
||||
|
||||
Imports library from given URL. You should call this on `hashchange`, passing the `addLibrary` value if you detect it as shown below. 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/)).
|
||||
|
||||
```js
|
||||
useEffect(() => {
|
||||
const onHashChange = () => {
|
||||
const hash = new URLSearchParams(window.location.hash.slice(1));
|
||||
const libraryUrl = hash.get("addLibrary");
|
||||
if (libraryUrl) {
|
||||
excalidrawRef.current.importLibrary(libraryUrl, hash.get("token"));
|
||||
}
|
||||
};
|
||||
window.addEventListener("hashchange", onHashChange, false);
|
||||
return () => {
|
||||
window.removeEventListener("hashchange", onHashChange);
|
||||
};
|
||||
}, []);
|
||||
```
|
||||
|
||||
Try out the [Demo](#Demo) to see it in action.
|
||||
|
||||
### Extra API's
|
||||
|
||||
#### `getSceneVersion`
|
||||
@@ -619,8 +584,6 @@ import { getElementsMap } from "@excalidraw/excalidraw";
|
||||
|
||||
This function returns an object where each element is mapped to its id.
|
||||
|
||||
### Restore utilities
|
||||
|
||||
#### `restoreAppState`
|
||||
|
||||
**_Signature_**
|
||||
@@ -669,7 +632,10 @@ 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
|
||||
**_The below APIs will be available in [next version](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/CHANGELOG.md#unreleased)_**
|
||||
|
||||
<details id="export-utils">
|
||||
<summary><strong>Export utilities</strong></summary>
|
||||
|
||||
#### `exportToCanvas`
|
||||
|
||||
@@ -753,3 +719,5 @@ This function returns a svg with the exported elements.
|
||||
| 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 |
|
||||
|
||||
</details>
|
||||
|
||||
@@ -1,755 +0,0 @@
|
||||
### 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-assets-dev` in `dist` directory which contains the assets needed for this app in prod and dev mode respectively.
|
||||
|
||||
Move the folder `excalidraw-assets` and `excalidraw-assets-dev` 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:
|
||||
|
||||
[](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:
|
||||
|
||||
[](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 make sure the container in which you render Excalidraw has non zero dimensions.
|
||||
|
||||
#### `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 |
|
||||
| refresh | `() => 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](#importlibrary) | `(url: string, token?: string) => void` | Imports library from given URL |
|
||||
| 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).
|
||||
|
||||
### importLibrary
|
||||
|
||||
Imports library from given URL. You should call this on `hashchange`, passing the `addLibrary` value if you detect it as shown below. 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/)).
|
||||
|
||||
```js
|
||||
useEffect(() => {
|
||||
const onHashChange = () => {
|
||||
const hash = new URLSearchParams(window.location.hash.slice(1));
|
||||
const libraryUrl = hash.get("addLibrary");
|
||||
if (libraryUrl) {
|
||||
excalidrawRef.current.importLibrary(libraryUrl, hash.get("token"));
|
||||
}
|
||||
};
|
||||
window.addEventListener("hashchange", onHashChange, false);
|
||||
return () => {
|
||||
window.removeEventListener("hashchange", onHashChange);
|
||||
};
|
||||
}, []);
|
||||
```
|
||||
|
||||
Try out the [Demo](#Demo) to see it in action.
|
||||
|
||||
### 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 |
|
||||
@@ -10,10 +10,11 @@ import "../../css/styles.scss";
|
||||
import { ExcalidrawAPIRefValue, ExcalidrawProps } from "../../types";
|
||||
import { IsMobileProvider } from "../../is-mobile";
|
||||
import { defaultLang } from "../../i18n";
|
||||
import { DEFAULT_UI_OPTIONS } from "../../constants";
|
||||
|
||||
const Excalidraw = (props: ExcalidrawProps) => {
|
||||
const {
|
||||
width,
|
||||
height,
|
||||
onChange,
|
||||
initialData,
|
||||
excalidrawRef,
|
||||
@@ -29,18 +30,8 @@ const Excalidraw = (props: ExcalidrawProps) => {
|
||||
libraryReturnUrl,
|
||||
theme,
|
||||
name,
|
||||
renderCustomStats,
|
||||
} = props;
|
||||
|
||||
const canvasActions = props.UIOptions?.canvasActions;
|
||||
|
||||
const UIOptions = {
|
||||
canvasActions: {
|
||||
...DEFAULT_UI_OPTIONS.canvasActions,
|
||||
...canvasActions,
|
||||
},
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// Block pinch-zooming on iOS outside of the content area
|
||||
const handleTouchMove = (event: TouchEvent) => {
|
||||
@@ -63,6 +54,8 @@ const Excalidraw = (props: ExcalidrawProps) => {
|
||||
<InitializeApp langCode={langCode}>
|
||||
<IsMobileProvider>
|
||||
<App
|
||||
width={width}
|
||||
height={height}
|
||||
onChange={onChange}
|
||||
initialData={initialData}
|
||||
excalidrawRef={excalidrawRef}
|
||||
@@ -78,8 +71,6 @@ const Excalidraw = (props: ExcalidrawProps) => {
|
||||
libraryReturnUrl={libraryReturnUrl}
|
||||
theme={theme}
|
||||
name={name}
|
||||
renderCustomStats={renderCustomStats}
|
||||
UIOptions={UIOptions}
|
||||
/>
|
||||
</IsMobileProvider>
|
||||
</InitializeApp>
|
||||
@@ -105,7 +96,6 @@ const areEqual = (
|
||||
|
||||
Excalidraw.defaultProps = {
|
||||
lanCode: defaultLang.code,
|
||||
UIOptions: DEFAULT_UI_OPTIONS,
|
||||
};
|
||||
|
||||
const forwardedRefComp = forwardRef<
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user