Compare commits

...

26 Commits

Author SHA1 Message Date
7988cdfb60 Merge branch 'master' of github.com:excalidraw/excalidraw into gcp-portal
* 'master' of github.com:excalidraw/excalidraw: (194 commits)
  fix: Make help toggle tabbable (#3310)
  chore: Update translations from Crowdin (#3270)
  chore(deps): bump @types/jest from 26.0.20 to 26.0.21 (#3298)
  chore(deps): bump @types/react-dom from 17.0.1 to 17.0.2 (#3296)
  chore(deps): bump @types/react from 17.0.2 to 17.0.3 (#3297)
  fix: Show Windows share icon for Windows users (#3306)
  fix: Update browser-fs-access to use new supported export (#3303)
  feat: replaces fontSize and fontFamily text with icons (#2857)
  fix: use random IV for link-sharing encryption (#2829) (#2833)
  fix: Don't scroll to content on INIT websocket message (#3291)
  docs: Release @excalidraw/excalidraw@0.5.0 🎉  (#3289)
  feat: set window.name in excalidraw app & also support target for excalidraw libraries (#3299)
  chore(deps-dev): bump css-loader in /src/packages/utils (#3292)
  chore(deps-dev): bump css-loader in /src/packages/excalidraw (#3293)
  chore(deps-dev): bump webpack in /src/packages/utils (#3294)
  chore(deps-dev): bump webpack in /src/packages/excalidraw (#3295)
  feat: support pasting file contents & always prefer system clip (#3257)
  fix: Don't show export and delete when library is empty (#3288)
  feat: Add label for name field and use input when editable in export dialog (#3286)
  fix: overflow in textinput in export dialog (#3284)
  ...
2021-03-23 13:58:07 +02:00
40656c70d1 fix: Make help toggle tabbable (#3310)
* fix: Make help toggle tabbable

* Update src/components/HelpIcon.tsx

Co-authored-by: Aakansha Doshi <aakansha1216@gmail.com>

Co-authored-by: Aakansha Doshi <aakansha1216@gmail.com>
2021-03-23 16:48:10 +05:30
c5b4b04d6b chore: Update translations from Crowdin (#3270) 2021-03-22 22:51:06 +02:00
1ad212677b chore(deps): bump @types/jest from 26.0.20 to 26.0.21 (#3298)
Bumps [@types/jest](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/jest) from 26.0.20 to 26.0.21.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/jest)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-03-22 22:50:15 +02:00
32427c355c chore(deps): bump @types/react-dom from 17.0.1 to 17.0.2 (#3296)
Bumps [@types/react-dom](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react-dom) from 17.0.1 to 17.0.2.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react-dom)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-03-22 18:12:02 +00:00
402a812159 chore(deps): bump @types/react from 17.0.2 to 17.0.3 (#3297)
Bumps [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react) from 17.0.2 to 17.0.3.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-03-22 20:00:23 +02:00
0480753581 fix: Show Windows share icon for Windows users (#3306)
* fix: Show Windows share icon for Windows users

* move function outside t he component
2021-03-22 17:02:20 +01:00
f7e17a28fa fix: Update browser-fs-access to use new supported export (#3303)
* Use new exported supported

* Bump to v0.15.3
2021-03-22 14:58:26 +01:00
78f3a92dd1 feat: replaces fontSize and fontFamily text with icons (#2857)
Co-authored-by: Hitesh Goyal <hiteshlyearn@Hiteshs-MacBook-Pro.local>
Co-authored-by: dwelle <luzar.david@gmail.com>
2021-03-22 14:26:35 +01:00
c8743a8c02 fix: use random IV for link-sharing encryption (#2829) (#2833)
* fix: use random IV for link-sharing encryption (#2829)

* fix: add backward compatibility for link-sharing encryption (#2829)
2021-03-21 22:31:35 -07:00
127c1be6ad fix: Don't scroll to content on INIT websocket message (#3291)
If you load a shared scene with at least another person on the scene, you can start seeing the content via the firebase response. If you scroll and you receive the response from the websocket INIT, then it scrolls you back to the center which is jarring.

This PR removes the scroll to content for that use case.
2021-03-21 17:25:19 +01:00
86bf2d697d docs: Release @excalidraw/excalidraw@0.5.0 🎉 (#3289)
* docs: Release @excalidraw/excalidraw@0.5.0

* update changelog

* update readme

* remove styles since github strips the styles in markdown
2021-03-21 19:19:09 +05:30
7ee8de0a46 feat: set window.name in excalidraw app & also support target for excalidraw libraries (#3299)
* feat: set window.name in excalidraw app so library installation always opens on same tab & also support target for excalidraw libraries

* update changelog and readme

* Update public/index.html

Co-authored-by: David Luzar <luzar.david@gmail.com>

* use level 4 heading

* Update src/packages/excalidraw/README.md

Co-authored-by: David Luzar <luzar.david@gmail.com>

Co-authored-by: David Luzar <luzar.david@gmail.com>
2021-03-21 18:13:52 +05:30
981f327b48 chore(deps-dev): bump css-loader in /src/packages/utils (#3292)
Bumps [css-loader](https://github.com/webpack-contrib/css-loader) from 5.1.2 to 5.1.3.
- [Release notes](https://github.com/webpack-contrib/css-loader/releases)
- [Changelog](https://github.com/webpack-contrib/css-loader/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack-contrib/css-loader/compare/v5.1.2...v5.1.3)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-03-21 12:55:37 +05:30
eeea8406c9 chore(deps-dev): bump css-loader in /src/packages/excalidraw (#3293)
Bumps [css-loader](https://github.com/webpack-contrib/css-loader) from 5.1.2 to 5.1.3.
- [Release notes](https://github.com/webpack-contrib/css-loader/releases)
- [Changelog](https://github.com/webpack-contrib/css-loader/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack-contrib/css-loader/compare/v5.1.2...v5.1.3)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-03-21 12:31:15 +05:30
0f249e3b26 chore(deps-dev): bump webpack in /src/packages/utils (#3294)
Bumps [webpack](https://github.com/webpack/webpack) from 5.24.3 to 5.27.1.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v5.24.3...v5.27.1)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-03-21 12:29:44 +05:30
2c7c80bd75 chore(deps-dev): bump webpack in /src/packages/excalidraw (#3295)
Bumps [webpack](https://github.com/webpack/webpack) from 5.24.3 to 5.27.1.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v5.24.3...v5.27.1)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-03-21 12:21:09 +05:30
94ad8eaa19 feat: support pasting file contents & always prefer system clip (#3257) 2021-03-20 20:20:47 +01:00
13d9374cde fix: Don't show export and delete when library is empty (#3288) 2021-03-20 21:58:37 +05:30
efb6d0825b feat: Add label for name field and use input when editable in export dialog (#3286)
* feat: Add label for name field and use input when editable in export dialog

* fix

* review fix

* dnt allow to edit file name when view mode

* Update src/components/ProjectName.tsx

Co-authored-by: David Luzar <luzar.david@gmail.com>

Co-authored-by: David Luzar <luzar.david@gmail.com>
2021-03-20 21:57:58 +05:30
80a61db72f fix: overflow in textinput in export dialog (#3284)
* fix: overflow in textinput in export dialog

* use width
2021-03-20 18:21:48 +05:30
9a13dd8836 fix: bail on noop updates for newElementWith (#3279) 2021-03-20 13:29:53 +01:00
cf6a5ff16b fix: state continuously updated when holding ctrl/cmd (#3283) 2021-03-20 13:28:28 +01:00
fa8c7abf50 fix: debounce.flush not invoked if lastArgs not defined (#3281) 2021-03-20 13:15:28 +01:00
c3ecbcb3ab feat: Allow host app to update title of drawing (#3273)
* Allow updating name on updateScene

* Revert "Allow updating name on updateScene"

This reverts commit 4e07a608d3.

* Make requested changes

* Make requested changes

* Remove customName from state

* Remove redundant if statement

* Add tests, update changelog and minor fixes

* remove eempty lines

* minor fixes

* no border and on hover no background change

* Give preference to name prop when initialData.appState.name is present and update specs

* minor fix

* Fix name input style in dark mode

Co-authored-by: Aakansha Doshi <aakansha1216@gmail.com>
2021-03-20 16:08:03 +05:30
1f7c75de63 feat: GCP Portal 2021-02-08 00:17:36 +02:00
74 changed files with 1715 additions and 1450 deletions

2
.env
View File

@ -1,5 +1,5 @@
REACT_APP_BACKEND_V1_GET_URL=https://json.excalidraw.com/api/v1/
REACT_APP_BACKEND_V2_GET_URL=https://json.excalidraw.com/api/v2/
REACT_APP_BACKEND_V2_POST_URL=https://json.excalidraw.com/api/v2/post/
REACT_APP_SOCKET_SERVER_URL=https://portal.excalidraw.com
REACT_APP_SOCKET_SERVER_URL=https://excalidraw-portal.uc.r.appspot.com
REACT_APP_FIREBASE_CONFIG='{"apiKey":"AIzaSyAd15pYlMci_xIp9ko6wkEsDzAAA0Dn0RU","authDomain":"excalidraw-room-persistence.firebaseapp.com","databaseURL":"https://excalidraw-room-persistence.firebaseio.com","projectId":"excalidraw-room-persistence","storageBucket":"excalidraw-room-persistence.appspot.com","messagingSenderId":"654800341332","appId":"1:654800341332:web:4a692de832b55bd57ce0c1"}'

View File

@ -23,11 +23,11 @@
"@sentry/integrations": "6.2.1",
"@testing-library/jest-dom": "5.11.9",
"@testing-library/react": "11.2.5",
"@types/jest": "26.0.20",
"@types/react": "17.0.2",
"@types/react-dom": "17.0.1",
"@types/jest": "26.0.21",
"@types/react": "17.0.3",
"@types/react-dom": "17.0.2",
"@types/socket.io-client": "1.4.36",
"browser-fs-access": "0.14.2",
"browser-fs-access": "0.15.3",
"clsx": "1.1.1",
"firebase": "8.2.10",
"i18next-browser-languagedetector": "6.0.1",

View File

@ -88,6 +88,8 @@
<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

View File

@ -11,6 +11,7 @@ import { t } from "../i18n";
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",
@ -18,11 +19,14 @@ export const actionChangeProjectName = register({
trackEvent("change", "title");
return { appState: { ...appState, name: value }, commitToHistory: false };
},
PanelComponent: ({ appState, updateData }) => (
PanelComponent: ({ appState, updateData, appProps }) => (
<ProjectName
label={t("labels.fileTitle")}
value={appState.name || "Unnamed"}
onChange={(name: string) => updateData(name)}
isNameEditable={
typeof appProps.name === "undefined" && !appState.viewModeEnabled
}
/>
),
});
@ -161,9 +165,7 @@ export const actionSaveAsScene = register({
title={t("buttons.saveAs")}
aria-label={t("buttons.saveAs")}
showAriaLabel={useIsMobile()}
hidden={
!("chooseFileSystemEntries" in window || "showOpenFilePicker" in window)
}
hidden={!supported}
onClick={() => updateData(null)}
/>
),

View File

@ -1,7 +1,6 @@
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 {
@ -21,6 +20,16 @@ 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 {
@ -413,13 +422,29 @@ export const actionChangeFontSize = register({
PanelComponent: ({ elements, appState, updateData }) => (
<fieldset>
<legend>{t("labels.fontSize")}</legend>
<ButtonSelect
<ButtonIconSelect
group="font-size"
options={[
{ 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: 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={getFormValue(
elements,
@ -456,16 +481,28 @@ export const actionChangeFontFamily = register({
};
},
PanelComponent: ({ elements, appState, updateData }) => {
const options: { value: FontFamily; text: string }[] = [
{ value: 1, text: t("labels.handDrawn") },
{ value: 2, text: t("labels.normal") },
{ value: 3, text: t("labels.code") },
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} />,
},
];
return (
<fieldset>
<legend>{t("labels.fontFamily")}</legend>
<ButtonSelect<FontFamily | false>
<ButtonIconSelect<FontFamily | false>
group="font-family"
options={options}
value={getFormValue(
@ -506,12 +543,24 @@ export const actionChangeTextAlign = register({
PanelComponent: ({ elements, appState, updateData }) => (
<fieldset>
<legend>{t("labels.textAlign")}</legend>
<ButtonSelect<TextAlign | false>
<ButtonIconSelect<TextAlign | false>
group="text-align"
options={[
{ value: "left", text: t("labels.left") },
{ value: "center", text: t("labels.center") },
{ value: "right", text: t("labels.right") },
{
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={getFormValue(
elements,

View File

@ -122,6 +122,7 @@ export class ActionManager implements ActionsManagerInterface {
appState={this.getAppState()}
updateData={updateData}
id={id}
appProps={this.app.props}
/>
);
}

View File

@ -1,6 +1,6 @@
import React from "react";
import { ExcalidrawElement } from "../element/types";
import { AppState } from "../types";
import { AppState, ExcalidrawProps } from "../types";
/** if false, the action should be prevented */
export type ActionResult =
@ -94,6 +94,7 @@ export interface Action {
elements: readonly ExcalidrawElement[];
appState: AppState;
updateData: (formData?: any) => void;
appProps: ExcalidrawProps;
id?: string;
}>;
perform: ActionFn;

View File

@ -7,12 +7,10 @@ import { AppState } from "./types";
import { SVG_EXPORT_TAG } from "./scene/export";
import { tryParseSpreadsheet, Spreadsheet, VALID_SPREADSHEET } from "./charts";
import { canvasToBlob } from "./data/blob";
const TYPE_ELEMENTS = "excalidraw/elements";
import { EXPORT_DATA_TYPES } from "./constants";
type ElementsClipboard = {
type: typeof TYPE_ELEMENTS;
created: number;
type: typeof EXPORT_DATA_TYPES.excalidrawClipboard;
elements: ExcalidrawElement[];
};
@ -31,8 +29,16 @@ export const probablySupportsClipboardBlob =
"ClipboardItem" in window &&
"toBlob" in HTMLCanvasElement.prototype;
const isElementsClipboard = (contents: any): contents is ElementsClipboard => {
if (contents?.type === TYPE_ELEMENTS) {
const clipboardContainsElements = (
contents: any,
): contents is { elements: ExcalidrawElement[] } => {
if (
[
EXPORT_DATA_TYPES.excalidraw,
EXPORT_DATA_TYPES.excalidrawClipboard,
].includes(contents?.type) &&
Array.isArray(contents.elements)
) {
return true;
}
return false;
@ -43,8 +49,7 @@ export const copyToClipboard = async (
appState: AppState,
) => {
const contents: ElementsClipboard = {
type: TYPE_ELEMENTS,
created: Date.now(),
type: EXPORT_DATA_TYPES.excalidrawClipboard,
elements: getSelectedElements(elements, appState),
};
const json = JSON.stringify(contents);
@ -131,15 +136,9 @@ export const parseClipboard = async (
try {
const systemClipboardData = JSON.parse(systemClipboard);
// system clipboard elements are newer than in-app clipboard
if (
isElementsClipboard(systemClipboardData) &&
(!appClipboardData?.created ||
appClipboardData.created < systemClipboardData.created)
) {
if (clipboardContainsElements(systemClipboardData)) {
return { elements: systemClipboardData.elements };
}
// in-app clipboard is newer than system clipboard
return appClipboardData;
} catch {
// system clipboard doesn't contain excalidraw elements → return plaintext

View File

@ -3,6 +3,7 @@ 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,
@ -303,6 +304,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
zenModeEnabled = false,
gridModeEnabled = false,
theme = defaultAppState.theme,
name = defaultAppState.name,
} = props;
this.state = {
...defaultAppState,
@ -314,6 +316,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
viewModeEnabled,
zenModeEnabled,
gridSize: gridModeEnabled ? GRID_SIZE : null,
name,
};
if (excalidrawRef) {
const readyPromise =
@ -523,7 +526,7 @@ class App extends React.Component<ExcalidrawProps, 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;
if (typeof this.props.viewModeEnabled !== "undefined") {
viewModeEnabled = this.props.viewModeEnabled;
}
@ -540,6 +543,10 @@ class App extends React.Component<ExcalidrawProps, AppState> {
theme = this.props.theme;
}
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
@ -556,6 +563,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
zenModeEnabled,
gridSize,
theme,
name,
});
},
() => {
@ -890,6 +898,13 @@ class App extends React.Component<ExcalidrawProps, AppState> {
gridSize: this.props.gridModeEnabled ? GRID_SIZE : null,
});
}
if (this.props.name && prevProps.name !== this.props.name) {
this.setState({
name: this.props.name,
});
}
document
.querySelector(".excalidraw")
?.classList.toggle("theme--dark", this.state.theme === "dark");
@ -1390,7 +1405,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
return;
}
if (event[KEYS.CTRL_OR_CMD]) {
if (event[KEYS.CTRL_OR_CMD] && this.state.isBindingEnabled) {
this.setState({ isBindingEnabled: false });
}
@ -3595,10 +3610,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
file?.name.endsWith(".excalidraw")
) {
this.setState({ isLoading: true });
if (
"chooseFileSystemEntries" in window ||
"showOpenFilePicker" in window
) {
if (supported) {
try {
// This will only work as of Chrome 86,
// but can be safely ignored on older releases.

View File

@ -31,9 +31,27 @@
.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;
border: none;
&:hover {
background: none;
}
width: auto;
max-width: 200px;
padding-left: 2px;
}
}
}

View File

@ -257,6 +257,7 @@ export const ExportDialog = ({
onClick={() => {
setModalIsShown(true);
}}
data-testid="export-button"
icon={exportFile}
type="button"
aria-label={t("buttons.export")}

View File

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

View File

@ -144,36 +144,41 @@ const LibraryMenuItems = ({
});
}}
/>
<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([]);
}
}}
/>
{!!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([]);
}
}}
/>
</>
)}
<a
href={`https://libraries.excalidraw.com?referrer=${referrer}`}
href={`https://libraries.excalidraw.com?target=${
window.name || "_blank"
}&referrer=${referrer}`}
target="_excalidraw_libraries"
>
{t("labels.libraries")}

View File

@ -1,25 +1,26 @@
import "./TextInput.scss";
import React, { Component } from "react";
import { selectNode, removeSelection } from "../utils";
type Props = {
value: string;
onChange: (value: string) => void;
label: string;
isNameEditable: boolean;
};
export class ProjectName extends Component<Props> {
private handleFocus = (event: React.FocusEvent<HTMLElement>) => {
selectNode(event.currentTarget);
type State = {
fileName: string;
};
export class ProjectName extends Component<Props, State> {
state = {
fileName: this.props.value,
};
private handleBlur = (event: React.FocusEvent<HTMLElement>) => {
const value = event.currentTarget.innerText.trim();
private handleBlur = (event: any) => {
const value = event.target.value;
if (value !== this.props.value) {
this.props.onChange(value);
}
removeSelection();
};
private handleKeyDown = (event: React.KeyboardEvent<HTMLElement>) => {
@ -31,32 +32,30 @@ export class ProjectName extends Component<Props> {
event.currentTarget.blur();
}
};
private makeEditable = (editable: HTMLSpanElement | null) => {
if (!editable) {
return;
}
try {
editable.contentEditable = "plaintext-only";
} catch {
editable.contentEditable = "true";
}
};
public render() {
return (
<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>
<>
<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>
)}
</>
);
}
}

View File

@ -58,6 +58,7 @@ export const ToolButton = React.forwardRef((props: ToolButtonProps, ref) => {
"ToolIcon--selected": props.selected,
},
)}
data-testid={props["data-testid"]}
hidden={props.hidden}
title={props.title}
aria-label={props["aria-label"]}

View File

@ -123,6 +123,22 @@ 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(
@ -794,3 +810,121 @@ 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 },
),
);

View File

@ -84,9 +84,15 @@ 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;

View File

@ -222,7 +222,8 @@
align-items: center;
svg {
width: 36px;
height: 18px;
height: 14px;
padding: 2px;
opacity: 0.6;
}
&.active svg {
@ -453,6 +454,14 @@
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;

View File

@ -1,5 +1,5 @@
import { cleanAppStateForExport } from "../appState";
import { MIME_TYPES } from "../constants";
import { EXPORT_DATA_TYPES, MIME_TYPES } from "../constants";
import { clearElementsForExport } from "../element";
import { CanvasError } from "../errors";
import { t } from "../i18n";
@ -121,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 !== "excalidrawlib") {
if (data.type !== EXPORT_DATA_TYPES.excalidrawLibrary) {
throw new Error(t("alerts.couldNotLoadInvalidFile"));
}
return data;

View File

@ -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 { MIME_TYPES } from "../constants";
import { EXPORT_DATA_TYPES, MIME_TYPES } from "../constants";
// -----------------------------------------------------------------------------
// PNG
@ -67,7 +67,10 @@ 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 === "excalidraw") {
if (
"type" in encodedData &&
encodedData.type === EXPORT_DATA_TYPES.excalidraw
) {
return metadata.text;
}
throw new Error("FAILED");
@ -115,7 +118,10 @@ 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 === "excalidraw") {
if (
"type" in encodedData &&
encodedData.type === EXPORT_DATA_TYPES.excalidraw
) {
return json;
}
throw new Error("FAILED");

View File

@ -1,6 +1,6 @@
import { fileOpen, fileSave } from "browser-fs-access";
import { cleanAppStateForExport } from "../appState";
import { MIME_TYPES } from "../constants";
import { EXPORT_DATA_TYPES, 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: "excalidraw",
type: EXPORT_DATA_TYPES.excalidraw,
version: 2,
source: window.location.origin,
elements: clearElementsForExport(elements),
@ -69,7 +69,7 @@ export const isValidExcalidrawData = (data?: {
appState?: any;
}): data is ImportedDataState => {
return (
data?.type === "excalidraw" &&
data?.type === EXPORT_DATA_TYPES.excalidraw &&
(!data.elements ||
(Array.isArray(data.elements) &&
(!data.appState || typeof data.appState === "object")))
@ -80,7 +80,7 @@ export const isValidLibrary = (json: any) => {
return (
typeof json === "object" &&
json &&
json.type === "excalidrawlib" &&
json.type === EXPORT_DATA_TYPES.excalidrawLibrary &&
json.version === 1
);
};
@ -89,7 +89,7 @@ export const saveLibraryAsJSON = async () => {
const library = await Library.loadLibrary();
const serialized = JSON.stringify(
{
type: "excalidrawlib",
type: EXPORT_DATA_TYPES.excalidrawLibrary,
version: 1,
library,
},

View File

@ -87,9 +87,30 @@ export const mutateElement = <TElement extends Mutable<ExcalidrawElement>>(
export const newElementWith = <TElement extends ExcalidrawElement>(
element: TElement,
updates: ElementUpdate<TElement>,
): TElement => ({
...element,
...updates,
version: element.version + 1,
versionNonce: randomInteger(),
});
): 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(),
};
};

View File

@ -207,7 +207,8 @@ export const textWysiwyg = ({
// prevent blur when changing properties from the menu
const onPointerDown = (event: MouseEvent) => {
if (
event.target instanceof HTMLElement &&
(event.target instanceof HTMLElement ||
event.target instanceof SVGElement) &&
event.target.closest(`.${CLASSES.SHAPE_ACTIONS_MENU}`) &&
!isWritableElement(event.target)
) {

View File

@ -448,15 +448,8 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
private handleRemoteSceneUpdate = (
elements: ReconciledElements,
{
init = false,
initFromSnapshot = false,
}: { init?: boolean; initFromSnapshot?: boolean } = {},
{ init = false }: { init?: boolean } = {},
) => {
if (init || initFromSnapshot) {
this.excalidrawAPI.setScrollToContent(elements);
}
this.excalidrawAPI.updateScene({
elements,
commitToHistory: !!init,

View File

@ -7,12 +7,27 @@ 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,
@ -31,8 +46,6 @@ 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 {
@ -93,7 +106,7 @@ const RoomDialog = ({
{"share" in navigator ? (
<ToolButton
type="button"
icon={isAppleBrowser ? shareIOS : share}
icon={getShareIcon()}
title={t("labels.share")}
aria-label={t("labels.share")}
onClick={shareRoomLink}

View File

@ -80,8 +80,10 @@ export type SocketUpdateData = SocketUpdateDataSource[keyof SocketUpdateDataSour
_brand: "socketUpdateData";
};
const IV_LENGTH_BYTES = 12; // 96 bits
export const createIV = () => {
const arr = new Uint8Array(12);
const arr = new Uint8Array(IV_LENGTH_BYTES);
return window.crypto.getRandomValues(arr);
};
@ -175,6 +177,22 @@ 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,
@ -183,6 +201,7 @@ 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 {};
@ -190,16 +209,19 @@ const importFromBackend = async (
let data: ImportedDataState;
if (privateKey) {
const buffer = await response.arrayBuffer();
const key = await getImportedKey(privateKey, "decrypt");
const iv = new Uint8Array(12);
const decrypted = await window.crypto.subtle.decrypt(
{
name: "AES-GCM",
iv,
},
key,
buffer,
);
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);
}
// We need to convert the decrypted array buffer to a string
const string = new window.TextDecoder("utf-8").decode(
new Uint8Array(decrypted) as any,
@ -263,9 +285,8 @@ export const exportToBackend = async (
true, // extractable
["encrypt", "decrypt"],
);
// 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);
const iv = createIV();
// 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(
@ -276,6 +297,11 @@ 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);
@ -283,7 +309,7 @@ export const exportToBackend = async (
try {
const response = await fetch(BACKEND_V2_POST, {
method: "POST",
body: encrypted,
body: payload,
});
const json = await response.json();
if (json.id) {

View File

@ -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,9 +92,9 @@
"centerHorizontally": "توسيط أفقي",
"distributeHorizontally": "التوزيع الأفقي",
"distributeVertically": "التوزيع عمودياً",
"viewMode": "",
"viewMode": "نمط العرض",
"toggleExportColorScheme": "",
"share": ""
"share": "مشاركة"
},
"buttons": {
"clearReset": "إعادة تعيين اللوحة",
@ -119,7 +119,7 @@
"edit": "تعديل",
"undo": "تراجع",
"redo": "إعادة تنفيذ",
"resetLibrary": "",
"resetLibrary": "إعادة ضبط المكتبة",
"createNewRoom": "إنشاء غرفة جديدة",
"fullScreen": "شاشة كاملة",
"darkMode": "الوضع المظلم",
@ -138,7 +138,7 @@
"decryptFailed": "تعذر فك تشفير البيانات.",
"uploadedSecurly": "تم تأمين التحميل بتشفير النهاية إلى النهاية، مما يعني أن خادوم Excalidraw والأطراف الثالثة لا يمكنها قراءة المحتوى.",
"loadSceneOverridePrompt": "تحميل الرسم الخارجي سيحل محل المحتوى الموجود لديك. هل ترغب في المتابعة؟",
"collabStopOverridePrompt": "",
"collabStopOverridePrompt": "إيقاف الجلسة سيؤدي إلى الكتابة فوق رسومك السابقة المخزنة داخليا. هل أنت متأكد؟\n\n(إذا كنت ترغب في الاحتفاظ برسمك المخزن داخليا، ببساطة أغلق علامة تبويب المتصفح بدلاً من ذلك.)",
"errorLoadingLibrary": "حصل خطأ أثناء تحميل مكتبة الطرف الثالث.",
"confirmAddLibrary": "هذا سيضيف {{numShapes}} شكل إلى مكتبتك. هل أنت متأكد؟",
"imageDoesNotContainScene": "استيراد الصور غير مدعوم في الوقت الراهن.\n\nهل تريد استيراد مشهد؟ لا يبدو أن هذه الصورة تحتوي على أي بيانات مشهد. هل قمت بسماح هذا أثناء التصدير؟",
@ -212,9 +212,9 @@
"curvedArrow": "",
"curvedLine": "",
"documentation": "",
"drag": "",
"editor": "",
"github": "",
"drag": "اسحب",
"editor": "المحرر",
"github": "عثرت على مشكلة؟ إرسال",
"howto": "",
"or": "",
"preventBinding": "",

View File

@ -61,7 +61,7 @@
"architect": "Архитект",
"artist": "Художник",
"cartoonist": "Карикатурист",
"fileTitle": "Заглавие на файл",
"fileTitle": "",
"colorPicker": "Избор на цвят",
"canvasBackground": "Фон на платно",
"drawingCanvas": "Платно за рисуване",

View File

@ -61,7 +61,7 @@
"architect": "Arquitecte",
"artist": "Artista",
"cartoonist": "Dibuixant",
"fileTitle": "Títol del fitxer",
"fileTitle": "",
"colorPicker": "Selector de colors",
"canvasBackground": "Fons del llenç",
"drawingCanvas": "Llenç de dibuix",

View File

@ -61,7 +61,7 @@
"architect": "Αρχιτέκτονας",
"artist": "Καλλιτέχνης",
"cartoonist": "Σκιτσογράφος",
"fileTitle": "Τίτλος αρχείου",
"fileTitle": "",
"colorPicker": "Επιλογή Χρώματος",
"canvasBackground": "Φόντο καμβά",
"drawingCanvas": "Σχεδίαση καμβά",
@ -94,7 +94,7 @@
"distributeVertically": "Κατακόρυφη κατανομή",
"viewMode": "Λειτουργία προβολής",
"toggleExportColorScheme": "Εναλλαγή εξαγωγής θέματος χρωμάτων",
"share": ""
"share": "Κοινοποίηση"
},
"buttons": {
"clearReset": "Επαναφορά του καμβά",

View File

@ -61,7 +61,7 @@
"architect": "Architect",
"artist": "Artist",
"cartoonist": "Cartoonist",
"fileTitle": "File title",
"fileTitle": "File name",
"colorPicker": "Color picker",
"canvasBackground": "Canvas background",
"drawingCanvas": "Drawing canvas",

View File

@ -61,7 +61,7 @@
"architect": "Arquitecto",
"artist": "Artista",
"cartoonist": "Caricatura",
"fileTitle": "Título del archivo",
"fileTitle": "Nombre del archivo",
"colorPicker": "Selector de color",
"canvasBackground": "Fondo del lienzo",
"drawingCanvas": "Lienzo de dibujo",

View File

@ -61,7 +61,7 @@
"architect": "معمار",
"artist": "هنرمند",
"cartoonist": "کارتونیست",
"fileTitle": "عنوان فایل",
"fileTitle": "",
"colorPicker": "انتخابگر رنگ",
"canvasBackground": "بوم",
"drawingCanvas": "بوم نقاشی",

View File

@ -61,7 +61,7 @@
"architect": "Arkkitehti",
"artist": "Taiteilija",
"cartoonist": "Sarjakuva",
"fileTitle": "Tiedoston otsikko",
"fileTitle": "Tiedostonimi",
"colorPicker": "Värin valinta",
"canvasBackground": "Piirtoalueen tausta",
"drawingCanvas": "Piirtoalue",

View File

@ -61,7 +61,7 @@
"architect": "Architecte",
"artist": "Artiste",
"cartoonist": "Caricaturiste",
"fileTitle": "Titre du fichier",
"fileTitle": "Nom du fichier",
"colorPicker": "Sélecteur de couleur",
"canvasBackground": "Arrière-plan du canevas",
"drawingCanvas": "Zone de dessin",

View File

@ -61,7 +61,7 @@
"architect": "ארכיטקט",
"artist": "אמן",
"cartoonist": "קריקטוריסט",
"fileTitle": "כותרת הקובץ",
"fileTitle": "",
"colorPicker": "בחירת צבע",
"canvasBackground": "רקע הלוח",
"drawingCanvas": "לוח ציור",

View File

@ -61,7 +61,7 @@
"architect": "वास्तुकार",
"artist": "कलाकार",
"cartoonist": "व्यंग्य चित्रकार",
"fileTitle": "फ़ाइल का शीर्षक",
"fileTitle": "",
"colorPicker": "रंग चयन",
"canvasBackground": "कैनवास बैकग्राउंड",
"drawingCanvas": "कैनवास बना रहे हैं",

View File

@ -61,7 +61,7 @@
"architect": "Tervezői",
"artist": "Művészi",
"cartoonist": "Karikatúrás",
"fileTitle": "Fájl címe",
"fileTitle": "",
"colorPicker": "Színválasztó",
"canvasBackground": "Vászon háttérszíne",
"drawingCanvas": "Rajzvászon",

View File

@ -61,7 +61,7 @@
"architect": "Arsitek",
"artist": "Artis",
"cartoonist": "Kartunis",
"fileTitle": "Judul file",
"fileTitle": "Nama file",
"colorPicker": "Pilihan Warna",
"canvasBackground": "Latar Kanvas",
"drawingCanvas": "Kanvas",
@ -143,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": "",
"invalidSceneUrl": "Tidak dapat impor pemandangan dari URL. Kemungkinan URL itu rusak atau tidak berisi data JSON Excalidraw yang valid.",
"resetLibrary": "Ini akan menghapus pustaka Anda. Anda yakin?"
},
"toolBar": {

View File

@ -61,7 +61,7 @@
"architect": "Architetto",
"artist": "Artista",
"cartoonist": "Fumettista",
"fileTitle": "Titolo del file",
"fileTitle": "Nome del file",
"colorPicker": "Selettore colore",
"canvasBackground": "Sfondo tela",
"drawingCanvas": "Area di disegno",

View File

@ -61,7 +61,7 @@
"architect": "正確",
"artist": "アート",
"cartoonist": "漫画風",
"fileTitle": "ファイル名",
"fileTitle": "",
"colorPicker": "色選択",
"canvasBackground": "キャンバスの背景",
"drawingCanvas": "キャンバスの描画",

View File

@ -61,7 +61,7 @@
"architect": "Amasdag",
"artist": "Anaẓur",
"cartoonist": "",
"fileTitle": "Azwel n ufaylu",
"fileTitle": "",
"colorPicker": "Amafran n yini",
"canvasBackground": "Agilal n teɣzut n usuneɣ",
"drawingCanvas": "Taɣzut n usuneɣ",

View File

@ -61,7 +61,7 @@
"architect": "건축가",
"artist": "예술가",
"cartoonist": "만화가",
"fileTitle": "파일명",
"fileTitle": "",
"colorPicker": "색상 선택기",
"canvasBackground": "캔버스 배경",
"drawingCanvas": "캔버스 그리기",

View File

@ -61,7 +61,7 @@
"architect": "ဗိသုကာ",
"artist": "ပန်းချီ",
"cartoonist": "ကာတွန်း",
"fileTitle": "ခေါင်းစဉ်",
"fileTitle": "",
"colorPicker": "အရောင်ရွေး",
"canvasBackground": "ကားချပ်နောက်ခံ",
"drawingCanvas": "ပုံဆွဲကားချပ်",

View File

@ -138,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": "",
"collabStopOverridePrompt": "Wanneer de sessie wordt gestopt, overschrijft u de eerdere, lokaal opgeslagen tekening. Weet je het zeker?\n\n(Als je de lokale tekening wilt behouden, sluit je in plaats daarvan het browsertabblad)",
"errorLoadingLibrary": "Bij het laden van de externe bibliotheek is een fout opgetreden.",
"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?",

View File

@ -61,7 +61,7 @@
"architect": "Arkitekt",
"artist": "Kunstnar",
"cartoonist": "Teiknar",
"fileTitle": "Filnamn",
"fileTitle": "",
"colorPicker": "Fargeveljar",
"canvasBackground": "Lerretsbakgrunn",
"drawingCanvas": "Lerret",

View File

@ -61,7 +61,7 @@
"architect": "Arquitècte",
"artist": "Artista",
"cartoonist": "Dessenhaire",
"fileTitle": "Títol del fichièr",
"fileTitle": "Nom del fichièr",
"colorPicker": "Selector de color",
"canvasBackground": "Rèireplan del canabàs",
"drawingCanvas": "Zòna de dessenh",

View File

@ -61,7 +61,7 @@
"architect": "ਭਵਨ ਨਿਰਮਾਣਕਾਰੀ",
"artist": "ਕਲਾਕਾਰ",
"cartoonist": "ਕਾਰਟੂਨਿਸਟ",
"fileTitle": "ਫਾਈਲ ਦਾ ਸਿਰਨਾਵਾਂ",
"fileTitle": "",
"colorPicker": "ਰੰਗ ਚੋਣਕਾਰ",
"canvasBackground": "ਕੈਨਵਸ ਦਾ ਬੈਕਗਰਾਉਂਡ",
"drawingCanvas": "ਡਰਾਇੰਗ ਕੈਨਵਸ",

View File

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

View File

@ -61,7 +61,7 @@
"architect": "Dokładny",
"artist": "Artystyczny",
"cartoonist": "Rysunkowy",
"fileTitle": "Tytuł pliku",
"fileTitle": "",
"colorPicker": "Paleta kolorów",
"canvasBackground": "Kolor dokumentu",
"drawingCanvas": "Obszar roboczy",

View File

@ -61,7 +61,7 @@
"architect": "Arquiteto",
"artist": "Artista",
"cartoonist": "Cartunista",
"fileTitle": "Título do arquivo",
"fileTitle": "Nome do arquivo",
"colorPicker": "Seletor de cores",
"canvasBackground": "Fundo da tela",
"drawingCanvas": "Tela de desenho",

View File

@ -61,7 +61,7 @@
"architect": "Arquitecto",
"artist": "Artista",
"cartoonist": "Caricaturista",
"fileTitle": "Título do ficheiro",
"fileTitle": "",
"colorPicker": "Seletor de cores",
"canvasBackground": "Fundo da tela",
"drawingCanvas": "Tela de desenho",

View File

@ -61,7 +61,7 @@
"architect": "Arhitect",
"artist": "Artist",
"cartoonist": "Caricaturist",
"fileTitle": "Denumirea fișierului",
"fileTitle": "Nume de fișier",
"colorPicker": "Selector de culoare",
"canvasBackground": "Fundalul pânzei",
"drawingCanvas": "Pânză pentru desenat",

View File

@ -61,7 +61,7 @@
"architect": "Архитектор",
"artist": "Художник",
"cartoonist": "Карикатурист",
"fileTitle": "Название файла",
"fileTitle": "Имя файла",
"colorPicker": "Выбор цвета",
"canvasBackground": "Фон холста",
"drawingCanvas": "Полотно",

View File

@ -61,7 +61,7 @@
"architect": "Arkitekt",
"artist": "Artist",
"cartoonist": "Serietecknare",
"fileTitle": "Filtitel",
"fileTitle": "Filnamn",
"colorPicker": "Färgväljare",
"canvasBackground": "Canvas-bakgrund",
"drawingCanvas": "Ritar canvas",

View File

@ -1,7 +1,7 @@
{
"labels": {
"paste": "Yapıştır",
"pasteCharts": "Dairesel grafik",
"pasteCharts": "Grafikleri yapıştır",
"selectAll": "Tümünü seç",
"multiSelect": "Seçime öge ekle",
"moveCanvas": "Tuvali taşı",
@ -68,7 +68,7 @@
"layers": "Katmanlar",
"actions": "Eylemler",
"language": "Dil",
"liveCollaboration": "",
"liveCollaboration": "Canlı ortak çalışma alanı",
"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": "",
"showGrid": "Izgarayı göster",
"addToLibrary": "Kütüphaneye ekle",
"removeFromLibrary": "Kütüphaneden kaldır",
"libraryLoadingMessage": "Kütüphane yükleniyor…",
@ -94,7 +94,7 @@
"distributeVertically": "Dikey dağıt",
"viewMode": "",
"toggleExportColorScheme": "",
"share": ""
"share": "Paylaş"
},
"buttons": {
"clearReset": "Tuvali sıfırla",
@ -119,7 +119,7 @@
"edit": "Düzenle",
"undo": "Geri Al",
"redo": "Yeniden yap",
"resetLibrary": "",
"resetLibrary": "Kütüphaneyi sıfırla",
"createNewRoom": "Yeni oda oluştur",
"fullScreen": "Tam ekran",
"darkMode": "Koyu tema",
@ -138,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": "",
"collabStopOverridePrompt": "Oturumu sonlandırmak daha önceki, yerel olarak kaydedilmiş çizimin üzerine kaydedilmesine sebep olacak. Emin misiniz?\n\n(Yerel çiziminizi kaybetmemek için tarayıcı sekmesini kapatabilirsiniz.)",
"errorLoadingLibrary": "Üçüncü taraf kitaplığı yüklerken bir hata oluştu.",
"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": "",
"resetLibrary": ""
"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?"
},
"toolBar": {
"selection": "Seçme",
@ -201,31 +201,31 @@
"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": ""
"shareTitle": "Excalidraw'da canlı ortak calışma oturumuna katıl"
},
"errorDialog": {
"title": "Hata"
},
"helpDialog": {
"blog": "",
"click": "",
"curvedArrow": "",
"curvedLine": "",
"documentation": "",
"drag": "",
"blog": "Blog'umuzu okuyun",
"click": "tıkla",
"curvedArrow": "Eğri ok",
"curvedLine": "Eğri çizgi",
"documentation": "Dokümantasyon",
"drag": "sürükle",
"editor": "",
"github": "",
"howto": "",
"or": "",
"github": "Bir hata mı buldun? Bildir",
"howto": "Rehberlerimizi takip edin",
"or": "veya",
"preventBinding": "",
"shapes": "",
"shortcuts": "",
"textFinish": "",
"textNewLine": "",
"title": "",
"shapes": "Şekiller",
"shortcuts": "Klavye kısayolları",
"textFinish": "(Metin) düzenlemeyi bitir",
"textNewLine": "Yeni satır ekle (metin)",
"title": "Yardım",
"view": "",
"zoomToFit": "",
"zoomToSelection": ""
"zoomToFit": "Tüm öğeleri sığdırmak için yakınlaştır",
"zoomToSelection": "Seçime yakınlaş"
},
"encrypted": {
"tooltip": "Çizimleriniz uçtan-uca şifrelenmiştir, Excalidraw'ın sunucuları bile onları göremez."
@ -240,18 +240,18 @@
"storage": "Depolama",
"title": "İnekler için istatistikler",
"total": "Toplam",
"version": "",
"versionCopy": "",
"versionNotAvailable": "",
"version": "Sürüm",
"versionCopy": "Kopyalamak için tıkla",
"versionNotAvailable": "Sürüm mevcut değil",
"width": "Genişlik"
},
"toast": {
"copyStyles": "",
"copyToClipboard": "",
"copyToClipboardAsPng": "",
"fileSaved": "",
"fileSavedToFilename": "",
"canvas": "",
"selection": ""
"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"
}
}

View File

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

View File

@ -61,7 +61,7 @@
"architect": "朴素",
"artist": "艺术",
"cartoonist": "漫画家",
"fileTitle": "文件标题",
"fileTitle": "",
"colorPicker": "调色盘",
"canvasBackground": "画布背景",
"drawingCanvas": "绘制 Canvas",

View File

@ -61,7 +61,7 @@
"architect": "精確",
"artist": "藝術",
"cartoonist": "卡通",
"fileTitle": "檔案標題",
"fileTitle": "檔案名稱",
"colorPicker": "色彩選擇工具",
"canvasBackground": "Canvas 背景",
"drawingCanvas": "繪圖 canvas",

View File

@ -12,12 +12,14 @@ The change should be grouped under one of the below section and must contain PR
Please add the latest change on the top under the correct section.
-->
## Unreleased
## 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).
- 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.
@ -35,6 +37,24 @@ 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)

View File

@ -28,7 +28,8 @@ If you want to load assets from a different path you can set a variable `window.
[Try here](https://codesandbox.io/s/excalidraw-ehlz3).
### Usage
<details id="usage">
<summary><strong>Usage</strong></summary>
1. If you are using a Web bundler (for instance, Webpack), you can import it as an ES6 module as shown below
@ -163,6 +164,8 @@ export default function App() {
}
```
To view the full example visit :point_down:
[![Edit excalidraw](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/excalidraw-ehlz3?fontsize=14&hidenavigation=1&theme=dark)
2. To use it in a browser directly:
@ -341,6 +344,8 @@ const excalidrawWrapper = document.getElementById("app");
ReactDOM.render(React.createElement(App), excalidrawWrapper);
```
To view the full example visit :point_down:
[![Edit excalidraw-in-browser](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/excalidraw-in-browser-tlqom?fontsize=14&hidenavigation=1&theme=dark)
Since Excalidraw doesn't support server side rendering yet so you will have to make sure the component is rendered once host is mounted.
@ -356,7 +361,10 @@ export default function IndexPage() {
}
```
### Props
</details>
<details id="props">
<summary><strong>Props</strong></summary>
| Name | Type | Default | Description |
| --- | --- | --- | --- |
@ -376,6 +384,7 @@ export default function IndexPage() {
| [`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 |
#### `width`
@ -526,15 +535,22 @@ 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). Default to `window.location.origin`.
If supplied, this URL will be used when user tries to install a library from [libraries.excalidraw.com](https://libraries.excalidraw.com). Default to `window.location.origin`. To install the libraries in the same tab from which it was opened, you need to set `window.name` (to any alphanumeric string) — if it's not set it will open in a new tab.
### `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.
### Extra API's
#### `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.
</details>
<details id="extra-apis">
<summary><strong>Extra API's</strong></summary>
#### `getSceneVersion`
@ -579,6 +595,9 @@ import { getElementsMap } from "@excalidraw/excalidraw";
This function returns an object where each element is mapped to its id.
<details id="restore-utils">
<summary><strong>Restore utilities</strong></summary>
#### `restoreAppState`
**_Signature_**
@ -627,7 +646,7 @@ import { restore } from "@excalidraw/excalidraw";
This function makes sure elements and state is set to appropriate values and set to default value if not present. It is combination of [restoreElements](#restoreElements) and [restoreAppState](#restoreAppState)
**_The below APIs will be available in [next version](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/CHANGELOG.md#unreleased)_**
</details>
<details id="export-utils">
<summary><strong>Export utilities</strong></summary>
@ -716,3 +735,4 @@ This function returns a svg with the exported elements.
| exportWithDarkMode | boolean | false | Indicates whether to export with dark mode |
</details>
</details>

View File

@ -29,6 +29,7 @@ const Excalidraw = (props: ExcalidrawProps) => {
gridModeEnabled,
libraryReturnUrl,
theme,
name,
} = props;
useEffect(() => {
@ -69,6 +70,7 @@ const Excalidraw = (props: ExcalidrawProps) => {
gridModeEnabled={gridModeEnabled}
libraryReturnUrl={libraryReturnUrl}
theme={theme}
name={name}
/>
</IsMobileProvider>
</InitializeApp>

View File

@ -1,6 +1,6 @@
{
"name": "@excalidraw/excalidraw",
"version": "0.4.3",
"version": "0.5.0",
"main": "dist/excalidraw.min.js",
"files": [
"dist/*"
@ -52,13 +52,13 @@
"babel-loader": "8.2.2",
"babel-plugin-transform-class-properties": "6.24.1",
"cross-env": "7.0.3",
"css-loader": "5.1.2",
"css-loader": "5.1.3",
"file-loader": "6.2.0",
"mini-css-extract-plugin": "1.3.9",
"sass-loader": "11.0.1",
"terser-webpack-plugin": "5.1.1",
"ts-loader": "8.0.18",
"webpack": "5.24.3",
"webpack": "5.27.1",
"webpack-bundle-analyzer": "4.4.0",
"webpack-cli": "4.5.0"
},

View File

@ -1497,10 +1497,10 @@ cross-spawn@^7.0.1, cross-spawn@^7.0.3:
shebang-command "^2.0.0"
which "^2.0.1"
css-loader@5.1.2:
version "5.1.2"
resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-5.1.2.tgz#b93dba498ec948b543b49d4fab5017205d4f5c3e"
integrity sha512-T7vTXHSx0KrVEg/xjcl7G01RcVXpcw4OELwDPvkr7izQNny85A84dK3dqrczuEfBcu7Yg7mdTjJLSTibRUoRZg==
css-loader@5.1.3:
version "5.1.3"
resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-5.1.3.tgz#87f6fc96816b20debe3cf682f85c7e56a963d0d1"
integrity sha512-CoPZvyh8sLiGARK3gqczpfdedbM74klGWurF2CsNZ2lhNaXdLIUks+3Mfax3WBeRuHoglU+m7KG/+7gY6G4aag==
dependencies:
camelcase "^6.2.0"
cssesc "^3.0.0"
@ -2663,10 +2663,10 @@ webpack-sources@^2.1.1:
source-list-map "^2.0.1"
source-map "^0.6.1"
webpack@5.24.3:
version "5.24.3"
resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.24.3.tgz#6ec0f5059f8d7c7961075fa553cfce7b7928acb3"
integrity sha512-x7lrWZ7wlWAdyKdML6YPvfVZkhD1ICuIZGODE5SzKJjqI9A4SpqGTjGJTc6CwaHqn19gGaoOR3ONJ46nYsn9rw==
webpack@5.27.1:
version "5.27.1"
resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.27.1.tgz#6808fb6e45e35290cdb8ae43c7a10884839a3079"
integrity sha512-rxIDsPZ3Apl3JcqiemiLmWH+hAq04YeOXqvCxNZOnTp8ZgM9NEPtbu4CaMfMEf9KShnx/Ym8uLGmM6P4XnwCoA==
dependencies:
"@types/eslint-scope" "^3.7.0"
"@types/estree" "^0.0.46"

View File

@ -44,11 +44,11 @@
"babel-loader": "8.2.2",
"babel-plugin-transform-class-properties": "6.24.1",
"cross-env": "7.0.3",
"css-loader": "5.1.2",
"css-loader": "5.1.3",
"file-loader": "6.2.0",
"sass-loader": "11.0.1",
"ts-loader": "8.0.18",
"webpack": "5.24.3",
"webpack": "5.27.1",
"webpack-bundle-analyzer": "4.4.0",
"webpack-cli": "4.5.0"
},

View File

@ -1446,10 +1446,10 @@ cross-spawn@^7.0.1, cross-spawn@^7.0.3:
shebang-command "^2.0.0"
which "^2.0.1"
css-loader@5.1.2:
version "5.1.2"
resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-5.1.2.tgz#b93dba498ec948b543b49d4fab5017205d4f5c3e"
integrity sha512-T7vTXHSx0KrVEg/xjcl7G01RcVXpcw4OELwDPvkr7izQNny85A84dK3dqrczuEfBcu7Yg7mdTjJLSTibRUoRZg==
css-loader@5.1.3:
version "5.1.3"
resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-5.1.3.tgz#87f6fc96816b20debe3cf682f85c7e56a963d0d1"
integrity sha512-CoPZvyh8sLiGARK3gqczpfdedbM74klGWurF2CsNZ2lhNaXdLIUks+3Mfax3WBeRuHoglU+m7KG/+7gY6G4aag==
dependencies:
camelcase "^6.2.0"
cssesc "^3.0.0"
@ -2595,10 +2595,10 @@ webpack-sources@^2.1.1:
source-list-map "^2.0.1"
source-map "^0.6.1"
webpack@5.24.3:
version "5.24.3"
resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.24.3.tgz#6ec0f5059f8d7c7961075fa553cfce7b7928acb3"
integrity sha512-x7lrWZ7wlWAdyKdML6YPvfVZkhD1ICuIZGODE5SzKJjqI9A4SpqGTjGJTc6CwaHqn19gGaoOR3ONJ46nYsn9rw==
webpack@5.27.1:
version "5.27.1"
resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.27.1.tgz#6808fb6e45e35290cdb8ae43c7a10884839a3079"
integrity sha512-rxIDsPZ3Apl3JcqiemiLmWH+hAq04YeOXqvCxNZOnTp8ZgM9NEPtbu4CaMfMEf9KShnx/Ym8uLGmM6P4XnwCoA==
dependencies:
"@types/eslint-scope" "^3.7.0"
"@types/estree" "^0.0.46"

View File

@ -3357,8 +3357,8 @@ Object {
"strokeStyle": "solid",
"strokeWidth": 1,
"type": "rectangle",
"version": 8,
"versionNonce": 1116226695,
"version": 4,
"versionNonce": 453191,
"width": 10,
"x": 10,
"y": 10,
@ -3429,7 +3429,7 @@ Object {
"elements": Array [
Object {
"angle": 0,
"backgroundColor": "transparent",
"backgroundColor": "#fa5252",
"boundElementIds": null,
"fillStyle": "hachure",
"groupIds": Array [],
@ -3452,78 +3452,6 @@ Object {
},
],
},
Object {
"appState": Object {
"editingGroupId": null,
"editingLinearElement": null,
"name": "Untitled-201933152653",
"selectedElementIds": Object {
"id0": true,
},
"viewBackgroundColor": "#ffffff",
},
"elements": Array [
Object {
"angle": 0,
"backgroundColor": "#fa5252",
"boundElementIds": null,
"fillStyle": "hachure",
"groupIds": Array [],
"height": 10,
"id": "id0",
"isDeleted": false,
"opacity": 100,
"roughness": 1,
"seed": 337897,
"strokeColor": "#000000",
"strokeSharpness": "sharp",
"strokeStyle": "solid",
"strokeWidth": 1,
"type": "rectangle",
"version": 5,
"versionNonce": 401146281,
"width": 10,
"x": 10,
"y": 10,
},
],
},
Object {
"appState": Object {
"editingGroupId": null,
"editingLinearElement": null,
"name": "Untitled-201933152653",
"selectedElementIds": Object {
"id0": true,
},
"viewBackgroundColor": "#ffffff",
},
"elements": Array [
Object {
"angle": 0,
"backgroundColor": "#fa5252",
"boundElementIds": null,
"fillStyle": "hachure",
"groupIds": Array [],
"height": 10,
"id": "id0",
"isDeleted": false,
"opacity": 100,
"roughness": 1,
"seed": 337897,
"strokeColor": "#000000",
"strokeSharpness": "sharp",
"strokeStyle": "solid",
"strokeWidth": 1,
"type": "rectangle",
"version": 6,
"versionNonce": 2019559783,
"width": 10,
"x": 10,
"y": 10,
},
],
},
Object {
"appState": Object {
"editingGroupId": null,
@ -3552,8 +3480,8 @@ Object {
"strokeStyle": "solid",
"strokeWidth": 1,
"type": "rectangle",
"version": 8,
"versionNonce": 1116226695,
"version": 4,
"versionNonce": 453191,
"width": 10,
"x": 10,
"y": 10,
@ -14745,7 +14673,7 @@ Object {
"strokeWidth": 2,
"type": "rectangle",
"version": 3,
"versionNonce": 81784553,
"versionNonce": 1505387817,
"width": 20,
"x": 10,
"y": 10,
@ -14764,14 +14692,14 @@ Object {
"isDeleted": false,
"opacity": 60,
"roughness": 2,
"seed": 23633383,
"seed": 238820263,
"strokeColor": "#c92a2a",
"strokeSharpness": "sharp",
"strokeStyle": "dotted",
"strokeWidth": 2,
"type": "rectangle",
"version": 13,
"versionNonce": 915032327,
"version": 9,
"versionNonce": 1604849351,
"width": 20,
"x": 40,
"y": 40,
@ -14934,7 +14862,7 @@ Object {
"opacity": 100,
"roughness": 1,
"seed": 449462985,
"strokeColor": "#000000",
"strokeColor": "#c92a2a",
"strokeSharpness": "sharp",
"strokeStyle": "solid",
"strokeWidth": 1,
@ -14981,6 +14909,42 @@ Object {
"x": 10,
"y": 10,
},
Object {
"angle": 0,
"backgroundColor": "#e64980",
"boundElementIds": null,
"fillStyle": "hachure",
"groupIds": Array [],
"height": 20,
"id": "id1",
"isDeleted": false,
"opacity": 100,
"roughness": 1,
"seed": 449462985,
"strokeColor": "#c92a2a",
"strokeSharpness": "sharp",
"strokeStyle": "solid",
"strokeWidth": 1,
"type": "rectangle",
"version": 4,
"versionNonce": 2019559783,
"width": 20,
"x": 40,
"y": 40,
},
],
},
Object {
"appState": Object {
"editingGroupId": null,
"editingLinearElement": null,
"name": "Untitled-201933152653",
"selectedElementIds": Object {
"id1": true,
},
"viewBackgroundColor": "#ffffff",
},
"elements": Array [
Object {
"angle": 0,
"backgroundColor": "transparent",
@ -14988,6 +14952,29 @@ Object {
"fillStyle": "hachure",
"groupIds": Array [],
"height": 20,
"id": "id0",
"isDeleted": false,
"opacity": 100,
"roughness": 1,
"seed": 337897,
"strokeColor": "#000000",
"strokeSharpness": "sharp",
"strokeStyle": "solid",
"strokeWidth": 1,
"type": "rectangle",
"version": 2,
"versionNonce": 1278240551,
"width": 20,
"x": 10,
"y": 10,
},
Object {
"angle": 0,
"backgroundColor": "#e64980",
"boundElementIds": null,
"fillStyle": "cross-hatch",
"groupIds": Array [],
"height": 20,
"id": "id1",
"isDeleted": false,
"opacity": 100,
@ -15042,9 +15029,9 @@ Object {
},
Object {
"angle": 0,
"backgroundColor": "transparent",
"backgroundColor": "#e64980",
"boundElementIds": null,
"fillStyle": "hachure",
"fillStyle": "cross-hatch",
"groupIds": Array [],
"height": 20,
"id": "id1",
@ -15055,7 +15042,7 @@ Object {
"strokeColor": "#c92a2a",
"strokeSharpness": "sharp",
"strokeStyle": "solid",
"strokeWidth": 1,
"strokeWidth": 2,
"type": "rectangle",
"version": 6,
"versionNonce": 1116226695,
@ -15065,65 +15052,6 @@ Object {
},
],
},
Object {
"appState": Object {
"editingGroupId": null,
"editingLinearElement": null,
"name": "Untitled-201933152653",
"selectedElementIds": Object {
"id1": true,
},
"viewBackgroundColor": "#ffffff",
},
"elements": Array [
Object {
"angle": 0,
"backgroundColor": "transparent",
"boundElementIds": null,
"fillStyle": "hachure",
"groupIds": Array [],
"height": 20,
"id": "id0",
"isDeleted": false,
"opacity": 100,
"roughness": 1,
"seed": 337897,
"strokeColor": "#000000",
"strokeSharpness": "sharp",
"strokeStyle": "solid",
"strokeWidth": 1,
"type": "rectangle",
"version": 2,
"versionNonce": 1278240551,
"width": 20,
"x": 10,
"y": 10,
},
Object {
"angle": 0,
"backgroundColor": "#e64980",
"boundElementIds": null,
"fillStyle": "hachure",
"groupIds": Array [],
"height": 20,
"id": "id1",
"isDeleted": false,
"opacity": 100,
"roughness": 1,
"seed": 449462985,
"strokeColor": "#c92a2a",
"strokeSharpness": "sharp",
"strokeStyle": "solid",
"strokeWidth": 1,
"type": "rectangle",
"version": 8,
"versionNonce": 238820263,
"width": 20,
"x": 40,
"y": 40,
},
],
},
Object {
"appState": Object {
"editingGroupId": null,
@ -15172,10 +15100,69 @@ Object {
"seed": 449462985,
"strokeColor": "#c92a2a",
"strokeSharpness": "sharp",
"strokeStyle": "dotted",
"strokeWidth": 2,
"type": "rectangle",
"version": 7,
"versionNonce": 1014066025,
"width": 20,
"x": 40,
"y": 40,
},
],
},
Object {
"appState": Object {
"editingGroupId": null,
"editingLinearElement": null,
"name": "Untitled-201933152653",
"selectedElementIds": Object {
"id1": true,
},
"viewBackgroundColor": "#ffffff",
},
"elements": Array [
Object {
"angle": 0,
"backgroundColor": "transparent",
"boundElementIds": null,
"fillStyle": "hachure",
"groupIds": Array [],
"height": 20,
"id": "id0",
"isDeleted": false,
"opacity": 100,
"roughness": 1,
"seed": 337897,
"strokeColor": "#000000",
"strokeSharpness": "sharp",
"strokeStyle": "solid",
"strokeWidth": 1,
"type": "rectangle",
"version": 9,
"version": 2,
"versionNonce": 1278240551,
"width": 20,
"x": 10,
"y": 10,
},
Object {
"angle": 0,
"backgroundColor": "#e64980",
"boundElementIds": null,
"fillStyle": "cross-hatch",
"groupIds": Array [],
"height": 20,
"id": "id1",
"isDeleted": false,
"opacity": 100,
"roughness": 2,
"seed": 238820263,
"strokeColor": "#c92a2a",
"strokeSharpness": "sharp",
"strokeStyle": "dotted",
"strokeWidth": 2,
"type": "rectangle",
"version": 8,
"versionNonce": 400692809,
"width": 20,
"x": 40,
@ -15183,183 +15170,6 @@ Object {
},
],
},
Object {
"appState": Object {
"editingGroupId": null,
"editingLinearElement": null,
"name": "Untitled-201933152653",
"selectedElementIds": Object {
"id1": true,
},
"viewBackgroundColor": "#ffffff",
},
"elements": Array [
Object {
"angle": 0,
"backgroundColor": "transparent",
"boundElementIds": null,
"fillStyle": "hachure",
"groupIds": Array [],
"height": 20,
"id": "id0",
"isDeleted": false,
"opacity": 100,
"roughness": 1,
"seed": 337897,
"strokeColor": "#000000",
"strokeSharpness": "sharp",
"strokeStyle": "solid",
"strokeWidth": 1,
"type": "rectangle",
"version": 2,
"versionNonce": 1278240551,
"width": 20,
"x": 10,
"y": 10,
},
Object {
"angle": 0,
"backgroundColor": "#e64980",
"boundElementIds": null,
"fillStyle": "cross-hatch",
"groupIds": Array [],
"height": 20,
"id": "id1",
"isDeleted": false,
"opacity": 100,
"roughness": 1,
"seed": 449462985,
"strokeColor": "#c92a2a",
"strokeSharpness": "sharp",
"strokeStyle": "solid",
"strokeWidth": 2,
"type": "rectangle",
"version": 10,
"versionNonce": 1604849351,
"width": 20,
"x": 40,
"y": 40,
},
],
},
Object {
"appState": Object {
"editingGroupId": null,
"editingLinearElement": null,
"name": "Untitled-201933152653",
"selectedElementIds": Object {
"id1": true,
},
"viewBackgroundColor": "#ffffff",
},
"elements": Array [
Object {
"angle": 0,
"backgroundColor": "transparent",
"boundElementIds": null,
"fillStyle": "hachure",
"groupIds": Array [],
"height": 20,
"id": "id0",
"isDeleted": false,
"opacity": 100,
"roughness": 1,
"seed": 337897,
"strokeColor": "#000000",
"strokeSharpness": "sharp",
"strokeStyle": "solid",
"strokeWidth": 1,
"type": "rectangle",
"version": 2,
"versionNonce": 1278240551,
"width": 20,
"x": 10,
"y": 10,
},
Object {
"angle": 0,
"backgroundColor": "#e64980",
"boundElementIds": null,
"fillStyle": "cross-hatch",
"groupIds": Array [],
"height": 20,
"id": "id1",
"isDeleted": false,
"opacity": 100,
"roughness": 1,
"seed": 449462985,
"strokeColor": "#c92a2a",
"strokeSharpness": "sharp",
"strokeStyle": "dotted",
"strokeWidth": 2,
"type": "rectangle",
"version": 11,
"versionNonce": 1505387817,
"width": 20,
"x": 40,
"y": 40,
},
],
},
Object {
"appState": Object {
"editingGroupId": null,
"editingLinearElement": null,
"name": "Untitled-201933152653",
"selectedElementIds": Object {
"id1": true,
},
"viewBackgroundColor": "#ffffff",
},
"elements": Array [
Object {
"angle": 0,
"backgroundColor": "transparent",
"boundElementIds": null,
"fillStyle": "hachure",
"groupIds": Array [],
"height": 20,
"id": "id0",
"isDeleted": false,
"opacity": 100,
"roughness": 1,
"seed": 337897,
"strokeColor": "#000000",
"strokeSharpness": "sharp",
"strokeStyle": "solid",
"strokeWidth": 1,
"type": "rectangle",
"version": 2,
"versionNonce": 1278240551,
"width": 20,
"x": 10,
"y": 10,
},
Object {
"angle": 0,
"backgroundColor": "#e64980",
"boundElementIds": null,
"fillStyle": "cross-hatch",
"groupIds": Array [],
"height": 20,
"id": "id1",
"isDeleted": false,
"opacity": 100,
"roughness": 2,
"seed": 23633383,
"strokeColor": "#c92a2a",
"strokeSharpness": "sharp",
"strokeStyle": "dotted",
"strokeWidth": 2,
"type": "rectangle",
"version": 12,
"versionNonce": 493213705,
"width": 20,
"x": 40,
"y": 40,
},
],
},
Object {
"appState": Object {
"editingGroupId": null,
@ -15405,14 +15215,14 @@ Object {
"isDeleted": false,
"opacity": 60,
"roughness": 2,
"seed": 23633383,
"seed": 238820263,
"strokeColor": "#c92a2a",
"strokeSharpness": "sharp",
"strokeStyle": "dotted",
"strokeWidth": 2,
"type": "rectangle",
"version": 13,
"versionNonce": 915032327,
"version": 9,
"versionNonce": 1604849351,
"width": 20,
"x": 40,
"y": 40,
@ -15448,7 +15258,7 @@ Object {
"strokeWidth": 2,
"type": "rectangle",
"version": 3,
"versionNonce": 81784553,
"versionNonce": 1505387817,
"width": 20,
"x": 10,
"y": 10,
@ -15464,14 +15274,14 @@ Object {
"isDeleted": false,
"opacity": 60,
"roughness": 2,
"seed": 23633383,
"seed": 238820263,
"strokeColor": "#c92a2a",
"strokeSharpness": "sharp",
"strokeStyle": "dotted",
"strokeWidth": 2,
"type": "rectangle",
"version": 13,
"versionNonce": 915032327,
"version": 9,
"versionNonce": 1604849351,
"width": 20,
"x": 40,
"y": 40,
@ -17069,8 +16879,8 @@ Object {
"strokeStyle": "solid",
"strokeWidth": 1,
"type": "rectangle",
"version": 5,
"versionNonce": 401146281,
"version": 3,
"versionNonce": 449462985,
"width": 10,
"x": 0,
"y": 0,
@ -17141,7 +16951,7 @@ Object {
"elements": Array [
Object {
"angle": 0,
"backgroundColor": "transparent",
"backgroundColor": "#fa5252",
"boundElementIds": null,
"fillStyle": "hachure",
"groupIds": Array [],
@ -17164,42 +16974,6 @@ Object {
},
],
},
Object {
"appState": Object {
"editingGroupId": null,
"editingLinearElement": null,
"name": "Untitled-201933152653",
"selectedElementIds": Object {
"id0": true,
},
"viewBackgroundColor": "#ffffff",
},
"elements": Array [
Object {
"angle": 0,
"backgroundColor": "#fa5252",
"boundElementIds": null,
"fillStyle": "hachure",
"groupIds": Array [],
"height": 10,
"id": "id0",
"isDeleted": false,
"opacity": 100,
"roughness": 1,
"seed": 337897,
"strokeColor": "#000000",
"strokeSharpness": "sharp",
"strokeStyle": "solid",
"strokeWidth": 1,
"type": "rectangle",
"version": 5,
"versionNonce": 401146281,
"width": 10,
"x": 0,
"y": 0,
},
],
},
],
}
`;
@ -20607,7 +20381,7 @@ Object {
"strokeWidth": 1,
"type": "rectangle",
"version": 6,
"versionNonce": 1006504105,
"versionNonce": 760410951,
"width": 20,
"x": 10,
"y": -10,
@ -20633,7 +20407,7 @@ Object {
"strokeWidth": 1,
"type": "rectangle",
"version": 6,
"versionNonce": 289600103,
"versionNonce": 1006504105,
"width": 30,
"x": 40,
"y": 0,
@ -20676,8 +20450,8 @@ Object {
"strokeStyle": "solid",
"strokeWidth": 1,
"type": "arrow",
"version": 11,
"versionNonce": 1315507081,
"version": 9,
"versionNonce": 81784553,
"width": 60,
"x": 130,
"y": 10,

View File

@ -3,6 +3,7 @@ import { render, waitFor } from "./test-utils";
import ExcalidrawApp from "../excalidraw-app";
import { API } from "./helpers/api";
import { getDefaultAppState } from "../appState";
import { EXPORT_DATA_TYPES } from "../constants";
const { h } = window;
@ -29,7 +30,7 @@ describe("appState", () => {
new Blob(
[
JSON.stringify({
type: "excalidraw",
type: EXPORT_DATA_TYPES.excalidraw,
appState: {
viewBackgroundColor: "#000",
},

View File

@ -3,6 +3,7 @@ import { fireEvent, GlobalTestState, render } from "./test-utils";
import Excalidraw from "../packages/excalidraw/index";
import { queryByText, queryByTestId } from "@testing-library/react";
import { GRID_SIZE } from "../constants";
import { t } from "../i18n";
const { h } = window;
@ -104,4 +105,29 @@ describe("<Excalidraw/>", () => {
expect(queryByTestId(container, "toggle-dark-mode")).toBe(null);
});
});
describe("Test name prop", () => {
it('should allow editing name when the name prop is "undefined"', async () => {
const { container } = await render(<Excalidraw />);
fireEvent.click(queryByTestId(container, "export-button")!);
const textInput: HTMLInputElement | null = document.querySelector(
".ExportDialog__name .TextInput",
);
expect(textInput?.value).toContain(`${t("labels.untitled")}`);
expect(textInput?.nodeName).toBe("INPUT");
});
it('should set the name and not allow editing when the name prop is present"', async () => {
const name = "test";
const { container } = await render(<Excalidraw name={name} />);
await fireEvent.click(queryByTestId(container, "export-button")!);
const textInput = document.querySelector(
".ExportDialog__name .TextInput--readonly",
);
expect(textInput?.textContent).toEqual(name);
expect(textInput?.nodeName).toBe("SPAN");
});
});
});

View File

@ -6,6 +6,7 @@ import { API } from "./helpers/api";
import { getDefaultAppState } from "../appState";
import { waitFor } from "@testing-library/react";
import { createUndoAction, createRedoAction } from "../actions/actionHistory";
import { EXPORT_DATA_TYPES } from "../constants";
const { h } = window;
@ -76,7 +77,7 @@ describe("history", () => {
new Blob(
[
JSON.stringify({
type: "excalidraw",
type: EXPORT_DATA_TYPES.excalidraw,
appState: {
...getDefaultAppState(),
viewBackgroundColor: "#000",

View File

@ -607,7 +607,7 @@ describe("regression tests", () => {
it("updates fontSize & fontFamily appState", () => {
UI.clickTool("text");
expect(h.state.currentItemFontFamily).toEqual(1); // Virgil
fireEvent.click(screen.getByText(/code/i));
fireEvent.click(screen.getByTitle(/code/i));
expect(h.state.currentItemFontFamily).toEqual(3); // Cascadia
});

View File

@ -187,6 +187,7 @@ export interface ExcalidrawProps {
gridModeEnabled?: boolean;
libraryReturnUrl?: string;
theme?: "dark" | "light";
name?: string;
}
export type SceneData = {

View File

@ -131,9 +131,7 @@ export const debounce = <T extends any[]>(
};
ret.flush = () => {
clearTimeout(handle);
if (lastArgs) {
fn(...lastArgs);
}
fn(...(lastArgs || []));
};
ret.cancel = () => {
clearTimeout(handle);

1697
yarn.lock

File diff suppressed because it is too large Load Diff