Compare commits

..

5 Commits

Author SHA1 Message Date
pomdtr
8984d8f19a don't clean export options on load 2022-06-29 10:23:48 +00:00
Achille Lacoin
b80706cd4a Update appState.ts 2022-06-28 17:53:01 +02:00
pomdtr
cf34cbdd30 fix export.test.tsx 2022-06-28 08:16:14 +00:00
pomdtr
6ead3ff839 fix export snapshot 2022-06-28 08:12:38 +00:00
pomdtr
d7f0d4ee21 [feat] serialize export options when embedding scene in an image 2022-06-27 20:31:05 +00:00
122 changed files with 5224 additions and 5096 deletions

View File

@@ -11,12 +11,3 @@ REACT_APP_WS_SERVER_URL=http://localhost:3002
REACT_APP_PORTAL_URL=
REACT_APP_FIREBASE_CONFIG='{"apiKey":"AIzaSyCMkxA60XIW8KbqMYL7edC4qT5l4qHX2h8","authDomain":"excalidraw-oss-dev.firebaseapp.com","projectId":"excalidraw-oss-dev","storageBucket":"excalidraw-oss-dev.appspot.com","messagingSenderId":"664559512677","appId":"1:664559512677:web:a385181f2928d328a7aa8c"}'
# put these in your .env.local, or make sure you don't commit!
# must be lowercase `true` when turned on
#
# whether to enable Service Workers in development
REACT_APP_DEV_ENABLE_SW=
# whether to disable live reload / HMR. Usuaully what you want to do when
# debugging Service Workers.
REACT_APP_DEV_DISABLE_LIVE_RELOAD=

37
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,37 @@
version: 2
updates:
- package-ecosystem: npm
directory: /
schedule:
interval: weekly
day: sunday
time: "01:00"
reviewers:
- lipis
assignees:
- lipis
open-pull-requests-limit: 20
- package-ecosystem: npm
directory: /src/packages/excalidraw/
schedule:
interval: weekly
day: sunday
time: "01:00"
reviewers:
- ad1992
assignees:
- ad1992
open-pull-requests-limit: 20
- package-ecosystem: npm
directory: /src/packages/utils/
schedule:
interval: weekly
day: sunday
time: "01:00"
reviewers:
- ad1992
assignees:
- ad1992
open-pull-requests-limit: 20

View File

@@ -1,4 +1,4 @@
name: Auto release excalidraw next
name: Auto release @excalidraw/excalidraw-next
on:
push:
branches:

View File

@@ -1,4 +1,4 @@
name: Auto release excalidraw preview
name: Auto release preview @excalidraw/excalidraw-preview
on:
issue_comment:
types: [created, edited]

View File

@@ -34,7 +34,7 @@ Last but not least, we're thankful to these companies for offering their service
## Who's integrating Excalidraw
[Google Cloud](https://googlecloudcheatsheet.withgoogle.com/architecture) • [Meta](https://meta.com/) • [CodeSandbox](https://codesandbox.io/) • [Obsidian Excalidraw](https://github.com/zsviczian/obsidian-excalidraw-plugin) • [Replit](https://replit.com/) • [Slite](https://slite.com/) • [Notion](https://notion.so/) • [HackerRank](https://www.hackerrank.com/) • [VS Code](https://marketplace.visualstudio.com/items?itemName=pomdtr.excalidraw-editor)
[Google Cloud](https://googlecloudcheatsheet.withgoogle.com/architecture) • [Meta](https://meta.com/) • [CodeSandbox](https://codesandbox.io/) • [Obsidian Excalidraw](https://github.com/zsviczian/obsidian-excalidraw-plugin) • [Replit](https://replit.com/) • [Slite](https://slite.com/) • [Notion](https://notion.so/) • [HackerRank](https://www.hackerrank.com/) •
## Documentation

View File

@@ -23,7 +23,7 @@
"@sentry/integrations": "6.2.5",
"@testing-library/jest-dom": "5.16.2",
"@testing-library/react": "12.1.5",
"@tldraw/vec": "1.7.1",
"@tldraw/vec": "1.4.3",
"@types/jest": "27.4.0",
"@types/pica": "5.1.3",
"@types/react": "17.0.39",
@@ -33,7 +33,7 @@
"clsx": "1.1.1",
"fake-indexeddb": "3.1.7",
"firebase": "8.3.3",
"i18next-browser-languagedetector": "6.1.4",
"i18next-browser-languagedetector": "6.1.2",
"idb-keyval": "6.0.3",
"image-blob-reduce": "3.0.1",
"jotai": "1.6.4",
@@ -59,11 +59,11 @@
"@excalidraw/eslint-config": "1.0.0",
"@excalidraw/prettier-config": "1.0.2",
"@types/chai": "4.3.0",
"@types/lodash.throttle": "4.1.7",
"@types/lodash.throttle": "4.1.6",
"@types/pako": "1.0.3",
"@types/resize-observer-browser": "0.1.7",
"@types/resize-observer-browser": "0.1.6",
"chai": "4.3.6",
"dotenv": "16.0.1",
"dotenv": "10.0.0",
"eslint-config-prettier": "8.5.0",
"eslint-plugin-prettier": "3.3.1",
"husky": "7.0.4",
@@ -71,7 +71,7 @@
"lint-staged": "12.3.7",
"pepjs": "0.5.3",
"prettier": "2.6.2",
"rewire": "6.0.0"
"rewire": "5.0.0"
},
"resolutions": {
"@typescript-eslint/typescript-estree": "5.10.2"
@@ -94,8 +94,7 @@
"build:app:docker": "REACT_APP_DISABLE_SENTRY=true react-scripts build",
"build:app": "REACT_APP_GIT_SHA=$VERCEL_GIT_COMMIT_SHA react-scripts build",
"build:version": "node ./scripts/build-version.js",
"build:prebuild": "node ./scripts/prebuild.js",
"build": "yarn build:prebuild && yarn build:app && yarn build:version",
"build": "yarn build:app && yarn build:version",
"eject": "react-scripts eject",
"fix:code": "yarn test:code --fix",
"fix:other": "yarn prettier --write",
@@ -113,8 +112,6 @@
"test:typecheck": "tsc",
"test:update": "yarn test:app --updateSnapshot --watchAll=false",
"test": "yarn test:app",
"autorelease": "node scripts/autorelease.js",
"prerelease": "node scripts/prerelease.js",
"release": "node scripts/release.js"
"autorelease": "node scripts/autorelease.js"
}
}

View File

@@ -98,22 +98,6 @@
/>
<link rel="stylesheet" href="fonts.css" type="text/css" />
<% if (process.env.REACT_APP_DEV_DISABLE_LIVE_RELOAD === "true") { %>
<script>
{
const _WebSocket = window.WebSocket;
window.WebSocket = function (url) {
if (/ws:\/\/localhost:.+?\/sockjs-node/.test(url)) {
console.info(
"[!!!] Live reload is disabled via process.env.REACT_APP_DEV_DISABLE_LIVE_RELOAD [!!!]",
);
} else {
return new _WebSocket(url);
}
};
}
</script>
<% } %>
<script>
window.EXCALIDRAW_ASSET_PATH = "/";
// setting this so that libraries installation reuses this window tab.

View File

@@ -5,25 +5,22 @@ const core = require("@actions/core");
const excalidrawDir = `${__dirname}/../src/packages/excalidraw`;
const excalidrawPackage = `${excalidrawDir}/package.json`;
const pkg = require(excalidrawPackage);
const isPreview = process.argv.slice(2)[0] === "preview";
const getShortCommitHash = () => {
return execSync("git rev-parse --short HEAD").toString().trim();
};
const publish = () => {
const tag = isPreview ? "preview" : "next";
try {
execSync(`yarn --frozen-lockfile`);
execSync(`yarn --frozen-lockfile`, { cwd: excalidrawDir });
execSync(`yarn run build:umd`, { cwd: excalidrawDir });
execSync(`yarn --cwd ${excalidrawDir} publish --tag ${tag}`);
console.info(`Published ${pkg.name}@${tag}🎉`);
execSync(`yarn --cwd ${excalidrawDir} publish`);
console.info("Published 🎉");
core.setOutput(
"result",
`**Preview version has been shipped** :rocket:
You can use [@excalidraw/excalidraw@${pkg.version}](https://www.npmjs.com/package/@excalidraw/excalidraw/v/${pkg.version}) for testing!`,
You can use [@excalidraw/excalidraw-preview@${pkg.version}](https://www.npmjs.com/package/@excalidraw/excalidraw-preview/v/${pkg.version}) for testing!`,
);
} catch (error) {
core.setOutput("result", "package couldn't be published :warning:!");
@@ -54,19 +51,27 @@ exec(`git diff --name-only HEAD^ HEAD`, async (error, stdout, stderr) => {
}
// update package.json
pkg.name = "@excalidraw/excalidraw-next";
let version = `${pkg.version}-${getShortCommitHash()}`;
// update readme
let data = fs.readFileSync(`${excalidrawDir}/README_NEXT.md`, "utf8");
const isPreview = process.argv.slice(2)[0] === "preview";
if (isPreview) {
// use pullNumber-commithash as the version for preview
const pullRequestNumber = process.argv.slice(3)[0];
version = `${pkg.version}-${pullRequestNumber}-${getShortCommitHash()}`;
// replace "excalidraw-next" with "excalidraw-preview"
pkg.name = "@excalidraw/excalidraw-preview";
data = data.replace(/excalidraw-next/g, "excalidraw-preview");
data = data.trim();
}
pkg.version = version;
fs.writeFileSync(excalidrawPackage, JSON.stringify(pkg, null, 2), "utf8");
fs.writeFileSync(`${excalidrawDir}/README.md`, data, "utf8");
console.info("Publish in progress...");
publish();
});

View File

@@ -36,7 +36,6 @@ const crowdinMap = {
"ru-RU": "en-ru",
"si-LK": "en-silk",
"sk-SK": "en-sk",
"sl-SI": "en-sl",
"sv-SE": "en-sv",
"ta-IN": "en-ta",
"tr-TR": "en-tr",
@@ -48,8 +47,6 @@ const crowdinMap = {
"lv-LV": "en-lv",
"cs-CZ": "en-cs",
"kk-KZ": "en-kk",
"vi-vn": "en-vi",
"mr-in": "en-mr",
};
const flags = {
@@ -89,7 +86,6 @@ const flags = {
"ru-RU": "🇷🇺",
"si-LK": "🇱🇰",
"sk-SK": "🇸🇰",
"sl-SI": "🇸🇮",
"sv-SE": "🇸🇪",
"ta-IN": "🇮🇳",
"tr-TR": "🇹🇷",
@@ -97,9 +93,6 @@ const flags = {
"zh-CN": "🇨🇳",
"zh-HK": "🇭🇰",
"zh-TW": "🇹🇼",
"eu-ES": "🇪🇦",
"vi-VN": "🇻🇳",
"mr-IN": "🇮🇳",
};
const languages = {
@@ -140,7 +133,6 @@ const languages = {
"ru-RU": "Русский",
"si-LK": "සිංහල",
"sk-SK": "Slovenčina",
"sl-SI": "Slovenščina",
"sv-SE": "Svenska",
"ta-IN": "Tamil",
"tr-TR": "Türkçe",
@@ -148,8 +140,6 @@ const languages = {
"zh-CN": "简体中文",
"zh-HK": "繁體中文 (香港)",
"zh-TW": "繁體中文",
"vi-VN": "Tiếng Việt",
"mr-IN": "मराठी",
};
const percentages = fs.readFileSync(

View File

@@ -1,20 +0,0 @@
const fs = require("fs");
// for development purposes we want to have the service-worker.js file
// accessible from the public folder. On build though, we need to compile it
// and CRA expects that file to be in src/ folder.
const moveServiceWorkerScript = () => {
const oldPath = "./public/service-worker.js";
const newPath = "./src/service-worker.js";
fs.rename(oldPath, newPath, (error) => {
if (error) {
throw error;
}
console.info("public/service-worker.js moved to src/");
});
};
// -----------------------------------------------------------------------------
moveServiceWorkerScript();

View File

@@ -1,37 +0,0 @@
const fs = require("fs");
const util = require("util");
const exec = util.promisify(require("child_process").exec);
const updateChangelog = require("./updateChangelog");
const excalidrawDir = `${__dirname}/../src/packages/excalidraw`;
const excalidrawPackage = `${excalidrawDir}/package.json`;
const updatePackageVersion = (nextVersion) => {
const pkg = require(excalidrawPackage);
pkg.version = nextVersion;
const content = `${JSON.stringify(pkg, null, 2)}\n`;
fs.writeFileSync(excalidrawPackage, content, "utf-8");
};
const prerelease = async (nextVersion) => {
try {
await updateChangelog(nextVersion);
updatePackageVersion(nextVersion);
await exec(`git add -u`);
await exec(
`git commit -m "docs: release @excalidraw/excalidraw@${nextVersion} 🎉"`,
);
console.info("Done!");
} catch (error) {
console.error(error);
process.exit(1);
}
};
const nextVersion = process.argv.slice(2)[0];
if (!nextVersion) {
console.error("Pass the next version to release!");
process.exit(1);
}
prerelease(nextVersion);

View File

@@ -1,44 +1,39 @@
const fs = require("fs");
const { execSync } = require("child_process");
const util = require("util");
const exec = util.promisify(require("child_process").exec);
const updateReadme = require("./updateReadme");
const updateChangelog = require("./updateChangelog");
const excalidrawDir = `${__dirname}/../src/packages/excalidraw`;
const excalidrawPackage = `${excalidrawDir}/package.json`;
const pkg = require(excalidrawPackage);
const originalReadMe = fs.readFileSync(`${excalidrawDir}/README.md`, "utf8");
const updateReadme = () => {
const excalidrawIndex = originalReadMe.indexOf("### Excalidraw");
// remove note for stable readme
const data = originalReadMe.slice(excalidrawIndex);
// update readme
fs.writeFileSync(`${excalidrawDir}/README.md`, data, "utf8");
const updatePackageVersion = (nextVersion) => {
const pkg = require(excalidrawPackage);
pkg.version = nextVersion;
const content = `${JSON.stringify(pkg, null, 2)}\n`;
fs.writeFileSync(excalidrawPackage, content, "utf-8");
};
const publish = () => {
const release = async (nextVersion) => {
try {
execSync(`yarn --frozen-lockfile`);
execSync(`yarn --frozen-lockfile`, { cwd: excalidrawDir });
execSync(`yarn run build:umd`, { cwd: excalidrawDir });
execSync(`yarn --cwd ${excalidrawDir} publish`);
updateReadme();
await updateChangelog(nextVersion);
updatePackageVersion(nextVersion);
await exec(`git add -u`);
await exec(
`git commit -m "docs: release @excalidraw/excalidraw@${nextVersion} 🎉"`,
);
/* eslint-disable no-console */
console.log("Done!");
} catch (error) {
console.error(error);
process.exit(1);
}
};
const release = () => {
updateReadme();
console.info("Note for stable readme removed");
publish();
console.info(`Published ${pkg.version}!`);
// revert readme after release
fs.writeFileSync(`${excalidrawDir}/README.md`, originalReadMe, "utf8");
console.info("Readme reverted");
};
release();
const nextVersion = process.argv.slice(2)[0];
if (!nextVersion) {
console.error("Pass the next version to release!");
process.exit(1);
}
release(nextVersion);

27
scripts/updateReadme.js Normal file
View File

@@ -0,0 +1,27 @@
const fs = require("fs");
const updateReadme = () => {
const excalidrawDir = `${__dirname}/../src/packages/excalidraw`;
let data = fs.readFileSync(`${excalidrawDir}/README_NEXT.md`, "utf8");
// remove note for unstable release
data = data.replace(
/<!-- unstable-readme-start-->[\s\S]*?<!-- unstable-readme-end-->/,
"",
);
// replace "excalidraw-next" with "excalidraw"
data = data.replace(/excalidraw-next/g, "excalidraw");
data = data.trim();
const demoIndex = data.indexOf("### Demo");
const excalidrawNextNote =
"#### Note\n\n**If you don't want to wait for the next stable release and try out the unreleased changes you can use [@excalidraw/excalidraw-next](https://www.npmjs.com/package/@excalidraw/excalidraw-next).**\n\n";
// Add excalidraw next note to try out for unreleased changes
data = data.slice(0, demoIndex) + excalidrawNextNote + data.slice(demoIndex);
// update readme
fs.writeFileSync(`${excalidrawDir}/README.md`, data, "utf8");
};
module.exports = updateReadme;

View File

@@ -42,7 +42,7 @@ export const actionAddToLibrary = register({
commitToHistory: false,
appState: {
...appState,
toast: { message: t("toast.addedToLibrary") },
toastMessage: t("toast.addedToLibrary"),
},
};
})

View File

@@ -107,16 +107,14 @@ export const actionCopyAsPng = register({
return {
appState: {
...appState,
toast: {
message: t("toast.copyToClipboardAsPng", {
exportSelection: selectedElements.length
? t("toast.selection")
: t("toast.canvas"),
exportColorScheme: appState.exportWithDarkMode
? t("buttons.darkMode")
: t("buttons.lightMode"),
}),
},
toastMessage: t("toast.copyToClipboardAsPng", {
exportSelection: selectedElements.length
? t("toast.selection")
: t("toast.canvas"),
exportColorScheme: appState.exportWithDarkMode
? t("buttons.darkMode")
: t("buttons.lightMode"),
}),
},
commitToHistory: false,
};

View File

@@ -128,15 +128,12 @@ const duplicateElements = (
{
...appState,
selectedGroupIds: {},
selectedElementIds: newElements.reduce(
(acc: Record<ExcalidrawElement["id"], true>, element) => {
if (!isBoundToContainer(element)) {
acc[element.id] = true;
}
return acc;
},
{},
),
selectedElementIds: newElements.reduce((acc, element) => {
if (!isBoundToContainer(element)) {
acc[element.id] = true;
}
return acc;
}, {} as any),
},
getNonDeletedElements(finalElements),
),

View File

@@ -144,15 +144,13 @@ export const actionSaveToActiveFile = register({
appState: {
...appState,
fileHandle,
toast: fileHandleExists
? {
message: fileHandle?.name
? t("toast.fileSavedToFilename").replace(
"{filename}",
`"${fileHandle.name}"`,
)
: t("toast.fileSaved"),
}
toastMessage: fileHandleExists
? fileHandle?.name
? t("toast.fileSavedToFilename").replace(
"{filename}",
`"${fileHandle.name}"`,
)
: t("toast.fileSaved")
: null,
},
};

View File

@@ -2,7 +2,6 @@ import { KEYS } from "../keys";
import { register } from "./register";
import { selectGroupsForSelectedElements } from "../groups";
import { getNonDeletedElements, isTextElement } from "../element";
import { ExcalidrawElement } from "../element/types";
export const actionSelectAll = register({
name: "selectAll",
@@ -16,19 +15,16 @@ export const actionSelectAll = register({
{
...appState,
editingGroupId: null,
selectedElementIds: elements.reduce(
(map: Record<ExcalidrawElement["id"], true>, element) => {
if (
!element.isDeleted &&
!(isTextElement(element) && element.containerId) &&
!element.locked
) {
map[element.id] = true;
}
return map;
},
{},
),
selectedElementIds: elements.reduce((map, element) => {
if (
!element.isDeleted &&
!(isTextElement(element) && element.containerId) &&
element.locked === false
) {
map[element.id] = true;
}
return map;
}, {} as any),
},
getNonDeletedElements(elements),
),

View File

@@ -36,7 +36,7 @@ export const actionCopyStyles = register({
return {
appState: {
...appState,
toast: { message: t("toast.copyStyles") },
toastMessage: t("toast.copyStyles"),
},
commitToHistory: false,
};

View File

@@ -81,7 +81,7 @@ export const getDefaultAppState = (): Omit<
showStats: false,
startBoundElement: null,
suggestedBindings: [],
toast: null,
toastMessage: null,
viewBackgroundColor: oc.white,
zenModeEnabled: false,
zoom: {
@@ -101,90 +101,228 @@ const APP_STATE_STORAGE_CONF = (<
Values extends {
/** whether to keep when storing to browser storage (localStorage/IDB) */
browser: boolean;
/** whether to keep when exporting to file/database */
export: boolean;
/** whether to keep when exporting to a text file */
text: boolean;
/** whether to keep when exporting to an image file */
image: boolean;
/** server (shareLink/collab/...) */
server: boolean;
},
T extends Record<keyof AppState, Values>,
>(config: { [K in keyof T]: K extends keyof AppState ? T[K] : never }) =>
config)({
theme: { browser: true, export: false, server: false },
collaborators: { browser: false, export: false, server: false },
currentChartType: { browser: true, export: false, server: false },
currentItemBackgroundColor: { browser: true, export: false, server: false },
currentItemEndArrowhead: { browser: true, export: false, server: false },
currentItemFillStyle: { browser: true, export: false, server: false },
currentItemFontFamily: { browser: true, export: false, server: false },
currentItemFontSize: { browser: true, export: false, server: false },
currentItemLinearStrokeSharpness: {
theme: { browser: true, text: false, image: false, server: false },
collaborators: { browser: false, text: false, image: false, server: false },
currentChartType: { browser: true, text: false, image: false, server: false },
currentItemBackgroundColor: {
browser: true,
export: false,
text: false,
image: false,
server: false,
},
currentItemEndArrowhead: {
browser: true,
text: false,
image: false,
server: false,
},
currentItemFillStyle: {
browser: true,
text: false,
image: false,
server: false,
},
currentItemFontFamily: {
browser: true,
text: false,
image: false,
server: false,
},
currentItemFontSize: {
browser: true,
text: false,
image: false,
server: false,
},
currentItemLinearStrokeSharpness: {
browser: true,
text: false,
image: false,
server: false,
},
currentItemOpacity: {
browser: true,
text: false,
image: false,
server: false,
},
currentItemRoughness: {
browser: true,
text: false,
image: false,
server: false,
},
currentItemStartArrowhead: {
browser: true,
text: false,
image: false,
server: false,
},
currentItemStrokeColor: {
browser: true,
text: false,
image: false,
server: false,
},
currentItemStrokeSharpness: {
browser: true,
text: false,
image: false,
server: false,
},
currentItemStrokeStyle: {
browser: true,
text: false,
image: false,
server: false,
},
currentItemStrokeWidth: {
browser: true,
text: false,
image: false,
server: false,
},
currentItemTextAlign: {
browser: true,
text: false,
image: false,
server: false,
},
cursorButton: { browser: true, text: false, image: false, server: false },
draggingElement: { browser: false, text: false, image: false, server: false },
editingElement: { browser: false, text: false, image: false, server: false },
editingGroupId: { browser: true, text: false, image: false, server: false },
editingLinearElement: {
browser: false,
text: false,
image: false,
server: false,
},
activeTool: { browser: true, text: false, image: false, server: false },
penMode: { browser: true, text: false, image: false, server: false },
penDetected: { browser: true, text: false, image: false, server: false },
errorMessage: { browser: false, text: false, image: false, server: false },
exportBackground: { browser: true, text: false, image: true, server: false },
exportEmbedScene: { browser: true, text: false, image: true, server: false },
exportScale: { browser: true, text: false, image: true, server: false },
exportWithDarkMode: {
browser: true,
text: false,
image: true,
server: false,
},
fileHandle: { browser: false, text: false, image: false, server: false },
gridSize: { browser: true, text: true, image: true, server: true },
height: { browser: false, text: false, image: false, server: false },
isBindingEnabled: {
browser: false,
text: false,
image: false,
server: false,
},
isLibraryOpen: { browser: true, text: false, image: false, server: false },
isLibraryMenuDocked: {
browser: true,
text: false,
image: false,
server: false,
},
isLoading: { browser: false, text: false, image: false, server: false },
isResizing: { browser: false, text: false, image: false, server: false },
isRotating: { browser: false, text: false, image: false, server: false },
lastPointerDownWith: {
browser: true,
text: false,
image: false,
server: false,
},
multiElement: { browser: false, text: false, image: false, server: false },
name: { browser: true, text: false, image: false, server: false },
offsetLeft: { browser: false, text: false, image: false, server: false },
offsetTop: { browser: false, text: false, image: false, server: false },
openMenu: { browser: true, text: false, image: false, server: false },
openPopup: { browser: false, text: false, image: false, server: false },
pasteDialog: { browser: false, text: false, image: false, server: false },
previousSelectedElementIds: {
browser: true,
text: false,
image: false,
server: false,
},
resizingElement: { browser: false, text: false, image: false, server: false },
scrolledOutside: { browser: true, text: false, image: false, server: false },
scrollX: { browser: true, text: false, image: false, server: false },
scrollY: { browser: true, text: false, image: false, server: false },
selectedElementIds: {
browser: true,
text: false,
image: false,
server: false,
},
selectedGroupIds: { browser: true, text: false, image: false, server: false },
selectionElement: {
browser: false,
text: false,
image: false,
server: false,
},
shouldCacheIgnoreZoom: {
browser: true,
text: false,
image: false,
server: false,
},
showHelpDialog: { browser: false, text: false, image: false, server: false },
showStats: { browser: true, text: false, image: false, server: false },
startBoundElement: {
browser: false,
text: false,
image: false,
server: false,
},
suggestedBindings: {
browser: false,
text: false,
image: false,
server: false,
},
toastMessage: { browser: false, text: false, image: false, server: false },
viewBackgroundColor: {
browser: true,
text: true,
image: true,
server: true,
},
width: { browser: false, text: false, image: false, server: false },
zenModeEnabled: { browser: true, text: false, image: false, server: false },
zoom: { browser: true, text: false, image: false, server: false },
viewModeEnabled: { browser: false, text: false, image: false, server: false },
pendingImageElementId: {
browser: false,
text: false,
image: false,
server: false,
},
showHyperlinkPopup: {
browser: false,
text: false,
image: false,
server: false,
},
currentItemOpacity: { browser: true, export: false, server: false },
currentItemRoughness: { browser: true, export: false, server: false },
currentItemStartArrowhead: { browser: true, export: false, server: false },
currentItemStrokeColor: { browser: true, export: false, server: false },
currentItemStrokeSharpness: { browser: true, export: false, server: false },
currentItemStrokeStyle: { browser: true, export: false, server: false },
currentItemStrokeWidth: { browser: true, export: false, server: false },
currentItemTextAlign: { browser: true, export: false, server: false },
cursorButton: { browser: true, export: false, server: false },
draggingElement: { browser: false, export: false, server: false },
editingElement: { browser: false, export: false, server: false },
editingGroupId: { browser: true, export: false, server: false },
editingLinearElement: { browser: false, export: false, server: false },
activeTool: { browser: true, export: false, server: false },
penMode: { browser: true, export: false, server: false },
penDetected: { browser: true, export: false, server: false },
errorMessage: { browser: false, export: false, server: false },
exportBackground: { browser: true, export: false, server: false },
exportEmbedScene: { browser: true, export: false, server: false },
exportScale: { browser: true, export: false, server: false },
exportWithDarkMode: { browser: true, export: false, server: false },
fileHandle: { browser: false, export: false, server: false },
gridSize: { browser: true, export: true, server: true },
height: { browser: false, export: false, server: false },
isBindingEnabled: { browser: false, export: false, server: false },
isLibraryOpen: { browser: true, export: false, server: false },
isLibraryMenuDocked: { browser: true, export: false, server: false },
isLoading: { browser: false, export: false, server: false },
isResizing: { browser: false, export: false, server: false },
isRotating: { browser: false, export: false, server: false },
lastPointerDownWith: { browser: true, export: false, server: false },
multiElement: { browser: false, export: false, server: false },
name: { browser: true, export: false, server: false },
offsetLeft: { browser: false, export: false, server: false },
offsetTop: { browser: false, export: false, server: false },
openMenu: { browser: true, export: false, server: false },
openPopup: { browser: false, export: false, server: false },
pasteDialog: { browser: false, export: false, server: false },
previousSelectedElementIds: { browser: true, export: false, server: false },
resizingElement: { browser: false, export: false, server: false },
scrolledOutside: { browser: true, export: false, server: false },
scrollX: { browser: true, export: false, server: false },
scrollY: { browser: true, export: false, server: false },
selectedElementIds: { browser: true, export: false, server: false },
selectedGroupIds: { browser: true, export: false, server: false },
selectionElement: { browser: false, export: false, server: false },
shouldCacheIgnoreZoom: { browser: true, export: false, server: false },
showHelpDialog: { browser: false, export: false, server: false },
showStats: { browser: true, export: false, server: false },
startBoundElement: { browser: false, export: false, server: false },
suggestedBindings: { browser: false, export: false, server: false },
toast: { browser: false, export: false, server: false },
viewBackgroundColor: { browser: true, export: true, server: true },
width: { browser: false, export: false, server: false },
zenModeEnabled: { browser: true, export: false, server: false },
zoom: { browser: true, export: false, server: false },
viewModeEnabled: { browser: false, export: false, server: false },
pendingImageElementId: { browser: false, export: false, server: false },
showHyperlinkPopup: { browser: false, export: false, server: false },
});
const _clearAppStateForStorage = <
ExportType extends "export" | "browser" | "server",
ExportType extends "image" | "text" | "browser" | "server",
>(
appState: Partial<AppState>,
exportType: ExportType,
@@ -211,8 +349,12 @@ export const clearAppStateForLocalStorage = (appState: Partial<AppState>) => {
return _clearAppStateForStorage(appState, "browser");
};
export const cleanAppStateForExport = (appState: Partial<AppState>) => {
return _clearAppStateForStorage(appState, "export");
export const cleanAppStateForTextExport = (appState: Partial<AppState>) => {
return _clearAppStateForStorage(appState, "text");
};
export const cleanAppStateForImageExport = (appState: Partial<AppState>) => {
return _clearAppStateForStorage(appState, "image");
};
export const clearAppStateForDatabase = (appState: Partial<AppState>) => {

View File

@@ -166,7 +166,7 @@ import {
isAndroid,
} from "../keys";
import { distance2d, getGridPoint, isPathALoop } from "../math";
import { renderScene } from "../renderer/renderScene";
import { renderScene } from "../renderer";
import { invalidateShapeForElement } from "../renderer/renderElement";
import {
calculateScrollCenter,
@@ -286,10 +286,6 @@ let currentScrollBars: ScrollBars = { horizontal: null, vertical: null };
let touchTimeout = 0;
let invalidateContextMenu = false;
// remove this hack when we can sync render & resizeObserver (state update)
// to rAF. See #5439
let THROTTLE_NEXT_RENDER = true;
let lastPointerUp: ((event: any) => void) | null = null;
const gesture: Gesture = {
pointers: new Map(),
@@ -384,7 +380,7 @@ class App extends React.Component<AppProps, AppState> {
getAppState: () => this.state,
getFiles: () => this.files,
refresh: this.refresh,
setToast: this.setToast,
setToastMessage: this.setToastMessage,
id: this.id,
setActiveTool: this.setActiveTool,
setCursor: this.setCursor,
@@ -473,6 +469,7 @@ class App extends React.Component<AppProps, AppState> {
}
public render() {
const { zenModeEnabled, viewModeEnabled } = this.state;
const selectedElement = getSelectedElements(
this.scene.getNonDeletedElements(),
this.state,
@@ -487,7 +484,7 @@ class App extends React.Component<AppProps, AppState> {
return (
<div
className={clsx("excalidraw excalidraw-container", {
"excalidraw--view-mode": this.state.viewModeEnabled,
"excalidraw--view-mode": viewModeEnabled,
"excalidraw--mobile": this.device.isMobile,
})}
ref={this.excalidrawContainerRef}
@@ -518,14 +515,17 @@ class App extends React.Component<AppProps, AppState> {
files: null,
})
}
zenModeEnabled={zenModeEnabled}
toggleZenMode={this.toggleZenMode}
langCode={getLanguage().code}
isCollaborating={this.props.isCollaborating}
renderTopRightUI={renderTopRightUI}
renderCustomFooter={renderFooter}
renderCustomStats={renderCustomStats}
viewModeEnabled={viewModeEnabled}
showExitZenModeBtn={
typeof this.props?.zenModeEnabled === "undefined" &&
this.state.zenModeEnabled
zenModeEnabled
}
showThemeBtn={
typeof this.props?.theme === "undefined" &&
@@ -549,12 +549,10 @@ class App extends React.Component<AppProps, AppState> {
onLinkOpen={this.props.onLinkOpen}
/>
)}
{this.state.toast !== null && (
{this.state.toastMessage !== null && (
<Toast
message={this.state.toast.message}
onClose={() => this.setToast(null)}
duration={this.state.toast.duration}
closable={this.state.toast.closable}
message={this.state.toastMessage}
clearToast={this.clearToast}
/>
)}
<main>{this.renderCanvas()}</main>
@@ -768,7 +766,6 @@ class App extends React.Component<AppProps, AppState> {
? { ...scene.appState.activeTool, type: "selection" }
: scene.appState.activeTool,
isLoading: false,
toast: this.state.toast,
};
if (initialData?.scrollToContent) {
scene.appState = {
@@ -862,7 +859,6 @@ class App extends React.Component<AppProps, AppState> {
if ("ResizeObserver" in window && this.excalidrawContainerRef?.current) {
this.resizeObserver = new ResizeObserver(() => {
THROTTLE_NEXT_RENDER = false;
// recompute device dimensions state
// ---------------------------------------------------------------------
this.refreshDeviceState(this.excalidrawContainerRef.current!);
@@ -908,7 +904,6 @@ class App extends React.Component<AppProps, AppState> {
} else {
this.updateDOMRect(this.initializeScene);
}
this.checkIfBrowserZoomed();
}
public componentWillUnmount() {
@@ -921,25 +916,8 @@ class App extends React.Component<AppProps, AppState> {
clearTimeout(touchTimeout);
touchTimeout = 0;
}
private checkIfBrowserZoomed = () => {
if (!this.device.isMobile) {
const scrollBarWidth = 10;
const widthRatio =
(window.outerWidth - scrollBarWidth) / window.innerWidth;
const isBrowserZoomed = widthRatio < 0.75 || widthRatio > 1.1;
if (isBrowserZoomed) {
this.setToast({
message: t("alerts.browserZoom"),
closable: true,
duration: Infinity,
});
} else {
this.setToast(null);
}
}
};
private onResize = withBatchedUpdates(() => {
this.checkIfBrowserZoomed();
this.scene
.getElementsIncludingDeleted()
.forEach((element) => invalidateShapeForElement(element));
@@ -951,10 +929,6 @@ class App extends React.Component<AppProps, AppState> {
document.removeEventListener(EVENT.COPY, this.onCopy);
document.removeEventListener(EVENT.PASTE, this.pasteFromClipboard);
document.removeEventListener(EVENT.CUT, this.onCut);
this.excalidrawContainerRef.current?.removeEventListener(
EVENT.WHEEL,
this.onWheel,
);
this.nearestScrollableContainer?.removeEventListener(
EVENT.SCROLL,
this.onScroll,
@@ -1003,12 +977,6 @@ class App extends React.Component<AppProps, AppState> {
this.removeEventListeners();
document.addEventListener(EVENT.POINTER_UP, this.removePointer); // #3553
document.addEventListener(EVENT.COPY, this.onCopy);
this.excalidrawContainerRef.current?.addEventListener(
EVENT.WHEEL,
this.onWheel,
{ passive: false },
);
if (this.props.handleKeyboardGlobally) {
document.addEventListener(EVENT.KEYDOWN, this.onKeyDown, false);
}
@@ -1225,8 +1193,7 @@ class App extends React.Component<AppProps, AppState> {
element.id !== this.state.editingElement.id
);
});
renderScene(
const { atLeastOneVisibleElement, scrollBars } = renderScene(
renderingElements,
this.state,
this.state.selectionElement,
@@ -1249,30 +1216,24 @@ class App extends React.Component<AppProps, AppState> {
isExporting: false,
renderScrollbars: !this.device.isMobile,
},
({ atLeastOneVisibleElement, scrollBars }) => {
if (scrollBars) {
currentScrollBars = scrollBars;
}
const scrolledOutside =
// hide when editing text
isTextElement(this.state.editingElement)
? false
: !atLeastOneVisibleElement && renderingElements.length > 0;
if (this.state.scrolledOutside !== scrolledOutside) {
this.setState({ scrolledOutside });
}
this.scheduleImageRefresh();
},
THROTTLE_NEXT_RENDER && window.EXCALIDRAW_THROTTLE_RENDER === true,
);
if (!THROTTLE_NEXT_RENDER) {
THROTTLE_NEXT_RENDER = true;
if (scrollBars) {
currentScrollBars = scrollBars;
}
const scrolledOutside =
// hide when editing text
isTextElement(this.state.editingElement)
? false
: !atLeastOneVisibleElement && renderingElements.length > 0;
if (this.state.scrolledOutside !== scrolledOutside) {
this.setState({ scrolledOutside });
}
this.history.record(this.state, this.scene.getElementsIncludingDeleted());
this.scheduleImageRefresh();
// Do not notify consumers if we're still loading the scene. Among other
// potential issues, this fixes a case where the tab isn't focused during
// init, which would trigger onChange with empty elements, which would then
@@ -1540,15 +1501,12 @@ class App extends React.Component<AppProps, AppState> {
this.state.isLibraryOpen && this.device.canDeviceFitSidebar
? this.state.isLibraryMenuDocked
: false,
selectedElementIds: newElements.reduce(
(acc: Record<ExcalidrawElement["id"], true>, element) => {
if (!isBoundToContainer(element)) {
acc[element.id] = true;
}
return acc;
},
{},
),
selectedElementIds: newElements.reduce((map, element) => {
if (!isBoundToContainer(element)) {
map[element.id] = true;
}
return map;
}, {} as any),
selectedGroupIds: {},
},
this.scene.getNonDeletedElements(),
@@ -1641,6 +1599,10 @@ class App extends React.Component<AppProps, AppState> {
});
};
toggleZenMode = () => {
this.actionManager.executeAction(actionToggleZenMode);
};
scrollToContent = (
target:
| ExcalidrawElement
@@ -1655,14 +1617,12 @@ class App extends React.Component<AppProps, AppState> {
});
};
setToast = (
toast: {
message: string;
closable?: boolean;
duration?: number;
} | null,
) => {
this.setState({ toast });
clearToast = () => {
this.setState({ toastMessage: null });
};
setToastMessage = (toastMessage: string) => {
this.setState({ toastMessage });
};
restoreFileFromShare = async () => {
@@ -1748,7 +1708,6 @@ class App extends React.Component<AppProps, AppState> {
private onKeyDown = withBatchedUpdates(
(event: React.KeyboardEvent | KeyboardEvent) => {
// normalize `event.key` when CapsLock is pressed #2372
if (
"Proxy" in window &&
((!event.shiftKey && /^[A-Z]$/.test(event.key)) ||
@@ -1772,14 +1731,6 @@ class App extends React.Component<AppProps, AppState> {
});
}
// prevent browser zoom in input fields
if (event[KEYS.CTRL_OR_CMD] && isWritableElement(event.target)) {
if (event.code === CODES.MINUS || event.code === CODES.EQUAL) {
event.preventDefault();
return;
}
}
// bail if
if (
// inside an input
@@ -1962,13 +1913,6 @@ class App extends React.Component<AppProps, AppState> {
},
);
private onWheel = withBatchedUpdates((event: WheelEvent) => {
// prevent browser pinch zoom on DOM elements
if (!(event.target instanceof HTMLCanvasElement) && event.ctrlKey) {
event.preventDefault();
}
});
private onKeyUp = withBatchedUpdates((event: KeyboardEvent) => {
if (event.key === KEYS.SPACE) {
if (this.state.viewModeEnabled) {
@@ -4310,13 +4254,10 @@ class App extends React.Component<AppProps, AppState> {
...prevState,
selectedElementIds: {
...prevState.selectedElementIds,
...elementsWithinSelection.reduce(
(acc: Record<ExcalidrawElement["id"], true>, element) => {
acc[element.id] = true;
return acc;
},
{},
),
...elementsWithinSelection.reduce((map, element) => {
map[element.id] = true;
return map;
}, {} as any),
...(pointerDownState.hit.element
? {
// if using ctrl/cmd, select the hitElement only if we
@@ -5783,6 +5724,7 @@ class App extends React.Component<AppProps, AppState> {
private handleWheel = withBatchedUpdates((event: WheelEvent) => {
event.preventDefault();
if (isPanning) {
return;
}

View File

@@ -18,15 +18,13 @@
left: -5px;
}
min-width: 1em;
min-height: 1em;
line-height: 1;
position: absolute;
bottom: -5px;
padding: 3px;
border-radius: 50%;
background-color: $oc-green-6;
color: $oc-white;
font-size: 0.6em;
font-family: "Cascadia";
font-size: 0.7em;
font-family: var(--ui-font);
}
}

View File

@@ -28,7 +28,7 @@ const CollabButton = ({
aria-label={t("labels.liveCollaboration")}
showAriaLabel={useDevice().isMobile}
>
{isCollaborating && (
{collaboratorCount > 0 && (
<div className="CollabButton-collaborators">{collaboratorCount}</div>
)}
</ToolButton>

View File

@@ -170,9 +170,7 @@ const ImageExportModal = ({
<Stack.Row gap={2}>
{actionManager.renderAction("changeExportScale")}
</Stack.Row>
<p style={{ marginLeft: "1em", userSelect: "none" }}>
{t("buttons.scale")}
</p>
<p style={{ marginLeft: "1em", userSelect: "none" }}>Scale</p>
</div>
<div
style={{

View File

@@ -39,7 +39,6 @@ import { trackEvent } from "../analytics";
import { useDevice } from "../components/App";
import { Stats } from "./Stats";
import { actionToggleStats } from "../actions/actionToggleStats";
import { actionToggleZenMode } from "../actions";
interface LayerUIProps {
actionManager: ActionManager;
@@ -52,13 +51,16 @@ interface LayerUIProps {
onLockToggle: () => void;
onPenModeToggle: () => void;
onInsertElements: (elements: readonly NonDeletedExcalidrawElement[]) => void;
zenModeEnabled: boolean;
showExitZenModeBtn: boolean;
showThemeBtn: boolean;
toggleZenMode: () => void;
langCode: Language["code"];
isCollaborating: boolean;
renderTopRightUI?: ExcalidrawProps["renderTopRightUI"];
renderCustomFooter?: ExcalidrawProps["renderFooter"];
renderCustomStats?: ExcalidrawProps["renderCustomStats"];
viewModeEnabled: boolean;
libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"];
UIOptions: AppProps["UIOptions"];
focusContainer: () => void;
@@ -77,12 +79,15 @@ const LayerUI = ({
onLockToggle,
onPenModeToggle,
onInsertElements,
zenModeEnabled,
showExitZenModeBtn,
showThemeBtn,
toggleZenMode,
isCollaborating,
renderTopRightUI,
renderCustomFooter,
renderCustomStats,
viewModeEnabled,
libraryReturnUrl,
UIOptions,
focusContainer,
@@ -166,7 +171,7 @@ const LayerUI = ({
<Section
heading="canvasActions"
className={clsx("zen-mode-transition", {
"transition-left": appState.zenModeEnabled,
"transition-left": zenModeEnabled,
})}
>
{/* the zIndex ensures this menu has higher stacking order,
@@ -187,7 +192,7 @@ const LayerUI = ({
<Section
heading="canvasActions"
className={clsx("zen-mode-transition", {
"transition-left": appState.zenModeEnabled,
"transition-left": zenModeEnabled,
})}
>
{/* the zIndex ensures this menu has higher stacking order,
@@ -227,7 +232,7 @@ const LayerUI = ({
<Section
heading="selectedShapeActions"
className={clsx("zen-mode-transition", {
"transition-left": appState.zenModeEnabled,
"transition-left": zenModeEnabled,
})}
>
<Island
@@ -297,34 +302,32 @@ const LayerUI = ({
<div className="App-menu App-menu_top">
<Stack.Col
gap={4}
className={clsx({
"disable-pointerEvents": appState.zenModeEnabled,
})}
className={clsx({ "disable-pointerEvents": zenModeEnabled })}
>
{appState.viewModeEnabled
{viewModeEnabled
? renderViewModeCanvasActions()
: renderCanvasActions()}
{shouldRenderSelectedShapeActions && renderSelectedShapeActions()}
</Stack.Col>
{!appState.viewModeEnabled && (
{!viewModeEnabled && (
<Section heading="shapes">
{(heading) => (
<Stack.Col gap={4} align="start">
<Stack.Row
gap={1}
className={clsx("App-toolbar-container", {
"zen-mode": appState.zenModeEnabled,
"zen-mode": zenModeEnabled,
})}
>
<PenModeButton
zenModeEnabled={appState.zenModeEnabled}
zenModeEnabled={zenModeEnabled}
checked={appState.penMode}
onChange={onPenModeToggle}
title={t("toolBar.penMode")}
penDetected={appState.penDetected}
/>
<LockButton
zenModeEnabled={appState.zenModeEnabled}
zenModeEnabled={zenModeEnabled}
checked={appState.activeTool.locked}
onChange={() => onLockToggle()}
title={t("toolBar.lock")}
@@ -332,7 +335,7 @@ const LayerUI = ({
<Island
padding={1}
className={clsx("App-toolbar", {
"zen-mode": appState.zenModeEnabled,
"zen-mode": zenModeEnabled,
})}
>
<HintViewer
@@ -368,7 +371,7 @@ const LayerUI = ({
className={clsx(
"layer-ui__wrapper__top-right zen-mode-transition",
{
"transition-right": appState.zenModeEnabled,
"transition-right": zenModeEnabled,
},
)}
>
@@ -393,8 +396,7 @@ const LayerUI = ({
className={clsx(
"layer-ui__wrapper__footer-left zen-mode-transition",
{
"layer-ui__wrapper__footer-left--transition-left":
appState.zenModeEnabled,
"layer-ui__wrapper__footer-left--transition-left": zenModeEnabled,
},
)}
>
@@ -406,12 +408,12 @@ const LayerUI = ({
zoom={appState.zoom}
/>
</Island>
{!appState.viewModeEnabled && (
{!viewModeEnabled && (
<>
<div
className={clsx("undo-redo-buttons zen-mode-transition", {
"layer-ui__wrapper__footer-left--transition-bottom":
appState.zenModeEnabled,
zenModeEnabled,
})}
>
{actionManager.renderAction("undo", { size: "small" })}
@@ -421,20 +423,20 @@ const LayerUI = ({
<div
className={clsx("eraser-buttons zen-mode-transition", {
"layer-ui__wrapper__footer-left--transition-left":
appState.zenModeEnabled,
zenModeEnabled,
})}
>
{actionManager.renderAction("eraser", { size: "small" })}
</div>
</>
)}
{!appState.viewModeEnabled &&
{!viewModeEnabled &&
appState.multiElement &&
device.isTouchScreen && (
<div
className={clsx("finalize-button zen-mode-transition", {
"layer-ui__wrapper__footer-left--transition-left":
appState.zenModeEnabled,
zenModeEnabled,
})}
>
{actionManager.renderAction("finalize", { size: "small" })}
@@ -448,7 +450,7 @@ const LayerUI = ({
"layer-ui__wrapper__footer-center zen-mode-transition",
{
"layer-ui__wrapper__footer-left--transition-bottom":
appState.zenModeEnabled,
zenModeEnabled,
},
)}
>
@@ -458,7 +460,7 @@ const LayerUI = ({
className={clsx(
"layer-ui__wrapper__footer-right zen-mode-transition",
{
"transition-right disable-pointerEvents": appState.zenModeEnabled,
"transition-right disable-pointerEvents": zenModeEnabled,
},
)}
>
@@ -468,7 +470,7 @@ const LayerUI = ({
className={clsx("disable-zen-mode", {
"disable-zen-mode--visible": showExitZenModeBtn,
})}
onClick={() => actionManager.executeAction(actionToggleZenMode)}
onClick={toggleZenMode}
>
{t("buttons.exitZenMode")}
</button>
@@ -541,6 +543,7 @@ const LayerUI = ({
canvas={canvas}
isCollaborating={isCollaborating}
renderCustomFooter={renderCustomFooter}
viewModeEnabled={viewModeEnabled}
showThemeBtn={showThemeBtn}
onImageAction={onImageAction}
renderTopRightUI={renderTopRightUI}

View File

@@ -36,6 +36,7 @@ type MobileMenuProps = {
isMobile: boolean,
appState: AppState,
) => JSX.Element | null;
viewModeEnabled: boolean;
showThemeBtn: boolean;
onImageAction: (data: { insertOnCanvasDirectly: boolean }) => void;
renderTopRightUI?: (
@@ -59,6 +60,7 @@ export const MobileMenu = ({
canvas,
isCollaborating,
renderCustomFooter,
viewModeEnabled,
showThemeBtn,
onImageAction,
renderTopRightUI,
@@ -123,7 +125,7 @@ export const MobileMenu = ({
!appState.editingElement &&
getSelectedElements(elements, appState).length === 0;
if (appState.viewModeEnabled) {
if (viewModeEnabled) {
return (
<div className="App-toolbar-content">
{actionManager.renderAction("toggleCanvasMenu")}
@@ -149,7 +151,7 @@ export const MobileMenu = ({
};
const renderCanvasActions = () => {
if (appState.viewModeEnabled) {
if (viewModeEnabled) {
return (
<>
{renderJSONExportDialog()}
@@ -183,7 +185,7 @@ export const MobileMenu = ({
};
return (
<>
{!appState.viewModeEnabled && renderToolbar()}
{!viewModeEnabled && renderToolbar()}
{renderStats()}
<div
className="App-bottom-bar"
@@ -214,7 +216,7 @@ export const MobileMenu = ({
</div>
</Section>
) : appState.openMenu === "shape" &&
!appState.viewModeEnabled &&
!viewModeEnabled &&
showSelectedShapeActions(appState, elements) ? (
<Section className="App-mobile-menu" heading="selectedShapeActions">
<SelectedShapeActions
@@ -227,20 +229,18 @@ export const MobileMenu = ({
) : null}
<footer className="App-toolbar">
{renderAppToolbar()}
{appState.scrolledOutside &&
!appState.openMenu &&
!appState.isLibraryOpen && (
<button
className="scroll-back-to-content"
onClick={() => {
setAppState({
...calculateScrollCenter(elements, appState, canvas),
});
}}
>
{t("buttons.scrollBackToContent")}
</button>
)}
{appState.scrolledOutside && !appState.openMenu && (
<button
className="scroll-back-to-content"
onClick={() => {
setAppState({
...calculateScrollCenter(elements, appState, canvas),
});
}}
>
{t("buttons.scrollBackToContent")}
</button>
)}
</footer>
</Island>
</div>

View File

@@ -2,6 +2,5 @@
.popover {
position: absolute;
z-index: 10;
padding: 5px 0 5px;
}
}

View File

@@ -69,27 +69,12 @@ export const Popover = ({
if (fitInViewport && popoverRef.current) {
const element = popoverRef.current;
const { x, y, width, height } = element.getBoundingClientRect();
const { innerWidth: viewportWidth, innerHeight: viewportHeight } = window;
//Position correctly when clicked on rightmost part or the bottom part of viewport
if (x + width - offsetLeft > viewportWidth) {
element.style.left = `${viewportWidth - width - 10}px`;
element.style.left = `${viewportWidth - width}px`;
}
if (y + height - offsetTop > viewportHeight) {
element.style.top = `${viewportHeight - height}px`;
}
//Resize to fit viewport on smaller screens
if (height >= viewportHeight) {
element.style.height = `${viewportHeight - 20}px`;
element.style.top = "10px";
element.style.overflowY = "scroll";
}
if (width >= viewportWidth) {
element.style.width = `${viewportWidth}px`;
element.style.left = "0px";
element.style.overflowX = "scroll";
}
}
}, [fitInViewport, viewportWidth, viewportHeight, offsetLeft, offsetTop]);

View File

@@ -2,9 +2,6 @@
.excalidraw {
.Toast {
$closeButtonSize: 1.2rem;
$closeButtonPadding: 0.4rem;
animation: fade-in 0.5s;
background-color: var(--button-gray-1);
border-radius: 4px;
@@ -18,24 +15,11 @@
text-align: center;
width: 300px;
z-index: 999999;
}
.Toast__message {
padding: 0 $closeButtonSize + ($closeButtonPadding);
color: var(--popup-text-color);
white-space: pre-wrap;
}
.close {
position: absolute;
top: 0;
right: 0;
padding: $closeButtonPadding;
.ToolIcon__icon {
width: $closeButtonSize;
height: $closeButtonSize;
}
}
.Toast__message {
color: var(--popup-text-color);
white-space: pre-wrap;
}
@keyframes fade-in {

View File

@@ -1,59 +1,34 @@
import { useCallback, useEffect, useRef } from "react";
import { close } from "./icons";
import { TOAST_TIMEOUT } from "../constants";
import "./Toast.scss";
import { ToolButton } from "./ToolButton";
const DEFAULT_TOAST_TIMEOUT = 5000;
export const Toast = ({
message,
onClose,
closable = false,
// To prevent autoclose, pass duration as Infinity
duration = DEFAULT_TOAST_TIMEOUT,
clearToast,
}: {
message: string;
onClose: () => void;
closable?: boolean;
duration?: number;
clearToast: () => void;
}) => {
const timerRef = useRef<number>(0);
const shouldAutoClose = duration !== Infinity;
const scheduleTimeout = useCallback(() => {
if (!shouldAutoClose) {
return;
}
timerRef.current = window.setTimeout(() => onClose(), duration);
}, [onClose, duration, shouldAutoClose]);
const scheduleTimeout = useCallback(
() =>
(timerRef.current = window.setTimeout(() => clearToast(), TOAST_TIMEOUT)),
[clearToast],
);
useEffect(() => {
if (!shouldAutoClose) {
return;
}
scheduleTimeout();
return () => clearTimeout(timerRef.current);
}, [scheduleTimeout, message, duration, shouldAutoClose]);
}, [scheduleTimeout, message]);
const onMouseEnter = shouldAutoClose
? () => clearTimeout(timerRef?.current)
: undefined;
const onMouseLeave = shouldAutoClose ? scheduleTimeout : undefined;
return (
<div
className="Toast"
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
onMouseEnter={() => clearTimeout(timerRef?.current)}
onMouseLeave={scheduleTimeout}
>
<p className="Toast__message">{message}</p>
{closable && (
<ToolButton
icon={close}
aria-label="close"
type="icon"
onClick={onClose}
className="close"
/>
)}
</div>
);
};

View File

@@ -212,14 +212,16 @@
}
}
.ToolIcon.ToolIcon__library {
top: calc(var(--sat) + 100px);
top: 100px;
}
.ToolIcon.ToolIcon__lock {
top: calc(var(--sat) + 60px);
margin-inline-end: 0;
top: 60px;
}
.ToolIcon.ToolIcon__penMode {
top: calc(var(--sat) + 140px);
margin-inline-end: 0;
top: 140px;
}
}

View File

@@ -32,6 +32,7 @@
}
.ToolIcon.ToolIcon__lock {
margin-inline-end: var(--space-factor);
&.ToolIcon_type_floating {
margin-left: 0.1rem;
}

View File

@@ -116,6 +116,7 @@ export const IMAGE_RENDER_TIMEOUT = 500;
export const TAP_TWICE_TIMEOUT = 300;
export const TOUCH_CTX_MENU_TIMEOUT = 500;
export const TITLE_TIMEOUT = 10000;
export const TOAST_TIMEOUT = 5000;
export const VERSION_TIMEOUT = 30000;
export const SCROLL_TIMEOUT = 100;
export const ZOOM_STEP = 0.1;

View File

@@ -0,0 +1,42 @@
import React from "react";
export const createInverseContext = <T extends unknown = null>(
initialValue: T,
) => {
const Context = React.createContext(initialValue) as React.Context<T> & {
_updateProviderValue?: (value: T) => void;
};
class InverseConsumer extends React.Component {
state = { value: initialValue };
constructor(props: any) {
super(props);
Context._updateProviderValue = (value: T) => this.setState({ value });
}
render() {
return (
<Context.Provider value={this.state.value}>
{this.props.children}
</Context.Provider>
);
}
}
class InverseProvider extends React.Component<{ value: T }> {
componentDidMount() {
Context._updateProviderValue?.(this.props.value);
}
componentDidUpdate() {
Context._updateProviderValue?.(this.props.value);
}
render() {
return <Context.Consumer>{() => this.props.children}</Context.Consumer>;
}
}
return {
Context,
Consumer: InverseConsumer,
Provider: InverseProvider,
};
};

View File

@@ -1,5 +1,5 @@
import { nanoid } from "nanoid";
import { cleanAppStateForExport } from "../appState";
import { cleanAppStateForImageExport } from "../appState";
import { ALLOWED_IMAGE_MIME_TYPES, MIME_TYPES } from "../constants";
import { clearElementsForExport } from "../element";
import { ExcalidrawElement, FileId } from "../element/types";
@@ -143,7 +143,7 @@ export const loadSceneOrLibraryFromBlob = async (
appState: {
theme: localAppState?.theme,
fileHandle: fileHandle || blob.handle || null,
...cleanAppStateForExport(data.appState || {}),
...cleanAppStateForImageExport(data.appState || {}),
...(localAppState
? calculateScrollCenter(
data.elements || [],

View File

@@ -1,4 +1,5 @@
import {
FileWithHandle,
fileOpen as _fileOpen,
fileSave as _fileSave,
FileSystemHandle,
@@ -25,9 +26,13 @@ export const fileOpen = <M extends boolean | undefined = false>(opts: {
extensions?: FILE_EXTENSION[];
description: string;
multiple?: M;
}): Promise<M extends false | undefined ? File : File[]> => {
}): Promise<
M extends false | undefined ? FileWithHandle : FileWithHandle[]
> => {
// an unsafe TS hack, alas not much we can do AFAIK
type RetType = M extends false | undefined ? File : File[];
type RetType = M extends false | undefined
? FileWithHandle
: FileWithHandle[];
const mimeTypes = opts.extensions?.reduce((mimeTypes, type) => {
mimeTypes.push(MIME_TYPES[type]);

View File

@@ -82,7 +82,7 @@ export const exportCanvas = async (
await import(/* webpackChunkName: "image" */ "./image")
).encodePngMetadata({
blob,
metadata: serializeAsJSON(elements, appState, files, "local"),
metadata: serializeAsJSON(elements, appState, files, "image"),
});
}

View File

@@ -1,5 +1,9 @@
import { fileOpen, fileSave } from "./filesystem";
import { cleanAppStateForExport, clearAppStateForDatabase } from "../appState";
import {
cleanAppStateForImageExport,
cleanAppStateForTextExport,
clearAppStateForDatabase,
} from "../appState";
import {
EXPORT_DATA_TYPES,
EXPORT_SOURCE,
@@ -43,25 +47,32 @@ export const serializeAsJSON = (
elements: readonly ExcalidrawElement[],
appState: Partial<AppState>,
files: BinaryFiles,
type: "local" | "database",
destination: "text" | "image" | "database",
): string => {
const cleanAppState = () => {
switch (destination) {
case "database":
return clearAppStateForDatabase(appState);
case "text":
return cleanAppStateForTextExport(appState);
case "image":
return cleanAppStateForImageExport(appState);
}
};
const data: ExportedDataState = {
type: EXPORT_DATA_TYPES.excalidraw,
version: VERSIONS.excalidraw,
source: EXPORT_SOURCE,
elements:
type === "local"
? clearElementsForExport(elements)
: clearElementsForDatabase(elements),
appState:
type === "local"
? cleanAppStateForExport(appState)
: clearAppStateForDatabase(appState),
destination === "database"
? clearElementsForDatabase(elements)
: clearElementsForExport(elements),
appState: cleanAppState(),
files:
type === "local"
? filterOutDeletedFiles(elements, files)
: // will be stripped from JSON
undefined,
destination === "database"
? // will be stripped from JSON
undefined
: filterOutDeletedFiles(elements, files),
};
return JSON.stringify(data, null, 2);
@@ -72,7 +83,7 @@ export const saveAsJSON = async (
appState: AppState,
files: BinaryFiles,
) => {
const serialized = serializeAsJSON(elements, appState, files, "local");
const serialized = serializeAsJSON(elements, appState, files, "text");
const blob = new Blob([serialized], {
type: MIME_TYPES.excalidraw,
});
@@ -98,12 +109,7 @@ export const loadFromJSON = async (
// gets resolved. Else, iOS users cannot open `.excalidraw` files.
// extensions: ["json", "excalidraw", "png", "svg"],
});
return loadFromBlob(
await normalizeFile(file),
localAppState,
localElements,
file.handle,
);
return loadFromBlob(await normalizeFile(file), localAppState, localElements);
};
export const isValidExcalidrawData = (data?: {

View File

@@ -5,7 +5,7 @@ import {
LibraryItems,
LibraryItems_anyVersion,
} from "../types";
import type { cleanAppStateForExport } from "../appState";
import type { cleanAppStateForTextExport } from "../appState";
import { VERSIONS } from "../constants";
export interface ExportedDataState {
@@ -13,7 +13,7 @@ export interface ExportedDataState {
version: number;
source: string;
elements: readonly ExcalidrawElement[];
appState: ReturnType<typeof cleanAppStateForExport>;
appState: ReturnType<typeof cleanAppStateForTextExport>;
files: BinaryFiles | undefined;
}

View File

@@ -19,7 +19,7 @@ const getStorageSizes = debounce((cb: (sizes: StorageSizes) => void) => {
}, STORAGE_SIZE_TIMEOUT);
type Props = {
setToast: (message: string) => void;
setToastMessage: (message: string) => void;
};
const CustomStats = (props: Props) => {
const [storageSizes, setStorageSizes] = useState<StorageSizes>({
@@ -68,7 +68,7 @@ const CustomStats = (props: Props) => {
onClick={async () => {
try {
await copyTextToSystemClipboard(getVersion());
props.setToast(t("toast.copyToClipboard"));
props.setToastMessage(t("toast.copyToClipboard"));
} catch {}
}}
title={t("stats.versionCopy")}

View File

@@ -8,12 +8,10 @@ import {
ExcalidrawElement,
InitializedExcalidrawImageElement,
} from "../../element/types";
import {
getSceneVersion,
restoreElements,
} from "../../packages/excalidraw/index";
import { getSceneVersion } from "../../packages/excalidraw/index";
import { Collaborator, Gesture } from "../../types";
import {
getFrame,
preventUnload,
resolvablePromise,
withBatchedUpdates,
@@ -49,9 +47,11 @@ import {
} from "../data/localStorage";
import Portal from "./Portal";
import RoomDialog from "./RoomDialog";
import { createInverseContext } from "../../createInverseContext";
import { t } from "../../i18n";
import { UserIdleState } from "../../types";
import { IDLE_THRESHOLD, ACTIVE_THRESHOLD } from "../../constants";
import { trackEvent } from "../../analytics";
import {
encodeFilesForUpload,
FileManager,
@@ -70,45 +70,52 @@ import {
import { decryptData } from "../../data/encryption";
import { resetBrowserStateVersions } from "../data/tabSync";
import { LocalData } from "../data/LocalData";
import { atom, useAtom } from "jotai";
import { jotaiStore } from "../../jotai";
export const collabAPIAtom = atom<CollabAPI | null>(null);
export const collabDialogShownAtom = atom(false);
export const isCollaboratingAtom = atom(false);
interface CollabState {
modalIsShown: boolean;
errorMessage: string;
username: string;
userState: UserIdleState;
activeRoomLink: string;
}
type CollabInstance = InstanceType<typeof Collab>;
type CollabInstance = InstanceType<typeof CollabWrapper>;
export interface CollabAPI {
/** function so that we can access the latest value from stale callbacks */
isCollaborating: () => boolean;
username: CollabState["username"];
userState: CollabState["userState"];
onPointerUpdate: CollabInstance["onPointerUpdate"];
startCollaboration: CollabInstance["startCollaboration"];
stopCollaboration: CollabInstance["stopCollaboration"];
initializeSocketClient: CollabInstance["initializeSocketClient"];
onCollabButtonClick: CollabInstance["onCollabButtonClick"];
syncElements: CollabInstance["syncElements"];
fetchImageFilesFromFirebase: CollabInstance["fetchImageFilesFromFirebase"];
setUsername: (username: string) => void;
}
interface PublicProps {
interface Props {
excalidrawAPI: ExcalidrawImperativeAPI;
onRoomClose?: () => void;
}
type Props = PublicProps & { modalIsShown: boolean };
const {
Context: CollabContext,
Consumer: CollabContextConsumer,
Provider: CollabContextProvider,
} = createInverseContext<{ api: CollabAPI | null }>({ api: null });
class Collab extends PureComponent<Props, CollabState> {
export { CollabContext, CollabContextConsumer };
class CollabWrapper extends PureComponent<Props, CollabState> {
portal: Portal;
fileManager: FileManager;
excalidrawAPI: Props["excalidrawAPI"];
activeIntervalId: number | null;
idleTimeoutId: number | null;
// marked as private to ensure we don't change it outside this class
private _isCollaborating: boolean = false;
private socketInitializationTimer?: number;
private lastBroadcastedOrReceivedSceneVersion: number = -1;
private collaborators = new Map<string, Collaborator>();
@@ -116,8 +123,10 @@ class Collab extends PureComponent<Props, CollabState> {
constructor(props: Props) {
super(props);
this.state = {
modalIsShown: false,
errorMessage: "",
username: importUsernameFromLocalStorage() || "",
userState: UserIdleState.ACTIVE,
activeRoomLink: "",
};
this.portal = new Portal(this);
@@ -155,18 +164,6 @@ class Collab extends PureComponent<Props, CollabState> {
window.addEventListener(EVENT.BEFORE_UNLOAD, this.beforeUnload);
window.addEventListener(EVENT.UNLOAD, this.onUnload);
const collabAPI: CollabAPI = {
isCollaborating: this.isCollaborating,
onPointerUpdate: this.onPointerUpdate,
startCollaboration: this.startCollaboration,
syncElements: this.syncElements,
fetchImageFilesFromFirebase: this.fetchImageFilesFromFirebase,
stopCollaboration: this.stopCollaboration,
setUsername: this.setUsername,
};
jotaiStore.set(collabAPIAtom, collabAPI);
if (
process.env.NODE_ENV === ENV.TEST ||
process.env.NODE_ENV === ENV.DEVELOPMENT
@@ -199,11 +196,7 @@ class Collab extends PureComponent<Props, CollabState> {
}
}
isCollaborating = () => jotaiStore.get(isCollaboratingAtom)!;
private setIsCollaborating = (isCollaborating: boolean) => {
jotaiStore.set(isCollaboratingAtom, isCollaborating);
};
isCollaborating = () => this._isCollaborating;
private onUnload = () => {
this.destroySocketClient({ isUnload: true });
@@ -215,7 +208,7 @@ class Collab extends PureComponent<Props, CollabState> {
);
if (
this.isCollaborating() &&
this._isCollaborating &&
(this.fileManager.shouldPreventUnload(syncableElements) ||
!isSavedToFirebase(this.portal, syncableElements))
) {
@@ -259,7 +252,12 @@ class Collab extends PureComponent<Props, CollabState> {
}
};
stopCollaboration = (keepRemoteState = true) => {
openPortal = async () => {
trackEvent("share", "room creation", `ui (${getFrame()})`);
return this.initializeSocketClient(null);
};
closePortal = () => {
this.queueBroadcastAllElements.cancel();
this.queueSaveToFirebase.cancel();
this.loadImageFiles.cancel();
@@ -269,26 +267,16 @@ class Collab extends PureComponent<Props, CollabState> {
this.excalidrawAPI.getSceneElementsIncludingDeleted(),
),
);
if (this.portal.socket && this.fallbackInitializationHandler) {
this.portal.socket.off(
"connect_error",
this.fallbackInitializationHandler,
);
}
if (!keepRemoteState) {
LocalData.fileStorage.reset();
this.destroySocketClient();
} else if (window.confirm(t("alerts.collabStopOverridePrompt"))) {
if (window.confirm(t("alerts.collabStopOverridePrompt"))) {
// hack to ensure that we prefer we disregard any new browser state
// that could have been saved in other tabs while we were collaborating
resetBrowserStateVersions();
window.history.pushState({}, APP_NAME, window.location.origin);
this.destroySocketClient();
trackEvent("share", "room closed");
LocalData.fileStorage.reset();
this.props.onRoomClose?.();
const elements = this.excalidrawAPI
.getSceneElementsIncludingDeleted()
@@ -307,20 +295,20 @@ class Collab extends PureComponent<Props, CollabState> {
};
private destroySocketClient = (opts?: { isUnload: boolean }) => {
this.lastBroadcastedOrReceivedSceneVersion = -1;
this.portal.close();
this.fileManager.reset();
if (!opts?.isUnload) {
this.setIsCollaborating(false);
this.setState({
activeRoomLink: "",
});
this.collaborators = new Map();
this.excalidrawAPI.updateScene({
collaborators: this.collaborators,
});
this.setState({
activeRoomLink: "",
});
this._isCollaborating = false;
LocalData.resumeSave("collaboration");
}
this.lastBroadcastedOrReceivedSceneVersion = -1;
this.portal.close();
this.fileManager.reset();
};
private fetchImageFilesFromFirebase = async (scene: {
@@ -361,9 +349,7 @@ class Collab extends PureComponent<Props, CollabState> {
}
};
private fallbackInitializationHandler: null | (() => any) = null;
startCollaboration = async (
private initializeSocketClient = async (
existingRoomLinkData: null | { roomId: string; roomKey: string },
): Promise<ImportedDataState | null> => {
if (this.portal.socket) {
@@ -386,23 +372,13 @@ class Collab extends PureComponent<Props, CollabState> {
const scenePromise = resolvablePromise<ImportedDataState | null>();
this.setIsCollaborating(true);
this._isCollaborating = true;
LocalData.pauseSave("collaboration");
const { default: socketIOClient } = await import(
/* webpackChunkName: "socketIoClient" */ "socket.io-client"
);
const fallbackInitializationHandler = () => {
this.initializeRoom({
roomLinkData: existingRoomLinkData,
fetchScene: true,
}).then((scene) => {
scenePromise.resolve(scene);
});
};
this.fallbackInitializationHandler = fallbackInitializationHandler;
try {
const socketServerData = await getCollabServer();
@@ -415,8 +391,6 @@ class Collab extends PureComponent<Props, CollabState> {
roomId,
roomKey,
);
this.portal.socket.once("connect_error", fallbackInitializationHandler);
} catch (error: any) {
console.error(error);
this.setState({ errorMessage: error.message });
@@ -445,10 +419,13 @@ class Collab extends PureComponent<Props, CollabState> {
// fallback in case you're not alone in the room but still don't receive
// initial SCENE_INIT message
this.socketInitializationTimer = window.setTimeout(
fallbackInitializationHandler,
INITIAL_SCENE_UPDATE_TIMEOUT,
);
this.socketInitializationTimer = window.setTimeout(() => {
this.initializeRoom({
roomLinkData: existingRoomLinkData,
fetchScene: true,
});
scenePromise.resolve(null);
}, INITIAL_SCENE_UPDATE_TIMEOUT);
// All socket listeners are moving to Portal
this.portal.socket.on(
@@ -553,12 +530,6 @@ class Collab extends PureComponent<Props, CollabState> {
}
| { fetchScene: false; roomLinkData?: null }) => {
clearTimeout(this.socketInitializationTimer!);
if (this.portal.socket && this.fallbackInitializationHandler) {
this.portal.socket.off(
"connect_error",
this.fallbackInitializationHandler,
);
}
if (fetchScene && roomLinkData && this.portal.socket) {
this.excalidrawAPI.resetScene();
@@ -596,8 +567,6 @@ class Collab extends PureComponent<Props, CollabState> {
const localElements = this.getSceneElementsIncludingDeleted();
const appState = this.excalidrawAPI.getAppState();
remoteElements = restoreElements(remoteElements, null);
const reconciledElements = _reconcileElements(
localElements,
remoteElements,
@@ -703,17 +672,19 @@ class Collab extends PureComponent<Props, CollabState> {
};
setCollaborators(sockets: string[]) {
const collaborators: InstanceType<typeof Collab>["collaborators"] =
new Map();
for (const socketId of sockets) {
if (this.collaborators.has(socketId)) {
collaborators.set(socketId, this.collaborators.get(socketId)!);
} else {
collaborators.set(socketId, {});
this.setState((state) => {
const collaborators: InstanceType<typeof CollabWrapper>["collaborators"] =
new Map();
for (const socketId of sockets) {
if (this.collaborators.has(socketId)) {
collaborators.set(socketId, this.collaborators.get(socketId)!);
} else {
collaborators.set(socketId, {});
}
}
}
this.collaborators = collaborators;
this.excalidrawAPI.updateScene({ collaborators });
this.collaborators = collaborators;
this.excalidrawAPI.updateScene({ collaborators });
});
}
public setLastBroadcastedOrReceivedSceneVersion = (version: number) => {
@@ -742,6 +713,7 @@ class Collab extends PureComponent<Props, CollabState> {
);
onIdleStateChange = (userState: UserIdleState) => {
this.setState({ userState });
this.portal.broadcastIdleChange(userState);
};
@@ -775,22 +747,18 @@ class Collab extends PureComponent<Props, CollabState> {
this.setLastBroadcastedOrReceivedSceneVersion(newVersion);
}, SYNC_FULL_SCENE_INTERVAL_MS);
queueSaveToFirebase = throttle(
() => {
if (this.portal.socketInitialized) {
this.saveCollabRoomToFirebase(
getSyncableElements(
this.excalidrawAPI.getSceneElementsIncludingDeleted(),
),
);
}
},
SYNC_FULL_SCENE_INTERVAL_MS,
{ leading: false },
);
queueSaveToFirebase = throttle(() => {
if (this.portal.socketInitialized) {
this.saveCollabRoomToFirebase(
getSyncableElements(
this.excalidrawAPI.getSceneElementsIncludingDeleted(),
),
);
}
}, SYNC_FULL_SCENE_INTERVAL_MS);
handleClose = () => {
jotaiStore.set(collabDialogShownAtom, false);
this.setState({ modalIsShown: false });
};
setUsername = (username: string) => {
@@ -802,10 +770,35 @@ class Collab extends PureComponent<Props, CollabState> {
saveUsernameToLocalStorage(username);
};
render() {
const { username, errorMessage, activeRoomLink } = this.state;
onCollabButtonClick = () => {
this.setState({
modalIsShown: true,
});
};
const { modalIsShown } = this.props;
/** PRIVATE. Use `this.getContextValue()` instead. */
private contextValue: CollabAPI | null = null;
/** Getter of context value. Returned object is stable. */
getContextValue = (): CollabAPI => {
if (!this.contextValue) {
this.contextValue = {} as CollabAPI;
}
this.contextValue.isCollaborating = this.isCollaborating;
this.contextValue.username = this.state.username;
this.contextValue.onPointerUpdate = this.onPointerUpdate;
this.contextValue.initializeSocketClient = this.initializeSocketClient;
this.contextValue.onCollabButtonClick = this.onCollabButtonClick;
this.contextValue.syncElements = this.syncElements;
this.contextValue.fetchImageFilesFromFirebase =
this.fetchImageFilesFromFirebase;
this.contextValue.setUsername = this.setUsername;
return this.contextValue;
};
render() {
const { modalIsShown, username, errorMessage, activeRoomLink } = this.state;
return (
<>
@@ -815,8 +808,8 @@ class Collab extends PureComponent<Props, CollabState> {
activeRoomLink={activeRoomLink}
username={username}
onUsernameChange={this.onUsernameChange}
onRoomCreate={() => this.startCollaboration(null)}
onRoomDestroy={this.stopCollaboration}
onRoomCreate={this.openPortal}
onRoomDestroy={this.closePortal}
setErrorMessage={(errorMessage) => {
this.setState({ errorMessage });
}}
@@ -829,6 +822,11 @@ class Collab extends PureComponent<Props, CollabState> {
onClose={() => this.setState({ errorMessage: "" })}
/>
)}
<CollabContextProvider
value={{
api: this.getContextValue(),
}}
/>
</>
);
}
@@ -836,7 +834,7 @@ class Collab extends PureComponent<Props, CollabState> {
declare global {
interface Window {
collab: InstanceType<typeof Collab>;
collab: InstanceType<typeof CollabWrapper>;
}
}
@@ -847,11 +845,4 @@ if (
window.collab = window.collab || ({} as Window["collab"]);
}
const _Collab: React.FC<PublicProps> = (props) => {
const [collabDialogShown] = useAtom(collabDialogShownAtom);
return <Collab {...props} modalIsShown={collabDialogShown} />;
};
export default _Collab;
export type TCollabClass = Collab;
export default CollabWrapper;

View File

@@ -4,7 +4,7 @@ import {
SocketUpdateDataSource,
} from "../data";
import { TCollabClass } from "./Collab";
import CollabWrapper from "./CollabWrapper";
import { ExcalidrawElement } from "../../element/types";
import {
@@ -20,14 +20,14 @@ import { BroadcastedExcalidrawElement } from "./reconciliation";
import { encryptData } from "../../data/encryption";
class Portal {
collab: TCollabClass;
collab: CollabWrapper;
socket: SocketIOClient.Socket | null = null;
socketInitialized: boolean = false; // we don't want the socket to emit any updates until it is fully initialized
roomId: string | null = null;
roomKey: string | null = null;
broadcastedElementVersions: Map<string, number> = new Map();
constructor(collab: TCollabClass) {
constructor(collab: CollabWrapper) {
this.collab = collab;
}

View File

@@ -14,8 +14,6 @@ import { t } from "../../i18n";
import "./RoomDialog.scss";
import Stack from "../../components/Stack";
import { AppState } from "../../types";
import { trackEvent } from "../../analytics";
import { getFrame } from "../../utils";
const getShareIcon = () => {
const navigator = window.navigator as any;
@@ -97,10 +95,7 @@ const RoomDialog = ({
title={t("roomDialog.button_startSession")}
aria-label={t("roomDialog.button_startSession")}
showAriaLabel={true}
onClick={() => {
trackEvent("share", "room creation", `ui (${getFrame()})`);
onRoomCreate();
}}
onClick={onRoomCreate}
/>
</div>
</>
@@ -165,10 +160,7 @@ const RoomDialog = ({
title={t("roomDialog.button_stopSession")}
aria-label={t("roomDialog.button_stopSession")}
showAriaLabel={true}
onClick={() => {
trackEvent("share", "room closed");
onRoomDestroy();
}}
onClick={onRoomDestroy}
/>
</div>
</>

View File

@@ -134,16 +134,9 @@ export type SocketUpdateData =
_brand: "socketUpdateData";
};
const RE_COLLAB_LINK = /^#room=([a-zA-Z0-9_-]+),([a-zA-Z0-9_-]+)$/;
export const isCollaborationLink = (link: string) => {
const hash = new URL(link).hash;
return RE_COLLAB_LINK.test(hash);
};
export const getCollaborationLinkData = (link: string) => {
const hash = new URL(link).hash;
const match = hash.match(RE_COLLAB_LINK);
const match = hash.match(/^#room=([a-zA-Z0-9_-]+),([a-zA-Z0-9_-]+)$/);
if (match && match[2].length !== 22) {
window.alert(t("alerts.invalidEncryptionKey"));
return null;

View File

@@ -1,5 +1,5 @@
import LanguageDetector from "i18next-browser-languagedetector";
import { useCallback, useEffect, useRef, useState } from "react";
import { useCallback, useContext, useEffect, useRef, useState } from "react";
import { trackEvent } from "../analytics";
import { getDefaultAppState } from "../appState";
import { ErrorDialog } from "../components/ErrorDialog";
@@ -45,26 +45,20 @@ import {
STORAGE_KEYS,
SYNC_BROWSER_TABS_TIMEOUT,
} from "./app_constants";
import Collab, {
import CollabWrapper, {
CollabAPI,
collabAPIAtom,
collabDialogShownAtom,
isCollaboratingAtom,
} from "./collab/Collab";
CollabContext,
CollabContextConsumer,
} from "./collab/CollabWrapper";
import { LanguageList } from "./components/LanguageList";
import {
exportToBackend,
getCollaborationLinkData,
isCollaborationLink,
loadScene,
} from "./data";
import { exportToBackend, getCollaborationLinkData, loadScene } from "./data";
import {
getLibraryItemsFromStorage,
importFromLocalStorage,
importUsernameFromLocalStorage,
} from "./data/localStorage";
import CustomStats from "./CustomStats";
import { restore, restoreAppState, RestoredDataState } from "../data/restore";
import { restoreAppState, RestoredDataState } from "../data/restore";
import { Tooltip } from "../components/Tooltip";
import { shield } from "../components/icons";
@@ -78,13 +72,8 @@ import { loadFilesFromFirebase } from "./data/firebase";
import { LocalData } from "./data/LocalData";
import { isBrowserStorageStateNewer } from "./data/tabSync";
import clsx from "clsx";
import { Provider, useAtom } from "jotai";
import { jotaiStore, useAtomWithInitialValue } from "../jotai";
import { reconcileElements } from "./collab/reconciliation";
import { parseLibraryTokensFromUrl, useHandleLibrary } from "../data/library";
window.EXCALIDRAW_THROTTLE_RENDER = true;
const isExcalidrawPlusSignedUser = document.cookie.includes(
COOKIES.AUTH_STATE_COOKIE,
);
@@ -181,7 +170,7 @@ const initializeScene = async (opts: {
if (roomLinkData) {
return {
scene: await opts.collabAPI.startCollaboration(roomLinkData),
scene: await opts.collabAPI.initializeSocketClient(roomLinkData),
isExternalScene: true,
id: roomLinkData.roomId,
key: roomLinkData.roomKey,
@@ -253,11 +242,7 @@ const ExcalidrawWrapper = () => {
const [excalidrawAPI, excalidrawRefCallback] =
useCallbackRefState<ExcalidrawImperativeAPI>();
const [collabAPI] = useAtom(collabAPIAtom);
const [, setCollabDialogShown] = useAtom(collabDialogShownAtom);
const [isCollaborating] = useAtomWithInitialValue(isCollaboratingAtom, () => {
return isCollaborationLink(window.location.href);
});
const collabAPI = useContext(CollabContext)?.api;
useHandleLibrary({
excalidrawAPI,
@@ -335,44 +320,21 @@ const ExcalidrawWrapper = () => {
}
};
initializeScene({ collabAPI }).then(async (data) => {
initializeScene({ collabAPI }).then((data) => {
loadImages(data, /* isInitialLoad */ true);
initialStatePromiseRef.current.promise.resolve({
...data.scene,
// at this point the state may have already been updated (e.g. when
// collaborating, we may have received updates from other clients)
appState: restoreAppState(
data.scene?.appState,
excalidrawAPI.getAppState(),
),
elements: reconcileElements(
data.scene?.elements || [],
excalidrawAPI.getSceneElementsIncludingDeleted(),
excalidrawAPI.getAppState(),
),
});
initialStatePromiseRef.current.promise.resolve(data.scene);
});
const onHashChange = async (event: HashChangeEvent) => {
event.preventDefault();
const libraryUrlTokens = parseLibraryTokensFromUrl();
if (!libraryUrlTokens) {
if (
collabAPI.isCollaborating() &&
!isCollaborationLink(window.location.href)
) {
collabAPI.stopCollaboration(false);
}
excalidrawAPI.updateScene({ appState: { isLoading: true } });
initializeScene({ collabAPI }).then((data) => {
loadImages(data);
if (data.scene) {
excalidrawAPI.updateScene({
...data.scene,
...restore(data.scene, null, null),
commitToHistory: true,
appState: restoreAppState(data.scene.appState, null),
});
}
});
@@ -660,7 +622,7 @@ const ExcalidrawWrapper = () => {
const renderCustomStats = () => {
return (
<CustomStats
setToast={(message) => excalidrawAPI!.setToast({ message })}
setToastMessage={(message) => excalidrawAPI!.setToastMessage(message)}
/>
);
};
@@ -674,19 +636,23 @@ const ExcalidrawWrapper = () => {
localStorage.setItem(STORAGE_KEYS.LOCAL_STORAGE_LIBRARY, serializedItems);
};
const onRoomClose = useCallback(() => {
LocalData.fileStorage.reset();
}, []);
return (
<div
style={{ height: "100%" }}
className={clsx("excalidraw-app", {
"is-collaborating": isCollaborating,
"is-collaborating": collabAPI?.isCollaborating(),
})}
>
<Excalidraw
ref={excalidrawRefCallback}
onChange={onChange}
initialData={initialStatePromiseRef.current.promise}
onCollabButtonClick={() => setCollabDialogShown(true)}
isCollaborating={isCollaborating}
onCollabButtonClick={collabAPI?.onCollabButtonClick}
isCollaborating={collabAPI?.isCollaborating()}
onPointerUpdate={collabAPI?.onPointerUpdate}
UIOptions={{
canvasActions: {
@@ -720,7 +686,12 @@ const ExcalidrawWrapper = () => {
onLibraryChange={onLibraryChange}
autoFocus={true}
/>
{excalidrawAPI && <Collab excalidrawAPI={excalidrawAPI} />}
{excalidrawAPI && (
<CollabWrapper
excalidrawAPI={excalidrawAPI}
onRoomClose={onRoomClose}
/>
)}
{errorMessage && (
<ErrorDialog
message={errorMessage}
@@ -734,9 +705,9 @@ const ExcalidrawWrapper = () => {
const ExcalidrawApp = () => {
return (
<TopErrorBoundary>
<Provider unstable_createStore={() => jotaiStore}>
<CollabContextConsumer>
<ExcalidrawWrapper />
</Provider>
</CollabContextConsumer>
</TopErrorBoundary>
);
};

1
src/global.d.ts vendored
View File

@@ -14,7 +14,6 @@ interface Window {
__EXCALIDRAW_SHA__: string | undefined;
EXCALIDRAW_ASSET_PATH: string | undefined;
EXCALIDRAW_EXPORT_SOURCE: string;
EXCALIDRAW_THROTTLE_RENDER: boolean | undefined;
gtag: Function;
}

View File

@@ -48,13 +48,10 @@ const allLanguages: Language[] = [
{ code: "ru-RU", label: "Русский" },
{ code: "sk-SK", label: "Slovenčina" },
{ code: "sv-SE", label: "Svenska" },
{ code: "sl-SI", label: "Slovenščina" },
{ code: "tr-TR", label: "Türkçe" },
{ code: "uk-UA", label: "Українська" },
{ code: "zh-CN", label: "简体中文" },
{ code: "zh-TW", label: "繁體中文" },
{ code: "vi-VN", label: "Tiếng Việt" },
{ code: "mr-IN", label: "मराठी" },
].concat([defaultLang]);
export const languages: Language[] = allLanguages
@@ -89,7 +86,7 @@ export const setLanguage = async (lang: Language) => {
currentLangData = {};
} else {
currentLangData = await import(
/* webpackChunkName: "locales/[request]" */ `./locales/${currentLang.code}.json`
/* webpackChunkName: "i18n-[request]" */ `./locales/${currentLang.code}.json`
);
}
};

View File

@@ -1,27 +1,4 @@
import { unstable_createStore, useAtom, WritableAtom } from "jotai";
import { useLayoutEffect } from "react";
import { unstable_createStore } from "jotai";
export const jotaiScope = Symbol();
export const jotaiStore = unstable_createStore();
export const useAtomWithInitialValue = <
T extends unknown,
A extends WritableAtom<T, T>,
>(
atom: A,
initialValue: T | (() => T),
) => {
const [value, setValue] = useAtom(atom);
useLayoutEffect(() => {
if (typeof initialValue === "function") {
// @ts-ignore
setValue(initialValue());
} else {
setValue(initialValue);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return [value, setValue] as const;
};

View File

@@ -120,13 +120,7 @@
"lockAll": "",
"unlockAll": ""
},
"statusPublished": "",
"sidebarLock": ""
},
"library": {
"noItems": "",
"hint_emptyLibrary": "",
"hint_emptyPrivateLibrary": ""
"statusPublished": ""
},
"buttons": {
"clearReset": "إعادة تعيين اللوحة",
@@ -187,8 +181,7 @@
"invalidSceneUrl": "تعذر استيراد المشهد من عنوان URL المتوفر. إما أنها مشوهة، أو لا تحتوي على بيانات Excalidraw JSON صالحة.",
"resetLibrary": "هذا سوف يمسح مكتبتك. هل أنت متأكد؟",
"removeItemsFromsLibrary": "حذف {{count}} عنصر (عناصر) من المكتبة؟",
"invalidEncryptionKey": "مفتاح التشفير يجب أن يكون من 22 حرفاً. التعاون المباشر معطل.",
"browserZoom": ""
"invalidEncryptionKey": "مفتاح التشفير يجب أن يكون من 22 حرفاً. التعاون المباشر معطل."
},
"errors": {
"unsupportedFileType": "نوع الملف غير مدعوم.",

View File

@@ -120,13 +120,7 @@
"lockAll": "",
"unlockAll": ""
},
"statusPublished": "",
"sidebarLock": ""
},
"library": {
"noItems": "",
"hint_emptyLibrary": "",
"hint_emptyPrivateLibrary": ""
"statusPublished": ""
},
"buttons": {
"clearReset": "Нулиране на платно",
@@ -187,8 +181,7 @@
"invalidSceneUrl": "",
"resetLibrary": "",
"removeItemsFromsLibrary": "",
"invalidEncryptionKey": "",
"browserZoom": ""
"invalidEncryptionKey": ""
},
"errors": {
"unsupportedFileType": "Този файлов формат не се поддържа.",

View File

@@ -120,13 +120,7 @@
"lockAll": "",
"unlockAll": ""
},
"statusPublished": "",
"sidebarLock": ""
},
"library": {
"noItems": "",
"hint_emptyLibrary": "",
"hint_emptyPrivateLibrary": ""
"statusPublished": ""
},
"buttons": {
"clearReset": "",
@@ -187,8 +181,7 @@
"invalidSceneUrl": "",
"resetLibrary": "",
"removeItemsFromsLibrary": "",
"invalidEncryptionKey": "",
"browserZoom": ""
"invalidEncryptionKey": ""
},
"errors": {
"unsupportedFileType": "",

View File

@@ -9,7 +9,7 @@
"copy": "Copia",
"copyAsPng": "Copia al porta-retalls com a PNG",
"copyAsSvg": "Copia al porta-retalls com a SVG",
"copyText": "Copia al porta-retalls com a text",
"copyText": "",
"bringForward": "Porta endavant",
"sendToBack": "Envia enrere",
"bringToFront": "Porta al davant",
@@ -115,18 +115,12 @@
"label": "Enllaç"
},
"elementLock": {
"lock": "Bloca",
"unlock": "Desbloca",
"lockAll": "Bloca-ho tot",
"unlockAll": "Desbloca-ho tot"
"lock": "",
"unlock": "",
"lockAll": "",
"unlockAll": ""
},
"statusPublished": "Publicat",
"sidebarLock": ""
},
"library": {
"noItems": "",
"hint_emptyLibrary": "",
"hint_emptyPrivateLibrary": ""
"statusPublished": ""
},
"buttons": {
"clearReset": "Neteja el llenç",
@@ -174,7 +168,7 @@
"couldNotLoadInvalidFile": "No s'ha pogut carregar un fitxer no vàlid",
"importBackendFailed": "Importació fallida.",
"cannotExportEmptyCanvas": "No es pot exportar un llenç buit.",
"couldNotCopyToClipboard": "No s'ha pogut copiar al porta-retalls.",
"couldNotCopyToClipboard": "",
"decryptFailed": "No s'ha pogut desencriptar.",
"uploadedSecurly": "La càrrega s'ha assegurat amb xifratge punta a punta, cosa que significa que el servidor Excalidraw i tercers no poden llegir el contingut.",
"loadSceneOverridePrompt": "Si carregas aquest dibuix extern, substituirá el que tens. Vols continuar?",
@@ -187,8 +181,7 @@
"invalidSceneUrl": "No s'ha pogut importar l'escena des de l'adreça URL proporcionada. Està malformada o no conté dades Excalidraw JSON vàlides.",
"resetLibrary": "Això buidarà la biblioteca. N'esteu segur?",
"removeItemsFromsLibrary": "Suprimir {{count}} element(s) de la biblioteca?",
"invalidEncryptionKey": "La clau d'encriptació ha de tenir 22 caràcters. La col·laboració en directe està desactivada.",
"browserZoom": ""
"invalidEncryptionKey": "La clau d'encriptació ha de tenir 22 caràcters. La col·laboració en directe està desactivada."
},
"errors": {
"unsupportedFileType": "Tipus de fitxer no suportat.",
@@ -197,7 +190,7 @@
"svgImageInsertError": "No ha estat possible inserir la imatge SVG. Les marques SVG semblen invàlides.",
"invalidSVGString": "SVG no vàlid.",
"cannotResolveCollabServer": "",
"importLibraryError": "No s'ha pogut carregar la biblioteca"
"importLibraryError": ""
},
"toolBar": {
"selection": "Selecció",
@@ -213,7 +206,7 @@
"lock": "Mantenir activa l'eina seleccionada desprès de dibuixar",
"penMode": "Evita el zoom i accepta solament el dibuix lliure amb bolígraf",
"link": "Afegeix / actualitza l'enllaç per a la forma seleccionada",
"eraser": "Esborrador"
"eraser": ""
},
"headings": {
"canvasActions": "Accions del llenç",
@@ -239,7 +232,7 @@
"publishLibrary": "Publiqueu la vostra pròpia llibreria",
"bindTextToElement": "Premeu enter per a afegir-hi text",
"deepBoxSelect": "Manteniu CtrlOrCmd per a selecció profunda, i per a evitar l'arrossegament",
"eraserRevert": "Mantingueu premuda Alt per a revertir els elements seleccionats per a esborrar"
"eraserRevert": ""
},
"canvasError": {
"cannotShowPreview": "No es pot mostrar la previsualització",
@@ -299,7 +292,7 @@
"howto": "Seguiu les nostres guies",
"or": "o",
"preventBinding": "Prevenir vinculació de la fletxa",
"tools": "Eines",
"tools": "",
"shortcuts": "Dreceres de teclat",
"textFinish": "Finalitza l'edició (editor de text)",
"textNewLine": "Afegeix una línia nova (editor de text)",
@@ -350,7 +343,7 @@
},
"noteItems": "Cada element de la biblioteca ha de tenir el seu propi nom per tal que sigui filtrable. S'hi inclouran els elements següents:",
"atleastOneLibItem": "Si us plau, seleccioneu si més no un element de la biblioteca per a començar",
"republishWarning": "Nota: alguns dels elements seleccionats s'han marcat com a publicats/enviats. Només hauríeu de reenviar elements quan actualitzeu una biblioteca existent."
"republishWarning": ""
},
"publishSuccessDialog": {
"title": "Biblioteca enviada",

View File

@@ -9,7 +9,7 @@
"copy": "Kopírovat",
"copyAsPng": "Zkopírovat do schránky jako PNG",
"copyAsSvg": "Zkopírovat do schránky jako SVG",
"copyText": "Zkopírovat do schránky jako text",
"copyText": "",
"bringForward": "Přenést blíž",
"sendToBack": "Přenést do pozadí",
"bringToFront": "Přenést do popředí",
@@ -36,14 +36,14 @@
"arrowhead_arrow": "Šipka",
"arrowhead_bar": "Kóta",
"arrowhead_dot": "Tečka",
"arrowhead_triangle": "Trojúhelník",
"arrowhead_triangle": "",
"fontSize": "Velikost písma",
"fontFamily": "Písmo",
"onlySelected": "Pouze vybrané",
"withBackground": "Pozadí",
"exportEmbedScene": "Vložit scénu",
"exportEmbedScene_details": "Data scény budou uložena do exportovaného souboru PNG/SVG tak, aby z něj mohla být scéna obnovena.\nZvýší se velikost exportovaného souboru.",
"addWatermark": "Přidat \"Vyrobeno s Excalidraw\"",
"withBackground": "",
"exportEmbedScene": "",
"exportEmbedScene_details": "",
"addWatermark": "",
"handDrawn": "Od ruky",
"normal": "Normální",
"code": "Kód",
@@ -120,13 +120,7 @@
"lockAll": "",
"unlockAll": ""
},
"statusPublished": "",
"sidebarLock": ""
},
"library": {
"noItems": "",
"hint_emptyLibrary": "",
"hint_emptyPrivateLibrary": ""
"statusPublished": ""
},
"buttons": {
"clearReset": "",
@@ -187,8 +181,7 @@
"invalidSceneUrl": "",
"resetLibrary": "",
"removeItemsFromsLibrary": "",
"invalidEncryptionKey": "",
"browserZoom": ""
"invalidEncryptionKey": ""
},
"errors": {
"unsupportedFileType": "",

View File

@@ -120,13 +120,7 @@
"lockAll": "",
"unlockAll": ""
},
"statusPublished": "",
"sidebarLock": ""
},
"library": {
"noItems": "",
"hint_emptyLibrary": "",
"hint_emptyPrivateLibrary": ""
"statusPublished": ""
},
"buttons": {
"clearReset": "",
@@ -187,8 +181,7 @@
"invalidSceneUrl": "",
"resetLibrary": "",
"removeItemsFromsLibrary": "",
"invalidEncryptionKey": "",
"browserZoom": ""
"invalidEncryptionKey": ""
},
"errors": {
"unsupportedFileType": "",

View File

@@ -120,13 +120,7 @@
"lockAll": "Alle sperren",
"unlockAll": "Alle entsperren"
},
"statusPublished": "Veröffentlicht",
"sidebarLock": "Seitenleiste offen lassen"
},
"library": {
"noItems": "Noch keine Elemente hinzugefügt...",
"hint_emptyLibrary": "Wähle ein Element auf der Zeichenfläche, um es hier hinzuzufügen. Oder installiere eine Bibliothek aus dem öffentlichen Verzeichnis.",
"hint_emptyPrivateLibrary": "Wähle ein Element von der Zeichenfläche, um es hier hinzuzufügen."
"statusPublished": ""
},
"buttons": {
"clearReset": "Zeichenfläche löschen & Hintergrundfarbe zurücksetzen",
@@ -187,8 +181,7 @@
"invalidSceneUrl": "Die Szene konnte nicht von der angegebenen URL importiert werden. Sie ist entweder fehlerhaft oder enthält keine gültigen Excalidraw JSON-Daten.",
"resetLibrary": "Dieses löscht deine Bibliothek. Bist du sicher?",
"removeItemsFromsLibrary": "{{count}} Element(e) aus der Bibliothek löschen?",
"invalidEncryptionKey": "Verschlüsselungsschlüssel muss 22 Zeichen lang sein. Die Live-Zusammenarbeit ist deaktiviert.",
"browserZoom": "Die Zoomstufe Deines Browsers ist nicht auf 100% gesetzt, was dazu führen kann, dass der Zeichenbereich falsch angezeigt wird"
"invalidEncryptionKey": "Verschlüsselungsschlüssel muss 22 Zeichen lang sein. Die Live-Zusammenarbeit ist deaktiviert."
},
"errors": {
"unsupportedFileType": "Nicht unterstützter Dateityp.",
@@ -350,7 +343,7 @@
},
"noteItems": "Jedes Bibliothekselement muss einen eigenen Namen haben, damit es gefiltert werden kann. Die folgenden Bibliothekselemente werden hinzugefügt:",
"atleastOneLibItem": "Bitte wähle mindestens ein Bibliothekselement aus, um zu beginnen",
"republishWarning": "Hinweis: Einige der ausgewählten Elemente sind bereits als veröffentlicht/eingereicht markiert. Du solltest Elemente nur erneut einreichen, wenn Du eine existierende Bibliothek oder Einreichung aktualisierst."
"republishWarning": ""
},
"publishSuccessDialog": {
"title": "Bibliothek übermittelt",

View File

@@ -120,13 +120,7 @@
"lockAll": "",
"unlockAll": ""
},
"statusPublished": "",
"sidebarLock": ""
},
"library": {
"noItems": "",
"hint_emptyLibrary": "",
"hint_emptyPrivateLibrary": ""
"statusPublished": ""
},
"buttons": {
"clearReset": "Επαναφορά του καμβά",
@@ -187,8 +181,7 @@
"invalidSceneUrl": "",
"resetLibrary": "Αυτό θα καθαρίσει τη βιβλιοθήκη σας. Είστε σίγουροι;",
"removeItemsFromsLibrary": "",
"invalidEncryptionKey": "Το κλειδί κρυπτογράφησης πρέπει να είναι 22 χαρακτήρες. Η ζωντανή συνεργασία είναι απενεργοποιημένη.",
"browserZoom": ""
"invalidEncryptionKey": "Το κλειδί κρυπτογράφησης πρέπει να είναι 22 χαρακτήρες. Η ζωντανή συνεργασία είναι απενεργοποιημένη."
},
"errors": {
"unsupportedFileType": "Μη υποστηριζόμενος τύπος αρχείου.",

View File

@@ -187,8 +187,7 @@
"invalidSceneUrl": "Couldn't import scene from the supplied URL. It's either malformed, or doesn't contain valid Excalidraw JSON data.",
"resetLibrary": "This will clear your library. Are you sure?",
"removeItemsFromsLibrary": "Delete {{count}} item(s) from library?",
"invalidEncryptionKey": "Encryption key must be of 22 characters. Live collaboration is disabled.",
"browserZoom": "Your browser's zoom level is not set to 100% which may cause the board to display incorrectly"
"invalidEncryptionKey": "Encryption key must be of 22 characters. Live collaboration is disabled."
},
"errors": {
"unsupportedFileType": "Unsupported file type.",

View File

@@ -120,13 +120,7 @@
"lockAll": "Bloquear todo",
"unlockAll": "Desbloquear todo"
},
"statusPublished": "Publicado",
"sidebarLock": "Mantener barra lateral abierta"
},
"library": {
"noItems": "",
"hint_emptyLibrary": "Seleccione un elemento en el lienzo para añadirlo aquí, o instale una biblioteca del repositorio público, a continuación.",
"hint_emptyPrivateLibrary": "Seleccione un elemento del lienzo para añadirlo aquí."
"statusPublished": ""
},
"buttons": {
"clearReset": "Limpiar lienzo y reiniciar el color de fondo",
@@ -187,8 +181,7 @@
"invalidSceneUrl": "No se ha podido importar la escena desde la URL proporcionada. Está mal formada, o no contiene datos de Excalidraw JSON válidos.",
"resetLibrary": "Esto borrará tu biblioteca. ¿Estás seguro?",
"removeItemsFromsLibrary": "¿Eliminar {{count}} elemento(s) de la biblioteca?",
"invalidEncryptionKey": "La clave de cifrado debe tener 22 caracteres. La colaboración en vivo está deshabilitada.",
"browserZoom": "El nivel de zoom de tu navegador no está configurado al 100%, lo que puede causar que el tablero se muestre de manera incorrecta"
"invalidEncryptionKey": "La clave de cifrado debe tener 22 caracteres. La colaboración en vivo está deshabilitada."
},
"errors": {
"unsupportedFileType": "Tipo de archivo no admitido.",
@@ -350,7 +343,7 @@
},
"noteItems": "Cada elemento de la biblioteca debe tener su propio nombre para que sea filtrable. Los siguientes elementos de la biblioteca serán incluidos:",
"atleastOneLibItem": "Por favor, seleccione al menos un elemento de la biblioteca para empezar",
"republishWarning": "Nota: algunos de los elementos seleccionados están marcados como ya publicados/enviados. Sólo debería volver a enviar elementos cuando se actualice una biblioteca o envío."
"republishWarning": ""
},
"publishSuccessDialog": {
"title": "Biblioteca enviada",

View File

@@ -115,18 +115,12 @@
"label": "Esteka"
},
"elementLock": {
"lock": "Blokeatu",
"unlock": "Desblokeatu",
"lockAll": "Blokeatu guztiak",
"unlockAll": "Desblokeatu guztiak"
"lock": "",
"unlock": "",
"lockAll": "",
"unlockAll": ""
},
"statusPublished": "Argitaratua",
"sidebarLock": ""
},
"library": {
"noItems": "",
"hint_emptyLibrary": "",
"hint_emptyPrivateLibrary": ""
"statusPublished": ""
},
"buttons": {
"clearReset": "Garbitu oihala",
@@ -187,8 +181,7 @@
"invalidSceneUrl": "Ezin izan da eszena inportatu emandako URLtik. Gaizki eratuta dago edo ez du baliozko Excalidraw JSON daturik.",
"resetLibrary": "Honek zure liburutegia garbituko du. Ziur zaude?",
"removeItemsFromsLibrary": "Liburutegitik {{count}} elementu ezabatu?",
"invalidEncryptionKey": "Enkriptazio-gakoak 22 karaktere izan behar ditu. Zuzeneko lankidetza desgaituta dago.",
"browserZoom": ""
"invalidEncryptionKey": "Enkriptazio-gakoak 22 karaktere izan behar ditu. Zuzeneko lankidetza desgaituta dago."
},
"errors": {
"unsupportedFileType": "Onartu gabeko fitxategi mota.",
@@ -197,7 +190,7 @@
"svgImageInsertError": "Ezin izan da SVG irudia txertatu. SVG markak baliogabea dirudi.",
"invalidSVGString": "SVG baliogabea.",
"cannotResolveCollabServer": "Ezin izan da elkarlaneko zerbitzarira konektatu. Mesedez, berriro kargatu orria eta saiatu berriro.",
"importLibraryError": "Ezin izan da liburutegia kargatu"
"importLibraryError": ""
},
"toolBar": {
"selection": "Hautapena",
@@ -307,7 +300,7 @@
"view": "Bistaratu",
"zoomToFit": "Egin zoom elementu guztiak ikusteko",
"zoomToSelection": "Zooma hautapenera",
"toggleElementLock": "Blokeatu/desbloketatu hautapena"
"toggleElementLock": ""
},
"clearCanvasDialog": {
"title": "Garbitu oihala"
@@ -350,7 +343,7 @@
},
"noteItems": "Liburutegiko elementu bakoitzak bere izena eduki behar du iragazi ahal izateko. Liburutegiko hurrengo elementuak barne daude:",
"atleastOneLibItem": "Hautatu gutxienez liburutegiko elementu bat gutxienez hasi ahal izateko",
"republishWarning": "Oharra: hautatutako elementu batzuk dagoeneko argitaratuta/bidalita bezala markatuta daude. Elementuak berriro bidali behar dituzu lehendik dagoen liburutegi edo bidalketa eguneratzen duzunean."
"republishWarning": ""
},
"publishSuccessDialog": {
"title": "Liburutegia bidali da",

View File

@@ -9,7 +9,7 @@
"copy": "کپی",
"copyAsPng": "کپی در حافطه موقت به صورت PNG",
"copyAsSvg": "کپی در حافطه موقت به صورت SVG",
"copyText": "کپی در حافطه موقت به صورت متن",
"copyText": "",
"bringForward": "جلو آوردن",
"sendToBack": "پس فرستادن",
"bringToFront": "جلو آوردن",
@@ -36,12 +36,12 @@
"arrowhead_arrow": "پیکان",
"arrowhead_bar": "میله ای",
"arrowhead_dot": "نقطه",
"arrowhead_triangle": "مثلث",
"arrowhead_triangle": "",
"fontSize": "اندازه قلم",
"fontFamily": "نوع قلم",
"onlySelected": "فقط انتخاب شده ها",
"withBackground": "پس زمینه",
"exportEmbedScene": "تعبیه صحنه",
"exportEmbedScene": "",
"exportEmbedScene_details": "متحوای صحنه به فایل خروجی SVG/PNG اضافه خواهد شد برای بازیابی صحنه به آن اضافه خواهد شد.\nباعث افزایش حجم فایل خروجی میشود.",
"addWatermark": "\"ساخته شده با Excalidraw\" را اضافه کن",
"handDrawn": "دست نویس",
@@ -65,13 +65,13 @@
"cartoonist": "کارتونیست",
"fileTitle": "نام فایل",
"colorPicker": "انتخابگر رنگ",
"canvasColors": "رنگ های بوم",
"canvasColors": "",
"canvasBackground": "بوم",
"drawingCanvas": "بوم نقاشی",
"layers": "لایه ها",
"actions": "عملیات",
"language": "زبان",
"liveCollaboration": "همکاری زنده",
"liveCollaboration": "",
"duplicateSelection": "تکرار",
"untitled": "بدون عنوان",
"name": "نام",
@@ -98,35 +98,29 @@
"flipHorizontal": "چرخش افقی",
"flipVertical": "چرخش عمودی",
"viewMode": "حالت نمایش",
"toggleExportColorScheme": "تغییر طرح خروجی رنگ",
"toggleExportColorScheme": "",
"share": "اشتراک‌گذاری",
"showStroke": "نمایش انتخاب کننده رنگ حاشیه",
"showBackground": "نمایش انتخاب کننده رنگ پس زمینه",
"toggleTheme": "تغییر تم",
"personalLib": "کتابخانه شخصی",
"excalidrawLib": "کتابخانه",
"decreaseFontSize": "کاهش اندازه فونت",
"increaseFontSize": "افزایش دادن اندازه فونت",
"unbindText": "بازکردن نوشته",
"bindText": "بستن نوشته",
"personalLib": "",
"excalidrawLib": "",
"decreaseFontSize": "",
"increaseFontSize": "",
"unbindText": "",
"bindText": "",
"link": {
"edit": "ویرایش لینک",
"create": "ایجاد پیوند",
"label": "لینک"
"edit": "",
"create": "",
"label": ""
},
"elementLock": {
"lock": "قفل",
"unlock": "باز کردن",
"lockAll": "قفل همه",
"unlockAll": "باز کردن قفل همه"
"lock": "",
"unlock": "",
"lockAll": "",
"unlockAll": ""
},
"statusPublished": "منتشر شده",
"sidebarLock": ""
},
"library": {
"noItems": "",
"hint_emptyLibrary": "",
"hint_emptyPrivateLibrary": ""
"statusPublished": ""
},
"buttons": {
"clearReset": "پاکسازی بوم نقاشی",
@@ -153,19 +147,19 @@
"edit": "ویرایش",
"undo": "بازگرد",
"redo": "از سر",
"resetLibrary": "تنظیم مجدد کتابخانه",
"resetLibrary": "",
"createNewRoom": "ایجاد یک اتاق جدید",
"fullScreen": "تمام‌صفحه",
"darkMode": "حالت تیره",
"lightMode": "حالت روشن",
"zenMode": "حالت ذن",
"exitZenMode": "خروج از حالت تمرکز",
"cancel": "لغو",
"clear": "پاک کردن",
"remove": "پاک کردن",
"publishLibrary": "انتشار",
"submit": "ارسال",
"confirm": "تایید"
"cancel": "",
"clear": "",
"remove": "",
"publishLibrary": "",
"submit": "",
"confirm": ""
},
"alerts": {
"clearReset": "این کار کل صفحه را پاک میکند. آیا مطمئنید؟",
@@ -174,34 +168,33 @@
"couldNotLoadInvalidFile": "عدم توانایی در بازگذاری فایل نامعتبر",
"importBackendFailed": "بارگیری از پشت صحنه با شکست مواجه شد.",
"cannotExportEmptyCanvas": "بوم خالی قابل تبدیل نیست.",
"couldNotCopyToClipboard": "به کلیپ بورد کپی نشد.",
"couldNotCopyToClipboard": "",
"decryptFailed": "رمزگشایی داده ها امکان پذیر نیست.",
"uploadedSecurly": "آپلود با رمزگذاری دو طرفه انجام میشود، به این معنی که سرور Excalidraw و اشخاص ثالث نمی توانند مطالب شما را بخوانند.",
"loadSceneOverridePrompt": "بارگزاری یک طرح خارجی محتوای فعلی رو از بین میبرد. آیا میخواهید ادامه دهید؟",
"collabStopOverridePrompt": "با توقف بوم نقاشی، نقشه قبلی و ذخیره شده محلی شما را بازنویسی می کند. مطمئنی؟\n\n(اگر می خواهید نقاشی محلی خود را حفظ کنید، به سادگی برگه مرورگر را ببندید.)",
"collabStopOverridePrompt": "",
"errorAddingToLibrary": "مورد به کتابخانه اضافه نشد",
"errorRemovingFromLibrary": "مورد از کتابخانه حذف نشد",
"confirmAddLibrary": "{{numShapes}} از اشکال به کتابخانه شما اضافه خواهد شد. مطمئن هستید؟",
"imageDoesNotContainScene": "به نظر نمی رسد این تصویر حاوی داده های بوم نقاشی باشد. آیا جاسازی صحنه را در حین خروجی فعال کرده اید?",
"imageDoesNotContainScene": "",
"cannotRestoreFromImage": "صحنه را نمی توان از این فایل تصویری بازیابی کرد",
"invalidSceneUrl": "بوم نقاشی از آدرس ارائه شده وارد نشد. این یا نادرست است، یا حاوی داده Excalidraw JSON معتبر نیست.",
"resetLibrary": "ین کار کل صفحه را پاک میکند. آیا مطمئنید?",
"removeItemsFromsLibrary": "حذف {{count}} آیتم(ها) از کتابخانه?",
"invalidEncryptionKey": "کلید رمزگذاری باید 22 کاراکتر باشد. همکاری زنده غیرفعال است.",
"browserZoom": ""
"invalidSceneUrl": "",
"resetLibrary": "",
"removeItemsFromsLibrary": "",
"invalidEncryptionKey": ""
},
"errors": {
"unsupportedFileType": "نوع فایل پشتیبانی نشده.",
"imageInsertError": "عکس ارسال نشد. بعداً دوباره تلاش کنید...",
"fileTooBig": "فایل خیلی بزرگ است حداکثر اندازه مجاز {{maxSize}}.",
"svgImageInsertError": "تصویر SVG وارد نشد. نشانه گذاری SVG نامعتبر به نظر می رسد.",
"invalidSVGString": "SVG نادرست.",
"cannotResolveCollabServer": "به سرور collab متصل نشد. لطفا صفحه را مجددا بارگذاری کنید و دوباره تلاش کنید.",
"importLibraryError": "داده‌ها بارگذاری نشدند"
"unsupportedFileType": "",
"imageInsertError": "",
"fileTooBig": "",
"svgImageInsertError": "",
"invalidSVGString": "",
"cannotResolveCollabServer": "",
"importLibraryError": ""
},
"toolBar": {
"selection": "گزینش",
"image": "وارد کردن تصویر",
"image": "",
"rectangle": "مستطیل",
"diamond": "لوزی",
"ellipse": "بیضی",
@@ -211,9 +204,9 @@
"text": "متن",
"library": "کتابخانه",
"lock": "ابزار انتخاب شده را بعد از کشیدن نگه دار",
"penMode": "از زوم کوچک کردن جلوگیری کنید و ورودی آزاد را فقط از قلم بپذیرید",
"link": "افزودن/به‌روزرسانی پیوند برای شکل انتخابی",
"eraser": "پاک کن"
"penMode": "",
"link": "",
"eraser": ""
},
"headings": {
"canvasActions": "عملیات روی بوم",
@@ -221,25 +214,25 @@
"shapes": "شکل‌ها"
},
"hints": {
"canvasPanning": "برای حرکت دادن بوم، چرخ ماوس یا فاصله را در حین کشیدن نگه دارید",
"canvasPanning": "",
"linearElement": "برای چند نقطه کلیک و برای یک خط بکشید",
"freeDraw": "کلیک کنید و بکشید و وقتی کار تمام شد رها کنید",
"text": "نکته: با برنامه انتخاب شده شما میتوانید با دوبار کلیک کردن هرکجا میخواید متن اظاف کنید",
"text_selected": "دوبار کلیک کنید یا Enter را فشار دهید تا نقاط را ویرایش کنید",
"text_editing": "Escape یا CtrlOrCmd+ENTER را فشار دهید تا ویرایش تمام شود",
"text_selected": "",
"text_editing": "",
"linearElementMulti": "روی آخرین نقطه کلیک کنید یا کلید ESC را بزنید یا کلید Enter را بزنید برای اتمام کار",
"lockAngle": "با نگه داشتن SHIFT هنگام چرخش می توانید زاویه ها را محدود کنید",
"resize": "می توانید با نگه داشتن SHIFT در هنگام تغییر اندازه، نسبت ها را محدود کنید،ALT را برای تغییر اندازه از مرکز نگه دارید",
"resizeImage": "با نگه داشتن SHIFT می توانید آزادانه اندازه را تغییر دهید،\nبرای تغییر اندازه از مرکز، ALT را نگه دارید",
"resizeImage": "",
"rotate": "با نگه داشتن SHIFT هنگام چرخش می توانید زاویه ها را محدود کنید",
"lineEditor_info": "دوبار کلیک کنید یا Enter را فشار دهید تا نقاط را ویرایش کنید",
"lineEditor_pointSelected": "برای حذف نقطه Delete برای کپی زدن Ctrl یا Cmd+D را بزنید و یا برای جابجایی بکشید",
"lineEditor_nothingSelected": "یک نقطه را برای ویرایش انتخاب کنید (SHIFT را برای انتخاب چندگانه نگه دارید)،\nیا Alt را نگه دارید و برای افزودن نقاط جدید کلیک کنید",
"placeImage": "برای قرار دادن تصویر کلیک کنید، یا کلیک کنید و بکشید تا اندازه آن به صورت دستی تنظیم شود",
"publishLibrary": "کتابخانه خود را منتشر کنید",
"bindTextToElement": "برای افزودن اینتر را بزنید",
"deepBoxSelect": "CtrlOrCmd را برای انتخاب عمیق و جلوگیری از کشیدن نگه دارید",
"eraserRevert": "Alt را نگه دارید تا عناصر علامت گذاری شده برای حذف برگردند"
"lineEditor_pointSelected": "",
"lineEditor_nothingSelected": "",
"placeImage": "",
"publishLibrary": "",
"bindTextToElement": "",
"deepBoxSelect": "",
"eraserRevert": ""
},
"canvasError": {
"cannotShowPreview": "پیش نمایش نشان داده نمی شود",
@@ -267,27 +260,27 @@
"desc_inProgressIntro": "جلسه همکاری آنلاین در حال انجام است.",
"desc_shareLink": "این لینک را با هر کسی که می خواهید با او همکاری کنید به اشتراک بگذارید:",
"desc_exitSession": "با پایان دادن جلسه، شما از اتاق حذف میکند، اما می توانید به صورت محلی کار خود را با بوم ادامه دهید. توجه داشته باشید که این مورد بر سایر افراد تأثیر نمی گذارد و همچنان می توانند در نسخه خود همکاری کنند.",
"shareTitle": "به یک جلسه همکاری زنده در Excalidraw بپیوندید"
"shareTitle": ""
},
"errorDialog": {
"title": "خطا"
},
"exportDialog": {
"disk_title": "ذخیره در دیسک",
"disk_details": "داده های صحنه را به فایلی که بعداً می توانید از آن وارد کنید صادر کنید.",
"disk_details": "",
"disk_button": "ذخیره در فایل",
"link_title": "لینک قابل اشتراک‌گذاری",
"link_details": "خروجی به عنوان یک پیوند فقط خواندنی.",
"link_button": "خروجی در فایل",
"excalidrawplus_description": "صحنه را در فضای کاری Excalidraw+ خود ذخیره کنید.",
"link_details": "",
"link_button": "",
"excalidrawplus_description": "",
"excalidrawplus_button": "خروجی گرفتن",
"excalidrawplus_exportError": "در حال حاضر نمی‌توان به Excalidraw+ صادر کرد..."
"excalidrawplus_exportError": ""
},
"helpDialog": {
"blog": "بلاگ ما را بخوانید",
"click": "کلیک",
"deepSelect": "انتخاب عمیق",
"deepBoxSelect": "انتخاب عمیق در کادر، و جلوگیری از کشیدن",
"deepSelect": "",
"deepBoxSelect": "",
"curvedArrow": "فلش خمیده",
"curvedLine": "منحنی",
"documentation": "مستندات",
@@ -299,7 +292,7 @@
"howto": "راهنمای ما را دنبال کنید",
"or": "یا",
"preventBinding": "مانع شدن از چسبیدن فلش ها",
"tools": "ابزار",
"tools": "",
"shortcuts": "میانبرهای صفحه کلید",
"textFinish": "پایان ویرایش (ویرایشگر متن)",
"textNewLine": "افزودن خط جدید (ویرایشگر متن)",
@@ -307,63 +300,63 @@
"view": "مشاهده",
"zoomToFit": "بزرگنمایی برای دیدن تمام آیتم ها",
"zoomToSelection": "بزرگنمایی قسمت انتخاب شده",
"toggleElementLock": "قفل/بازکردن انتخاب شده ها"
"toggleElementLock": ""
},
"clearCanvasDialog": {
"title": "پاک کردن بوم"
"title": ""
},
"publishDialog": {
"title": "انتشار کتابخانه",
"itemName": "نام آیتم",
"authorName": "نام نویسنده",
"githubUsername": "نام کاربری گیت هاب",
"twitterUsername": "نام کاربری توییتر",
"libraryName": "نام کتابخانه",
"libraryDesc": "توضیحات کتابخانه",
"website": "تارنما",
"title": "",
"itemName": "",
"authorName": "",
"githubUsername": "",
"twitterUsername": "",
"libraryName": "",
"libraryDesc": "",
"website": "",
"placeholder": {
"authorName": "نام یا نام کاربری شما",
"libraryName": "اسم کتابخانه",
"libraryDesc": "شرحی از کتابخانه شما برای کمک به مردم برای درک استفاده از آن",
"githubHandle": "دسته GitHub (اختیاری)، بنابراین می توانید پس از ارسال برای بررسی، کتابخانه را ویرایش کنید",
"twitterHandle": "نام کاربری توییتر (اختیاری)، بنابراین می دانیم هنگام تبلیغ در توییتر به چه کسی اعتبار دهیم",
"website": "پیوند به وب سایت شخصی شما یا هر جای دیگر (اختیاری)"
"authorName": "",
"libraryName": "",
"libraryDesc": "",
"githubHandle": "",
"twitterHandle": "",
"website": ""
},
"errors": {
"required": "لازم",
"website": "وارد کردن آدرس درست"
"required": "",
"website": ""
},
"noteDescription": {
"pre": "کتابخانه خود را ارسال کنید تا در آن گنجانده شود ",
"link": "مخزن کتابخانه عمومی",
"post": "تا افراد دیگر در نقاشی های خود از آن استفاده کنند."
"pre": "",
"link": "",
"post": ""
},
"noteGuidelines": {
"pre": "کتابخانه باید ابتدا به صورت دستی تایید شود. لطفاً بخوانید ",
"link": "دستورالعمل‌ها",
"post": " قبل از ارسال برای برقراری ارتباط و ایجاد تغییرات در صورت درخواست، به یک حساب GitHub نیاز دارید، اما به شدت الزامی نیست."
"pre": "",
"link": "",
"post": ""
},
"noteLicense": {
"pre": "با ارسال، موافقت می کنید که کتابخانه تحت عنوان منتشر شود ",
"link": "پروانهٔ MIT ",
"post": "که به طور خلاصه به این معنی است که هر کسی می تواند بدون محدودیت از آنها استفاده کند."
"pre": "",
"link": "",
"post": ""
},
"noteItems": "هر مورد کتابخانه باید نام خاص خود را داشته باشد تا قابل فیلتر باشد. اقلام کتابخانه زیر شامل خواهد شد:",
"atleastOneLibItem": "لطفاً حداقل یک مورد از کتابخانه را برای شروع انتخاب کنید",
"republishWarning": "توجه: برخی از موارد انتخاب شده به عنوان قبلاً منتشر شده/ارسال شده علامت گذاری شده اند. شما فقط باید هنگام به‌روزرسانی یک کتابخانه موجود یا ارسال، موارد را دوباره ارسال کنید."
"noteItems": "",
"atleastOneLibItem": "",
"republishWarning": ""
},
"publishSuccessDialog": {
"title": "کتابخانه ارسال شد",
"content": "تشکر از شما {{authorName}}. کتابخانه شما برای بررسی ارسال شده است. می توانید وضعیت را پیگیری کنید",
"link": "اینجا"
"title": "",
"content": "",
"link": ""
},
"confirmDialog": {
"resetLibrary": "تنظیم مجدد کتابخانه",
"removeItemsFromLib": "موارد انتخاب شده از موارد پسندیده حذف شوند"
"resetLibrary": "",
"removeItemsFromLib": ""
},
"encrypted": {
"tooltip": "شما در یک محیط رمزگزاری شده دو طرفه در حال طراحی هستید پس Excalidraw هرگز طرح های شما را نمیبند.",
"link": "پست وبلاگ در مورد رمزگذاری سرتاسر در Excalidraw"
"link": ""
},
"stats": {
"angle": "زاویه",
@@ -381,60 +374,60 @@
"width": "عرض"
},
"toast": {
"addedToLibrary": "به مجموعه اضافه شد",
"addedToLibrary": "",
"copyStyles": "کپی سبک.",
"copyToClipboard": "در کلیپ‌بورد کپی شد.",
"copyToClipboardAsPng": "کپی {{exportSelection}} در کلیپبورد به عنوان PNG\n({{exportColorScheme}})",
"copyToClipboardAsPng": "",
"fileSaved": "فایل ذخیره شد.",
"fileSavedToFilename": "ذخیره در {filename}",
"canvas": "بوم",
"selection": "انتخاب"
},
"colors": {
"ffffff": "سفید",
"f8f9fa": "خاکستری 0",
"f1f3f5": "خاکستری 1",
"fff5f5": "قرمز 0",
"fff0f6": "صورتی 0",
"f8f0fc": "انگوری 0",
"f3f0ff": "بنفش 0",
"edf2ff": "نیلی 0",
"e7f5ff": "آبی 0",
"e3fafc": "آبی نفتی 0",
"e6fcf5": "آبی فیروزه ای 0",
"ebfbee": "سبز 0",
"f4fce3": "لیمویی 0",
"fff9db": "زرد 0",
"fff4e6": "نارنجی 0",
"transparent": "شفاف",
"ced4da": "خاکستری 4",
"868e96": "خاکستری 6",
"fa5252": "قرمز 6",
"e64980": "صورتی 6",
"be4bdb": "انگوری 6",
"7950f2": "بنفش 6",
"4c6ef5": "نیلی 6",
"228be6": "آبی 6",
"15aabf": "آبی نفتی 6",
"12b886": "آبی فیروزه ای 6",
"40c057": "سبز 6",
"82c91e": "لیمویی 6",
"fab005": "زرد 6",
"fd7e14": "نارنجی 6",
"000000": "سیاه",
"343a40": "خاکستری 8",
"495057": "خاکستری 7",
"c92a2a": "قرمز 9",
"a61e4d": "صورتی 9",
"862e9c": "انگوری 9",
"5f3dc4": "بنفش 9",
"364fc7": "نیلی 9",
"1864ab": "آبی 9",
"0b7285": "آبی نفتی 9",
"087f5b": "آبی فیروزه ای 9",
"2b8a3e": "سبز 9",
"5c940d": "لیمویی 9",
"e67700": "زرد 9",
"d9480f": "نارنجی 9"
"ffffff": "",
"f8f9fa": "",
"f1f3f5": "",
"fff5f5": "",
"fff0f6": "",
"f8f0fc": "",
"f3f0ff": "",
"edf2ff": "",
"e7f5ff": "",
"e3fafc": "",
"e6fcf5": "",
"ebfbee": "",
"f4fce3": "",
"fff9db": "",
"fff4e6": "",
"transparent": "",
"ced4da": "",
"868e96": "",
"fa5252": "",
"e64980": "",
"be4bdb": "",
"7950f2": "",
"4c6ef5": "",
"228be6": "",
"15aabf": "",
"12b886": "",
"40c057": "",
"82c91e": "",
"fab005": "",
"fd7e14": "",
"000000": "",
"343a40": "",
"495057": "",
"c92a2a": "",
"a61e4d": "",
"862e9c": "",
"5f3dc4": "",
"364fc7": "",
"1864ab": "",
"0b7285": "",
"087f5b": "",
"2b8a3e": "",
"5c940d": "",
"e67700": "",
"d9480f": ""
}
}

View File

@@ -120,13 +120,7 @@
"lockAll": "",
"unlockAll": ""
},
"statusPublished": "Julkaistu",
"sidebarLock": "Pidä sivupalkki avoinna"
},
"library": {
"noItems": "Kirjastossa ei ole vielä yhtään kohdetta...",
"hint_emptyLibrary": "Valitse lisättävä kohde piirtoalueelta, tai asenna alta julkinen kirjasto.",
"hint_emptyPrivateLibrary": "Valitse lisättävä kohde piirtoalueelta."
"statusPublished": "Julkaistu"
},
"buttons": {
"clearReset": "Tyhjennä piirtoalue",
@@ -187,8 +181,7 @@
"invalidSceneUrl": "Teosta ei voitu tuoda annetusta URL-osoitteesta. Tallenne on vioittunut, tai osoitteessa ei ole Excalidraw JSON-dataa.",
"resetLibrary": "Tämä tyhjentää kirjastosi. Jatketaanko?",
"removeItemsFromsLibrary": "Poista {{count}} kohdetta kirjastosta?",
"invalidEncryptionKey": "Salausavaimen on oltava 22 merkkiä pitkä. Live-yhteistyö ei ole käytössä.",
"browserZoom": ""
"invalidEncryptionKey": "Salausavaimen on oltava 22 merkkiä pitkä. Live-yhteistyö ei ole käytössä."
},
"errors": {
"unsupportedFileType": "Tiedostotyyppiä ei tueta.",

View File

@@ -105,28 +105,22 @@
"toggleTheme": "Changer le thème",
"personalLib": "Bibliothèque personnelle",
"excalidrawLib": "Bibliothèque Excalidraw",
"decreaseFontSize": "Diminuer la taille de police",
"decreaseFontSize": "Réduire la taille de police",
"increaseFontSize": "Augmenter la taille de police",
"unbindText": "Dissocier le texte",
"bindText": "Associer le texte au conteneur",
"unbindText": "Délier le texte",
"bindText": "Lier le texte au conteneur",
"link": {
"edit": "Modifier le lien",
"create": "Ajouter un lien",
"create": "Créer un lien",
"label": "Lien"
},
"elementLock": {
"lock": "Verrouiller",
"unlock": "Déverrouiller",
"lockAll": "Tout verrouiller",
"unlockAll": "Tout déverrouiller"
"unlockAll": "Tout déverouiller"
},
"statusPublished": "Publié",
"sidebarLock": "Maintenir la barre latérale ouverte"
},
"library": {
"noItems": "Aucun élément n'a encore été ajouté ...",
"hint_emptyLibrary": "Sélectionnez un élément sur le canevas pour l'ajouter ici ou installez une bibliothèque depuis le dépôt public, ci-dessous.",
"hint_emptyPrivateLibrary": "Sélectionnez un élément sur le canevas pour l'ajouter ici."
"statusPublished": ""
},
"buttons": {
"clearReset": "Réinitialiser le canevas",
@@ -187,8 +181,7 @@
"invalidSceneUrl": "Impossible d'importer la scène depuis l'URL fournie. Elle est soit incorrecte, soit ne contient pas de données JSON Excalidraw valides.",
"resetLibrary": "Cela va effacer votre bibliothèque. Êtes-vous sûr·e ?",
"removeItemsFromsLibrary": "Supprimer {{count}} élément(s) de la bibliothèque ?",
"invalidEncryptionKey": "La clé de chiffrement doit comporter 22 caractères. La collaboration en direct est désactivée.",
"browserZoom": "Le niveau de zoom de votre navigateur n'est pas défini sur 100 %, ce qui peut entraîner un affichage incorrect du tableau"
"invalidEncryptionKey": "La clé de chiffrement doit comporter 22 caractères. La collaboration en direct est désactivée."
},
"errors": {
"unsupportedFileType": "Type de fichier non supporté.",
@@ -350,7 +343,7 @@
},
"noteItems": "Chaque élément de la bibliothèque doit avoir son propre nom afin qu'il soit filtrable. Les éléments de bibliothèque suivants seront inclus :",
"atleastOneLibItem": "Veuillez sélectionner au moins un élément de bibliothèque pour commencer",
"republishWarning": "Remarque : certains des éléments sélectionnés sont marqués comme étant déjà publiés/soumis. Vous devez uniquement resoumettre des éléments lors de la mise à jour d'une bibliothèque ou d'une soumission existante."
"republishWarning": ""
},
"publishSuccessDialog": {
"title": "Bibliothèque soumise",

View File

@@ -1,440 +0,0 @@
{
"labels": {
"paste": "Paste",
"pasteCharts": "Pegar gráficos",
"selectAll": "Seleccionar todo",
"multiSelect": "Engadir elemento á selección",
"moveCanvas": "Mover o lenzo",
"cut": "Cortar",
"copy": "Copiar",
"copyAsPng": "Copiar no portapapeis como PNG",
"copyAsSvg": "Copiar no portapapeis como SVG",
"copyText": "Copia no portapapeis como texto",
"bringForward": "Traer cara adiante",
"sendToBack": "Enviar cara atrás",
"bringToFront": "Traer á fronte",
"sendBackward": "Enviar ao fondo",
"delete": "Borrar",
"copyStyles": "Copiar estilo",
"pasteStyles": "Pegar estilo",
"stroke": "Trazo",
"background": "Fondo",
"fill": "Recheo",
"strokeWidth": "Largo do trazo",
"strokeStyle": "Estilo do trazo",
"strokeStyle_solid": "Sólido",
"strokeStyle_dashed": "Liña de trazos",
"strokeStyle_dotted": "Liña de puntos",
"sloppiness": "Estilo de trazo",
"opacity": "Opacidade",
"textAlign": "Aliñar texto",
"edges": "Bordos",
"sharp": "Agudo",
"round": "Redondo",
"arrowheads": "Puntas de frecha",
"arrowhead_none": "Ningunha",
"arrowhead_arrow": "Frecha",
"arrowhead_bar": "Barra",
"arrowhead_dot": "Punto",
"arrowhead_triangle": "Triángulo",
"fontSize": "Tamaño da fonte",
"fontFamily": "Tipo de fonte",
"onlySelected": "Só seleccionados",
"withBackground": "Fondo",
"exportEmbedScene": "Inserir escena",
"exportEmbedScene_details": "",
"addWatermark": "Engadir \"Feito con Excalidraw\"",
"handDrawn": "Debuzado á man",
"normal": "Normal",
"code": "Código",
"small": "Pequeno",
"medium": "Mediano",
"large": "Grande",
"veryLarge": "Moi grande",
"solid": "Sólido",
"hachure": "Folleto",
"crossHatch": "Raiado transversal",
"thin": "Estreito",
"bold": "Groso",
"left": "Esquerda",
"center": "Centrado",
"right": "Dereita",
"extraBold": "Moi groso",
"architect": "Arquitecto",
"artist": "Artista",
"cartoonist": "Caricatura",
"fileTitle": "Nome do arquivo",
"colorPicker": "Selector de cor",
"canvasColors": "Usado en lenzo",
"canvasBackground": "Fondo do lenzo",
"drawingCanvas": "Lenzo de debuxo",
"layers": "Capas",
"actions": "Accións",
"language": "Idioma",
"liveCollaboration": "Colaboración en directo",
"duplicateSelection": "Duplicar",
"untitled": "Sen título",
"name": "Nome",
"yourName": "O teu nome",
"madeWithExcalidraw": "Feito con Excalidraw",
"group": "Agrupar selección",
"ungroup": "Desagrupar selección",
"collaborators": "Colaboradores",
"showGrid": "Mostrar cuadrícula",
"addToLibrary": "Engadir á biblioteca",
"removeFromLibrary": "Eliminar da biblioteca",
"libraryLoadingMessage": "Cargando biblioteca…",
"libraries": "Explorar bibliotecas",
"loadingScene": "Cargando escena…",
"align": "Aliñamento",
"alignTop": "Aliñamento superior",
"alignBottom": "Aliñamento inferior",
"alignLeft": "Aliñar a esquerda",
"alignRight": "Aliñar a dereita",
"centerVertically": "Centrar verticalmente",
"centerHorizontally": "Centrar horizontalmente",
"distributeHorizontally": "Distribuír horizontalmente",
"distributeVertically": "Distribuír verticalmente",
"flipHorizontal": "",
"flipVertical": "",
"viewMode": "Modo de visualización",
"toggleExportColorScheme": "Alternar esquema de cores de exportación",
"share": "Compartir",
"showStroke": "Mostrar selector de cores do trazo",
"showBackground": "Mostrar selector de cores do fondo",
"toggleTheme": "Alternar tema",
"personalLib": "Biblioteca Persoal",
"excalidrawLib": "Biblioteca Excalidraw",
"decreaseFontSize": "Diminuír tamaño da fonte",
"increaseFontSize": "Aumentar o tamaño da fonte",
"unbindText": "Desvincular texto",
"bindText": "Ligar o texto ao contedor",
"link": {
"edit": "Editar ligazón",
"create": "Crear ligazón",
"label": "Ligazón"
},
"elementLock": {
"lock": "Bloquear",
"unlock": "Desbloquear",
"lockAll": "Bloquear todo",
"unlockAll": "Desbloquear todo"
},
"statusPublished": "Publicado",
"sidebarLock": "Manter a barra lateral aberta"
},
"library": {
"noItems": "Aínda non hai elementos engadidos...",
"hint_emptyLibrary": "",
"hint_emptyPrivateLibrary": ""
},
"buttons": {
"clearReset": "Limpar o lenzo",
"exportJSON": "Exportar a arquivo",
"exportImage": "Gardar como imaxe",
"export": "Exportar",
"exportToPng": "Exportar a PNG",
"exportToSvg": "Exportar a SVG",
"copyToClipboard": "Copiar ao portapapeis",
"copyPngToClipboard": "Copiar PNG ao portapapeis",
"scale": "Escala",
"save": "Gardar no ficheiro actual",
"saveAs": "Gardar como",
"load": "Cargar",
"getShareableLink": "Obter unha ligazón que se poida compartir",
"close": "Pechar",
"selectLanguage": "Seleccionar idioma",
"scrollBackToContent": "Volver ao contido",
"zoomIn": "Ampliar",
"zoomOut": "Reducir",
"resetZoom": "Reiniciar zoom",
"menu": "Menú",
"done": "Feito",
"edit": "Editar",
"undo": "Desfacer",
"redo": "Refacer",
"resetLibrary": "Reiniciar biblioteca",
"createNewRoom": "Crear nova sala",
"fullScreen": "Pantalla completa",
"darkMode": "Modo escuro",
"lightMode": "Modo claro",
"zenMode": "Modo zen",
"exitZenMode": "Saír do modo zen",
"cancel": "Cancelar",
"clear": "Limpar",
"remove": "Eliminar",
"publishLibrary": "Publicar",
"submit": "Enviar",
"confirm": "Confirmar"
},
"alerts": {
"clearReset": "Isto limpará todo o lenzo. Estás seguro?",
"couldNotCreateShareableLink": "Non se puido crear unha ligazón para compartir.",
"couldNotCreateShareableLinkTooBig": "Non se puido crear a ligazón para compartir: a escena é demasiado grande",
"couldNotLoadInvalidFile": "Non se puido cargar o ficheiro non válido",
"importBackendFailed": "",
"cannotExportEmptyCanvas": "",
"couldNotCopyToClipboard": "",
"decryptFailed": "Non se poideron descifrar os datos.",
"uploadedSecurly": "A carga foi asegurada con cifrado de extremo a extremo, o que significa que o servidor de Excalidraw e terceiros non poder ler o contenido.",
"loadSceneOverridePrompt": "Si carga este debuxo externo, reemprazará o que ten. ¿Desexa continuar?",
"collabStopOverridePrompt": "",
"errorAddingToLibrary": "",
"errorRemovingFromLibrary": "",
"confirmAddLibrary": "",
"imageDoesNotContainScene": "",
"cannotRestoreFromImage": "",
"invalidSceneUrl": "",
"resetLibrary": "",
"removeItemsFromsLibrary": "",
"invalidEncryptionKey": "",
"browserZoom": ""
},
"errors": {
"unsupportedFileType": "",
"imageInsertError": "",
"fileTooBig": "",
"svgImageInsertError": "",
"invalidSVGString": "",
"cannotResolveCollabServer": "",
"importLibraryError": ""
},
"toolBar": {
"selection": "Selección",
"image": "",
"rectangle": "Rectángulo",
"diamond": "Diamante",
"ellipse": "Elipse",
"arrow": "Frecha",
"line": "Liña",
"freedraw": "Debuxar",
"text": "Texto",
"library": "Biblioteca",
"lock": "Manter a ferramenta seleccionada activa despois de debuxar",
"penMode": "",
"link": "",
"eraser": ""
},
"headings": {
"canvasActions": "Accións do lenzo",
"selectedShapeActions": "",
"shapes": ""
},
"hints": {
"canvasPanning": "",
"linearElement": "",
"freeDraw": "",
"text": "",
"text_selected": "",
"text_editing": "",
"linearElementMulti": "",
"lockAngle": "",
"resize": "",
"resizeImage": "",
"rotate": "",
"lineEditor_info": "",
"lineEditor_pointSelected": "",
"lineEditor_nothingSelected": "",
"placeImage": "",
"publishLibrary": "",
"bindTextToElement": "",
"deepBoxSelect": "",
"eraserRevert": ""
},
"canvasError": {
"cannotShowPreview": "",
"canvasTooBig": "",
"canvasTooBigTip": ""
},
"errorSplash": {
"headingMain_pre": "",
"headingMain_button": "",
"clearCanvasMessage": "",
"clearCanvasMessage_button": "",
"clearCanvasCaveat": "",
"trackedToSentry_pre": "",
"trackedToSentry_post": "",
"openIssueMessage_pre": "",
"openIssueMessage_button": "",
"openIssueMessage_post": "",
"sceneContent": ""
},
"roomDialog": {
"desc_intro": "",
"desc_privacy": "",
"button_startSession": "",
"button_stopSession": "",
"desc_inProgressIntro": "",
"desc_shareLink": "",
"desc_exitSession": "",
"shareTitle": ""
},
"errorDialog": {
"title": "Erro"
},
"exportDialog": {
"disk_title": "",
"disk_details": "",
"disk_button": "",
"link_title": "",
"link_details": "",
"link_button": "",
"excalidrawplus_description": "",
"excalidrawplus_button": "",
"excalidrawplus_exportError": ""
},
"helpDialog": {
"blog": "",
"click": "",
"deepSelect": "",
"deepBoxSelect": "",
"curvedArrow": "",
"curvedLine": "",
"documentation": "",
"doubleClick": "",
"drag": "",
"editor": "",
"editSelectedShape": "",
"github": "",
"howto": "",
"or": "",
"preventBinding": "",
"tools": "",
"shortcuts": "",
"textFinish": "",
"textNewLine": "",
"title": "",
"view": "",
"zoomToFit": "",
"zoomToSelection": "",
"toggleElementLock": ""
},
"clearCanvasDialog": {
"title": ""
},
"publishDialog": {
"title": "",
"itemName": "",
"authorName": "",
"githubUsername": "",
"twitterUsername": "",
"libraryName": "",
"libraryDesc": "",
"website": "",
"placeholder": {
"authorName": "",
"libraryName": "",
"libraryDesc": "",
"githubHandle": "",
"twitterHandle": "",
"website": ""
},
"errors": {
"required": "",
"website": ""
},
"noteDescription": {
"pre": "",
"link": "",
"post": ""
},
"noteGuidelines": {
"pre": "",
"link": "",
"post": ""
},
"noteLicense": {
"pre": "",
"link": "",
"post": ""
},
"noteItems": "",
"atleastOneLibItem": "",
"republishWarning": ""
},
"publishSuccessDialog": {
"title": "",
"content": "",
"link": ""
},
"confirmDialog": {
"resetLibrary": "",
"removeItemsFromLib": ""
},
"encrypted": {
"tooltip": "",
"link": ""
},
"stats": {
"angle": "",
"element": "",
"elements": "",
"height": "",
"scene": "",
"selected": "",
"storage": "",
"title": "",
"total": "",
"version": "",
"versionCopy": "",
"versionNotAvailable": "",
"width": ""
},
"toast": {
"addedToLibrary": "",
"copyStyles": "",
"copyToClipboard": "",
"copyToClipboardAsPng": "",
"fileSaved": "",
"fileSavedToFilename": "",
"canvas": "",
"selection": ""
},
"colors": {
"ffffff": "",
"f8f9fa": "",
"f1f3f5": "",
"fff5f5": "",
"fff0f6": "",
"f8f0fc": "",
"f3f0ff": "",
"edf2ff": "",
"e7f5ff": "",
"e3fafc": "",
"e6fcf5": "",
"ebfbee": "",
"f4fce3": "",
"fff9db": "",
"fff4e6": "",
"transparent": "",
"ced4da": "",
"868e96": "",
"fa5252": "",
"e64980": "",
"be4bdb": "",
"7950f2": "",
"4c6ef5": "",
"228be6": "",
"15aabf": "",
"12b886": "",
"40c057": "",
"82c91e": "",
"fab005": "",
"fd7e14": "",
"000000": "",
"343a40": "",
"495057": "",
"c92a2a": "",
"a61e4d": "",
"862e9c": "",
"5f3dc4": "",
"364fc7": "",
"1864ab": "",
"0b7285": "",
"087f5b": "",
"2b8a3e": "",
"5c940d": "",
"e67700": "",
"d9480f": ""
}
}

View File

@@ -120,13 +120,7 @@
"lockAll": "לנעול הכל",
"unlockAll": "שחרור הכול"
},
"statusPublished": "",
"sidebarLock": ""
},
"library": {
"noItems": "",
"hint_emptyLibrary": "",
"hint_emptyPrivateLibrary": ""
"statusPublished": ""
},
"buttons": {
"clearReset": "אפס את הלוח",
@@ -187,8 +181,7 @@
"invalidSceneUrl": "ייבוא המידע מן סצינה מכתובת האינטרנט נכשלה. המידע בנוי באופן משובש או שהוא אינו קובץ JSON תקין של Excalidraw.",
"resetLibrary": "פעולה זו תנקה את כל הלוח. אתה בטוח?",
"removeItemsFromsLibrary": "מחיקת {{count}} פריטים(ים) מתוך הספריה?",
"invalidEncryptionKey": "מפתח ההצפנה חייב להיות בן 22 תוים. השיתוף החי מבוטל.",
"browserZoom": ""
"invalidEncryptionKey": "מפתח ההצפנה חייב להיות בן 22 תוים. השיתוף החי מבוטל."
},
"errors": {
"unsupportedFileType": "סוג הקובץ אינו נתמך.",

View File

@@ -120,13 +120,7 @@
"lockAll": "सब ताले के अंदर रखे",
"unlockAll": "सब ताले के बाहर निकाले"
},
"statusPublished": "प्रकाशित",
"sidebarLock": "साइडबार खुला रखे."
},
"library": {
"noItems": "अभी तक कोई आइटम जोडा नहीं गया.",
"hint_emptyLibrary": "यहाँ जोड़ने के लिए पटल से एक वस्तु चुने, अथवा जन कोष से एक संग्रह नीचे स्थापित करें.",
"hint_emptyPrivateLibrary": "यहाँ जोड़ने के लिए पटल से एक वस्तु चुने."
"statusPublished": "प्रकाशित"
},
"buttons": {
"clearReset": "कैनवास रीसेट करें",
@@ -187,8 +181,7 @@
"invalidSceneUrl": "",
"resetLibrary": "",
"removeItemsFromsLibrary": "",
"invalidEncryptionKey": "",
"browserZoom": "आपके ब्राउज़र का ज़ूम लेवल 100% नहीं हैं इस कारण दृष्य पटल ग़लत दिख सकता हैं"
"invalidEncryptionKey": ""
},
"errors": {
"unsupportedFileType": "",

View File

@@ -120,13 +120,7 @@
"lockAll": "",
"unlockAll": ""
},
"statusPublished": "",
"sidebarLock": ""
},
"library": {
"noItems": "",
"hint_emptyLibrary": "",
"hint_emptyPrivateLibrary": ""
"statusPublished": ""
},
"buttons": {
"clearReset": "Vászon törlése",
@@ -187,8 +181,7 @@
"invalidSceneUrl": "Nem sikerült importálni a jelenetet a megadott URL-ről. Rossz formátumú, vagy nem tartalmaz érvényes Excalidraw JSON-adatokat.",
"resetLibrary": "Ezzel törlöd a könyvtárát. biztos vagy ebben?",
"removeItemsFromsLibrary": "{{count}} elemet törölsz a könyvtárból?",
"invalidEncryptionKey": "A titkosítási kulcsnak 22 karakterből kell állnia. Az élő együttműködés le van tiltva.",
"browserZoom": ""
"invalidEncryptionKey": "A titkosítási kulcsnak 22 karakterből kell állnia. Az élő együttműködés le van tiltva."
},
"errors": {
"unsupportedFileType": "Nem támogatott fájltípus.",

View File

@@ -120,13 +120,7 @@
"lockAll": "Kunci semua",
"unlockAll": "Lepas semua"
},
"statusPublished": "Telah terbit",
"sidebarLock": "Biarkan sidebar tetap terbuka"
},
"library": {
"noItems": "Belum ada item yang ditambahkan...",
"hint_emptyLibrary": "Pilih item pada kanvas untuk menambahkan nya di sini, atau pasang pustaka dari gudang di bawah ini.",
"hint_emptyPrivateLibrary": "Pilih item pada kanvas untuk menambahkan nya di sini."
"statusPublished": ""
},
"buttons": {
"clearReset": "Setel Ulang Kanvas",
@@ -187,8 +181,7 @@
"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?",
"removeItemsFromsLibrary": "Hapus {{count}} item dari pustaka?",
"invalidEncryptionKey": "Sandi enkripsi harus 22 karakter. Kolaborasi langsung dinonaktifkan.",
"browserZoom": "Pembesaran peramban Anda tidak 100% yang mana dapat menyebabkan layar tidak menampilkan dengan benar"
"invalidEncryptionKey": "Sandi enkripsi harus 22 karakter. Kolaborasi langsung dinonaktifkan."
},
"errors": {
"unsupportedFileType": "Tipe file tidak didukung.",
@@ -197,7 +190,7 @@
"svgImageInsertError": "Tidak dapat menyisipkan gambar SVG. Markup SVG sepertinya tidak valid.",
"invalidSVGString": "SVG tidak valid.",
"cannotResolveCollabServer": "Tidak dapat terhubung ke server kolab. Muat ulang laman dan coba lagi.",
"importLibraryError": "Tidak dapat memuat pustaka"
"importLibraryError": ""
},
"toolBar": {
"selection": "Pilihan",
@@ -350,7 +343,7 @@
},
"noteItems": "Setiap item pustaka harus memiliki nama, sehingga bisa disortir. Item pustaka di bawah ini akan dimasukan:",
"atleastOneLibItem": "Pilih setidaknya satu item pustaka untuk mulai",
"republishWarning": "Catatan: beberapa item yang dipilih telah ditandai sebagai sudah dipublikasikan/diserahkan. Anda hanya dapat menyerahkan kembali item-item ketika memperbarui pustaka atau pengumpulan."
"republishWarning": ""
},
"publishSuccessDialog": {
"title": "Pustaka telah dikirm",

View File

@@ -120,13 +120,7 @@
"lockAll": "Blocca tutto",
"unlockAll": "Sblocca tutto"
},
"statusPublished": "Pubblicato",
"sidebarLock": "Mantieni aperta la barra laterale"
},
"library": {
"noItems": "Nessun elemento ancora aggiunto...",
"hint_emptyLibrary": "Selezionare un elemento su tela per aggiungerlo qui, o installare una libreria dal repository pubblico, sotto.",
"hint_emptyPrivateLibrary": "Selezionare un elemento su tela per aggiungerlo qui."
"statusPublished": ""
},
"buttons": {
"clearReset": "Svuota la tela",
@@ -187,8 +181,7 @@
"invalidSceneUrl": "Impossibile importare la scena dall'URL fornito. Potrebbe essere malformato o non contenere dati JSON Excalidraw validi.",
"resetLibrary": "Questa azione cancellerà l'intera libreria. Sei sicuro?",
"removeItemsFromsLibrary": "Eliminare {{count}} elementi dalla libreria?",
"invalidEncryptionKey": "La chiave di cifratura deve essere composta da 22 caratteri. La collaborazione live è disabilitata.",
"browserZoom": "Il livello di zoom del tuo browser non è impostato al 100%, il che potrebbe causare una visualizzazione scorretta della scheda"
"invalidEncryptionKey": "La chiave di cifratura deve essere composta da 22 caratteri. La collaborazione live è disabilitata."
},
"errors": {
"unsupportedFileType": "Tipo di file non supportato.",
@@ -350,7 +343,7 @@
},
"noteItems": "Ogni elemento della libreria deve avere il proprio nome, così che sia filtrabile. Gli elementi della seguente libreria saranno inclusi:",
"atleastOneLibItem": "Sei pregato di selezionare almeno un elemento della libreria per iniziare",
"republishWarning": "Nota: alcuni degli elementi selezionati sono contrassegnati come già pubblicati/presentati. È necessario reinviare gli elementi solo quando si aggiorna una libreria o una presentazione esistente."
"republishWarning": ""
},
"publishSuccessDialog": {
"title": "Libreria inviata",

View File

@@ -9,7 +9,7 @@
"copy": "コピー",
"copyAsPng": "PNGとしてクリップボードへコピー",
"copyAsSvg": "SVGとしてクリップボードへコピー",
"copyText": "テキストとしてクリップボードにコピー",
"copyText": "",
"bringForward": "前面に移動",
"sendToBack": "最背面に移動",
"bringToFront": "最前面に移動",
@@ -120,13 +120,7 @@
"lockAll": "すべてロック",
"unlockAll": "すべてのロックを解除"
},
"statusPublished": "公開済み",
"sidebarLock": ""
},
"library": {
"noItems": "",
"hint_emptyLibrary": "",
"hint_emptyPrivateLibrary": ""
"statusPublished": ""
},
"buttons": {
"clearReset": "キャンバスのリセット",
@@ -174,7 +168,7 @@
"couldNotLoadInvalidFile": "無効なファイルを読み込めませんでした。",
"importBackendFailed": "サーバーからの読み込みに失敗しました。",
"cannotExportEmptyCanvas": "空のキャンバスはエクスポートできません。",
"couldNotCopyToClipboard": "クリップボードにコピーできませんでした。",
"couldNotCopyToClipboard": "",
"decryptFailed": "データを復号できませんでした。",
"uploadedSecurly": "データのアップロードはエンドツーエンド暗号化によって保護されています。Excalidrawサーバーと第三者はデータの内容を見ることができません。",
"loadSceneOverridePrompt": "外部図面を読み込むと、既存のコンテンツが置き換わります。続行しますか?",
@@ -187,8 +181,7 @@
"invalidSceneUrl": "指定された URL からシーンをインポートできませんでした。不正な形式であるか、有効な Excalidraw JSON データが含まれていません。",
"resetLibrary": "ライブラリを消去します。本当によろしいですか?",
"removeItemsFromsLibrary": "{{count}} 個のアイテムをライブラリから削除しますか?",
"invalidEncryptionKey": "暗号化キーは22文字でなければなりません。ライブコラボレーションは無効化されています。",
"browserZoom": ""
"invalidEncryptionKey": "暗号化キーは22文字でなければなりません。ライブコラボレーションは無効化されています。"
},
"errors": {
"unsupportedFileType": "サポートされていないファイル形式です。",
@@ -197,7 +190,7 @@
"svgImageInsertError": "SVGイメージを挿入できませんでした。SVGマークアップは無効に見えます。",
"invalidSVGString": "無効なSVGです。",
"cannotResolveCollabServer": "コラボレーションサーバに接続できませんでした。ページを再読み込みして、もう一度お試しください。",
"importLibraryError": "ライブラリを読み込めませんでした。"
"importLibraryError": ""
},
"toolBar": {
"selection": "選択",
@@ -238,7 +231,7 @@
"placeImage": "クリックして画像を配置するか、クリックしてドラッグしてサイズを手動で設定します",
"publishLibrary": "自分のライブラリを公開",
"bindTextToElement": "Enterを押してテキストを追加",
"deepBoxSelect": "CtrlOrCmd を押し続けることでドラッグを抑止し、深い選択を行います",
"deepBoxSelect": "CtrlOrCmd を押し続けることでドラッグを抑止し、深い選択を行",
"eraserRevert": "Alt を押し続けることで削除マークされた要素を元に戻す"
},
"canvasError": {
@@ -307,7 +300,7 @@
"view": "表示",
"zoomToFit": "すべての要素が収まるようにズーム",
"zoomToSelection": "選択要素にズーム",
"toggleElementLock": "選択したアイテムをロック/ロック解除"
"toggleElementLock": ""
},
"clearCanvasDialog": {
"title": "キャンバスを消去"
@@ -350,7 +343,7 @@
},
"noteItems": "各ライブラリ項目は、フィルタリングのために独自の名前を持つ必要があります。以下のライブラリアイテムが含まれます:",
"atleastOneLibItem": "開始するには少なくとも1つのライブラリ項目を選択してください",
"republishWarning": "注意: 選択された項目の中には、すでに公開/投稿済みと表示されているものがあります。既存のライブラリや投稿を更新する場合のみ、アイテムを再投稿してください。"
"republishWarning": ""
},
"publishSuccessDialog": {
"title": "ライブラリを送信しました",

View File

@@ -62,7 +62,7 @@
"extraBold": "Azuran aṭas",
"architect": "Amasdag",
"artist": "Anaẓur",
"cartoonist": "Amefɣul",
"cartoonist": "",
"fileTitle": "Isem n ufaylu",
"colorPicker": "Amafran n yini",
"canvasColors": "Yettwaseqdec di teɣzut n usuneɣ",
@@ -107,26 +107,20 @@
"excalidrawLib": "Tamkarḍit n Excalidraw",
"decreaseFontSize": "Senqes tiddi n tsefsit",
"increaseFontSize": "Sali tiddi n tsefsit",
"unbindText": "Serreḥ iweḍris",
"bindText": "Arez aḍris s anagbar",
"unbindText": "",
"bindText": "",
"link": {
"edit": "Ẓreg aseɣwen",
"create": "Snulfu-d aseɣwen",
"label": "Aseɣwen"
},
"elementLock": {
"lock": "Sekkeṛ",
"unlock": "Serreḥ",
"lockAll": "Sekkeṛ akk",
"unlockAll": "Serreḥ akk"
"lock": "",
"unlock": "",
"lockAll": "",
"unlockAll": ""
},
"statusPublished": "Yeffeɣ-d",
"sidebarLock": ""
},
"library": {
"noItems": "",
"hint_emptyLibrary": "",
"hint_emptyPrivateLibrary": ""
"statusPublished": ""
},
"buttons": {
"clearReset": "Ales awennez n teɣzut n usuneɣ",
@@ -187,8 +181,7 @@
"invalidSceneUrl": "Ulamek taktert n usayes seg URL i d-ittunefken. Ahat mačči d tameɣtut neɣ ur tegbir ara isefka JSON n Excalidraw.",
"resetLibrary": "Ayagi ad isfeḍ tamkarḍit-inek•m. Tetḥeqqeḍ?",
"removeItemsFromsLibrary": "Ad tekkseḍ {{count}} n uferdis (en) si temkarḍit?",
"invalidEncryptionKey": "Tasarut n uwgelhen isefk ad tesɛu 22 n yiekkilen. Amɛiwen srid yensa.",
"browserZoom": ""
"invalidEncryptionKey": "Tasarut n uwgelhen isefk ad tesɛu 22 n yiekkilen. Amɛiwen srid yensa."
},
"errors": {
"unsupportedFileType": "Anaw n ufaylu ur yettwasefrak ara.",
@@ -197,7 +190,7 @@
"svgImageInsertError": "D awezɣi tugra n tugna SVG. Acraḍ SVG yettban-d d armeɣtu.",
"invalidSVGString": "SVG armeɣtu.",
"cannotResolveCollabServer": "Ulamek tuqqna s aqeddac n umyalel. Ma ulac uɣilif ales asali n usebter sakin eɛreḍ tikkelt-nniḍen.",
"importLibraryError": "Ur d-ssalay ara tamkarḍit"
"importLibraryError": ""
},
"toolBar": {
"selection": "Tafrayt",
@@ -340,7 +333,7 @@
},
"noteGuidelines": {
"pre": "Tamkarḍit teḥwaǧ ad tettwaqbel s ufus qbel. Ma ulac uɣilif ɣer ",
"link": "iwellihen",
"link": "",
"post": " send ad tazneḍ. Tesriḍ amiḍan n GitHub akken ad tmmeslayeḍ yerna ad tgeḍ ibeddilen ma yelaq, maca mačči d ayen yettwaḥetmen."
},
"noteLicense": {
@@ -350,7 +343,7 @@
},
"noteItems": "Yal aferdis n temkarḍit isefk ad isɛu isem-is i yiman-is akken ad yili wamek ara yettusizdeg. Iferdisen-agi n temkarḍit ad ddun:",
"atleastOneLibItem": "Ma ulac uɣilif fern ma drus yiwen n uferdis n temkarḍit akken ad tebduḍ",
"republishWarning": "Tamawt: kra n yiferdisen yettwafernen ttwacerḍen ffeɣen-d/ttwaznen. Isefk ad talseḍ tuzzna n yiferdisen anagar mi ara tleqqemeḍ tamkarḍit neɣ tuzzna yellan."
"republishWarning": ""
},
"publishSuccessDialog": {
"title": "Tamkarḍit tettwazen",

View File

@@ -120,13 +120,7 @@
"lockAll": "",
"unlockAll": ""
},
"statusPublished": "",
"sidebarLock": ""
},
"library": {
"noItems": "",
"hint_emptyLibrary": "",
"hint_emptyPrivateLibrary": ""
"statusPublished": ""
},
"buttons": {
"clearReset": "",
@@ -187,8 +181,7 @@
"invalidSceneUrl": "",
"resetLibrary": "",
"removeItemsFromsLibrary": "",
"invalidEncryptionKey": "",
"browserZoom": ""
"invalidEncryptionKey": ""
},
"errors": {
"unsupportedFileType": "",

View File

@@ -65,13 +65,13 @@
"cartoonist": "만화가",
"fileTitle": "파일 이름",
"colorPicker": "색상 선택기",
"canvasColors": "캔버스에서 사용되었음",
"canvasColors": "",
"canvasBackground": "캔버스 배경",
"drawingCanvas": "캔버스 그리기",
"layers": "레이어",
"actions": "동작",
"language": "언어",
"liveCollaboration": "실시간",
"liveCollaboration": "라이브",
"duplicateSelection": "복제",
"untitled": "제목 없음",
"name": "이름",
@@ -120,13 +120,7 @@
"lockAll": "모두 잠금",
"unlockAll": "모두 잠금 해제"
},
"statusPublished": "게시됨",
"sidebarLock": ""
},
"library": {
"noItems": "",
"hint_emptyLibrary": "",
"hint_emptyPrivateLibrary": ""
"statusPublished": ""
},
"buttons": {
"clearReset": "캔버스 초기화",
@@ -187,8 +181,7 @@
"invalidSceneUrl": "제공된 URL에서 화면을 가져오는데 실패했습니다. 주소가 잘못되거나, 유효한 Excalidraw JSON 데이터를 포함하고 있지 않은 것일 수 있습니다.",
"resetLibrary": "당신의 라이브러리를 초기화 합니다. 계속하시겠습니까?",
"removeItemsFromsLibrary": "{{count}}개의 아이템을 라이브러리에서 삭제하시겠습니까?",
"invalidEncryptionKey": "암호화 키는 반드시 22글자여야 합니다. 실시간 협업이 비활성화됩니다.",
"browserZoom": ""
"invalidEncryptionKey": "암호화 키는 반드시 22글자여야 합니다. 실시간 협업이 비활성화됩니다."
},
"errors": {
"unsupportedFileType": "지원하지 않는 파일 형식 입니다.",
@@ -211,7 +204,7 @@
"text": "텍스트",
"library": "라이브러리",
"lock": "선택된 도구 유지하기",
"penMode": "핀치 줌을 하지 않도록 하고 펜을 통해서 자유롭게 그리기",
"penMode": "",
"link": "선택한 도형에 대해서 링크를 추가/업데이트",
"eraser": "지우개"
},
@@ -238,7 +231,7 @@
"placeImage": "클릭해서 이미지를 배치하거나, 클릭하고 드래그해서 사이즈를 조정하기",
"publishLibrary": "당신만의 라이브러리를 게시하기",
"bindTextToElement": "Enter 키를 눌러서 텍스트 추가하기",
"deepBoxSelect": "CtrlOrCmd 키를 눌러서 깊게 선택하고, 드래그하지 않도록 하기",
"deepBoxSelect": "",
"eraserRevert": "Alt를 눌러서 삭제하도록 지정된 요소를 되돌리기"
},
"canvasError": {
@@ -286,8 +279,8 @@
"helpDialog": {
"blog": "블로그 읽어보기",
"click": "클릭",
"deepSelect": "깊게 선택",
"deepBoxSelect": "영역을 깊게 선택하고, 드래그하지 않도록 하기",
"deepSelect": "",
"deepBoxSelect": "",
"curvedArrow": "곡선 화살표",
"curvedLine": "곡선",
"documentation": "설명서",
@@ -350,7 +343,7 @@
},
"noteItems": "각각의 라이브러리는 분류할 수 있도록 고유한 이름을 가져야 합니다. 다음의 라이브러리 항목이 포함됩니다:",
"atleastOneLibItem": "최소한 하나의 라이브러리를 선택해주세요",
"republishWarning": "참고: 선택된 항목의 일부는 이미 제출/게시되었습니다. 기존의 라이브러리나 제출물을 업데이트하는 경우에만 제출하세요."
"republishWarning": ""
},
"publishSuccessDialog": {
"title": "라이브러리 제출됨",

View File

@@ -120,13 +120,7 @@
"lockAll": "",
"unlockAll": ""
},
"statusPublished": "",
"sidebarLock": ""
},
"library": {
"noItems": "",
"hint_emptyLibrary": "",
"hint_emptyPrivateLibrary": ""
"statusPublished": ""
},
"buttons": {
"clearReset": "",
@@ -187,8 +181,7 @@
"invalidSceneUrl": "",
"resetLibrary": "",
"removeItemsFromsLibrary": "",
"invalidEncryptionKey": "",
"browserZoom": ""
"invalidEncryptionKey": ""
},
"errors": {
"unsupportedFileType": "",

View File

@@ -120,13 +120,7 @@
"lockAll": "Fiksēt visu",
"unlockAll": "Atbrīvot visu"
},
"statusPublished": "Publicēts",
"sidebarLock": "Paturēt atvērtu sānjoslu"
},
"library": {
"noItems": "Neviena vienība vēl nav pievienota...",
"hint_emptyLibrary": "Atlasiet objektu tāfelē, lai to šeit pievienotu, vai pievienojiet publisku bibliotēku zemāk.",
"hint_emptyPrivateLibrary": "Atlasiet objektu tāfelē, lai to šeit pievienotu."
"statusPublished": "Publicēts"
},
"buttons": {
"clearReset": "Atiestatīt tāfeli",
@@ -187,8 +181,7 @@
"invalidSceneUrl": "Nevarēja importēt ainu no norādītā URL. Vai nu tas ir nederīgs, vai nesatur derīgus Excalidraw JSON datus.",
"resetLibrary": "Šī funkcija iztukšos bibliotēku. Vai turpināt?",
"removeItemsFromsLibrary": "Vai izņemt {{count}} vienumu(s) no bibliotēkas?",
"invalidEncryptionKey": "Šifrēšanas atslēgai jābūt 22 simbolus garai. Tiešsaistes sadarbība ir izslēgta.",
"browserZoom": "Pārlūka pietuvināšanas līmenis nav 100%; šis var sakropļot tāfeles izskatu"
"invalidEncryptionKey": "Šifrēšanas atslēgai jābūt 22 simbolus garai. Tiešsaistes sadarbība ir izslēgta."
},
"errors": {
"unsupportedFileType": "Neatbalstīts datnes veids.",

View File

@@ -120,13 +120,7 @@
"lockAll": "सर्व कुलूपात ठेवा",
"unlockAll": "सर्व कुलूपातून बाहेर"
},
"statusPublished": "प्रकाशित करा",
"sidebarLock": "साइडबार उघडं ठेवा"
},
"library": {
"noItems": "अजून कोणतेही आइटम जोडलेले नाही...",
"hint_emptyLibrary": "पटल वर एक वस्तु निवडुन एथे जोडा, किव्हा एक संग्रह जन कोष कडुन खाली स्थापित करा.",
"hint_emptyPrivateLibrary": "ईथे जोडण्या साठी पटल वरून एक वस्तु निवडा."
"statusPublished": "प्रकाशित करा"
},
"buttons": {
"clearReset": "पटल पुसा",
@@ -187,8 +181,7 @@
"invalidSceneUrl": "दिलेल्या यू-आर-एल पासून दृश्य आणू शकलो नाही. तो एकतर बरोबार नाही आहे किंवा त्यात वैध एक्सकेलीड्रॉ जेसन डेटा नाही.",
"resetLibrary": "पटल स्वच्छ होणार, तुम्हाला खात्री आहे का?",
"removeItemsFromsLibrary": "संग्रहातून {{count}} तत्व (एक किव्हा अनेक) काढू?",
"invalidEncryptionKey": "कूटबद्धन कुंजी 22 अक्षरांची असणे आवश्यक आहे. थेट सहयोग अक्षम केले आहे.",
"browserZoom": "वेब ब्राउज़र चे ज़ूम लेवल 100% नाही आहे त्या कारणानी पटल चूक दिसू सकतो"
"invalidEncryptionKey": "कूटबद्धन कुंजी 22 अक्षरांची असणे आवश्यक आहे. थेट सहयोग अक्षम केले आहे."
},
"errors": {
"unsupportedFileType": "असमर्थित फाइल प्रकार.",

View File

@@ -120,13 +120,7 @@
"lockAll": "",
"unlockAll": ""
},
"statusPublished": "",
"sidebarLock": ""
},
"library": {
"noItems": "",
"hint_emptyLibrary": "",
"hint_emptyPrivateLibrary": ""
"statusPublished": ""
},
"buttons": {
"clearReset": "ကားချပ်ရှင်းလင်း",
@@ -187,8 +181,7 @@
"invalidSceneUrl": "",
"resetLibrary": "",
"removeItemsFromsLibrary": "",
"invalidEncryptionKey": "",
"browserZoom": ""
"invalidEncryptionKey": ""
},
"errors": {
"unsupportedFileType": "",

View File

@@ -120,13 +120,7 @@
"lockAll": "Lås alle",
"unlockAll": "Lås opp alle"
},
"statusPublished": "Publisert",
"sidebarLock": "Holde sidemenyen åpen"
},
"library": {
"noItems": "Ingen elementer lagt til ennå...",
"hint_emptyLibrary": "Velg et objekt på lerretet for å legge det til her, eller installer et bibliotek fra den offentlige samlingen under.",
"hint_emptyPrivateLibrary": "Velg et objekt på lerretet for å legge det til her."
"statusPublished": "Publisert"
},
"buttons": {
"clearReset": "Tøm lerretet og tilbakestill bakgrunnsfargen",
@@ -187,8 +181,7 @@
"invalidSceneUrl": "Kunne ikke importere scene fra den oppgitte URL-en. Den er enten ødelagt, eller inneholder ikke gyldig Excalidraw JSON-data.",
"resetLibrary": "Dette vil tømme biblioteket ditt. Er du sikker?",
"removeItemsFromsLibrary": "Slett {{count}} element(er) fra biblioteket?",
"invalidEncryptionKey": "Krypteringsnøkkel må ha 22 tegn. Live-samarbeid er deaktivert.",
"browserZoom": "Nettleserens zoomnivå er ikke satt til 100%, som kan føre til at lerretet vises feil"
"invalidEncryptionKey": "Krypteringsnøkkel må ha 22 tegn. Live-samarbeid er deaktivert."
},
"errors": {
"unsupportedFileType": "Filtypen støttes ikke.",

View File

@@ -120,13 +120,7 @@
"lockAll": "",
"unlockAll": ""
},
"statusPublished": "",
"sidebarLock": ""
},
"library": {
"noItems": "",
"hint_emptyLibrary": "",
"hint_emptyPrivateLibrary": ""
"statusPublished": ""
},
"buttons": {
"clearReset": "Canvas opnieuw instellen",
@@ -187,8 +181,7 @@
"invalidSceneUrl": "Kan scène niet importeren vanuit de opgegeven URL. Het is onjuist of bevat geen geldige Excalidraw JSON-gegevens.",
"resetLibrary": "Dit zal je bibliotheek wissen. Weet je het zeker?",
"removeItemsFromsLibrary": "Verwijder {{count}} item(s) uit bibliotheek?",
"invalidEncryptionKey": "Encryptiesleutel moet 22 tekens zijn. Live samenwerking is uitgeschakeld.",
"browserZoom": ""
"invalidEncryptionKey": "Encryptiesleutel moet 22 tekens zijn. Live samenwerking is uitgeschakeld."
},
"errors": {
"unsupportedFileType": "Niet-ondersteund bestandstype.",

View File

@@ -120,13 +120,7 @@
"lockAll": "",
"unlockAll": ""
},
"statusPublished": "",
"sidebarLock": ""
},
"library": {
"noItems": "",
"hint_emptyLibrary": "",
"hint_emptyPrivateLibrary": ""
"statusPublished": ""
},
"buttons": {
"clearReset": "Tilbakestill lerretet",
@@ -187,8 +181,7 @@
"invalidSceneUrl": "Kunne ikkje hente noko scene frå den URL-en. Ho er anten øydelagd eller inneheld ikkje gyldig Excalidraw JSON-data.",
"resetLibrary": "Dette vil fjerne alt innhald frå biblioteket. Er du sikker?",
"removeItemsFromsLibrary": "Slette {{count}} element frå biblioteket?",
"invalidEncryptionKey": "Krypteringsnøkkelen må ha 22 teikn. Sanntidssamarbeid er deaktivert.",
"browserZoom": ""
"invalidEncryptionKey": "Krypteringsnøkkelen må ha 22 teikn. Sanntidssamarbeid er deaktivert."
},
"errors": {
"unsupportedFileType": "Filtypen er ikkje støtta.",

View File

@@ -120,13 +120,7 @@
"lockAll": "Tot verrolhar",
"unlockAll": "Tot desverrolhar"
},
"statusPublished": "",
"sidebarLock": ""
},
"library": {
"noItems": "",
"hint_emptyLibrary": "",
"hint_emptyPrivateLibrary": ""
"statusPublished": ""
},
"buttons": {
"clearReset": "Reïnicializar lo canabàs",
@@ -187,8 +181,7 @@
"invalidSceneUrl": "Importacion impossibla de la scèna a partir de lURL provesida. Es siá mal formatada o siá conten pas cap de donada JSON Excalidraw valida.",
"resetLibrary": "Aquò suprimirà vòstra bibliotèca. O volètz vertadièrament?",
"removeItemsFromsLibrary": "Suprimir {{count}} element(s) de la bibliotèca?",
"invalidEncryptionKey": "La clau de chiframent deu conténer 22 caractèrs. La collaboracion en dirèct es desactivada.",
"browserZoom": ""
"invalidEncryptionKey": "La clau de chiframent deu conténer 22 caractèrs. La collaboracion en dirèct es desactivada."
},
"errors": {
"unsupportedFileType": "Tipe de fichièr pas pres en carga.",

View File

@@ -120,13 +120,7 @@
"lockAll": "",
"unlockAll": ""
},
"statusPublished": "",
"sidebarLock": ""
},
"library": {
"noItems": "",
"hint_emptyLibrary": "",
"hint_emptyPrivateLibrary": ""
"statusPublished": ""
},
"buttons": {
"clearReset": "ਕੈਨਵਸ ਰੀਸੈੱਟ ਕਰੋ",
@@ -187,8 +181,7 @@
"invalidSceneUrl": "ਦਿੱਤੀ ਗਈ URL 'ਚੋਂ ਦ੍ਰਿਸ਼ ਨੂੰ ਆਯਾਤ ਨਹੀਂ ਕਰ ਸਕੇ। ਇਹ ਜਾਂ ਤਾਂ ਖਰਾਬ ਹੈ, ਜਾਂ ਇਸ ਵਿੱਚ ਜਾਇਜ਼ Excalidraw JSON ਡਾਟਾ ਸ਼ਾਮਲ ਨਹੀਂ ਹੈ।",
"resetLibrary": "ਇਹ ਤੁਹਾਡੀ ਲਾਇਬ੍ਰੇਰੀ ਨੂੰ ਸਾਫ ਕਰ ਦੇਵੇਗਾ। ਕੀ ਤੁਸੀਂ ਪੱਕਾ ਇੰਝ ਕਰਨਾ ਚਾਹੁੰਦੇ ਹੋ?",
"removeItemsFromsLibrary": "ਲਾਇਬ੍ਰੇਰੀ ਵਿੱਚੋਂ {{count}} ਚੀਜ਼(-ਜ਼ਾਂ) ਮਿਟਾਉਣੀਆਂ ਹਨ?",
"invalidEncryptionKey": "",
"browserZoom": ""
"invalidEncryptionKey": ""
},
"errors": {
"unsupportedFileType": "",

View File

@@ -1,26 +1,25 @@
{
"ar-SA": 91,
"ar-SA": 92,
"bg-BG": 58,
"bn-BD": 0,
"ca-ES": 97,
"cs-CZ": 24,
"ca-ES": 95,
"cs-CZ": 23,
"da-DK": 34,
"de-DE": 100,
"el-GR": 82,
"de-DE": 99,
"el-GR": 83,
"en": 100,
"es-ES": 99,
"eu-ES": 98,
"fa-IR": 98,
"eu-ES": 97,
"fa-IR": 60,
"fi-FI": 98,
"fr-FR": 100,
"gl-ES": 45,
"he-IL": 94,
"hi-IN": 62,
"hu-HU": 94,
"id-ID": 100,
"it-IT": 100,
"fr-FR": 99,
"he-IL": 95,
"hi-IN": 61,
"hu-HU": 96,
"id-ID": 99,
"it-IT": 99,
"ja-JP": 98,
"kab-KAB": 95,
"kab-KAB": 93,
"kk-KZ": 22,
"ko-KR": 98,
"lt-LT": 22,
@@ -28,24 +27,23 @@
"mr-IN": 100,
"my-MM": 44,
"nb-NO": 100,
"nl-NL": 86,
"nn-NO": 95,
"oc-FR": 98,
"pa-IN": 87,
"pl-PL": 88,
"pt-BR": 95,
"nl-NL": 87,
"nn-NO": 96,
"oc-FR": 99,
"pa-IN": 89,
"pl-PL": 89,
"pt-BR": 96,
"pt-PT": 80,
"ro-RO": 100,
"ru-RU": 100,
"si-LK": 8,
"sk-SK": 100,
"si-LK": 9,
"sk-SK": 99,
"sl-SI": 100,
"sv-SE": 100,
"ta-IN": 98,
"tr-TR": 99,
"uk-UA": 99,
"vi-VN": 13,
"zh-CN": 100,
"tr-TR": 100,
"uk-UA": 90,
"zh-CN": 99,
"zh-HK": 27,
"zh-TW": 100
}

View File

@@ -120,13 +120,7 @@
"lockAll": "",
"unlockAll": ""
},
"statusPublished": "",
"sidebarLock": ""
},
"library": {
"noItems": "",
"hint_emptyLibrary": "",
"hint_emptyPrivateLibrary": ""
"statusPublished": ""
},
"buttons": {
"clearReset": "Wyczyść dokument i zresetuj kolor dokumentu",
@@ -187,8 +181,7 @@
"invalidSceneUrl": "Nie udało się zaimportować sceny z podanego adresu URL. Jest ona wadliwa lub nie zawiera poprawnych danych Excalidraw w formacie JSON.",
"resetLibrary": "To wyczyści twoją bibliotekę. Jesteś pewien?",
"removeItemsFromsLibrary": "Usunąć {{count}} element(ów) z biblioteki?",
"invalidEncryptionKey": "Klucz szyfrowania musi składać się z 22 znaków. Współpraca na żywo jest wyłączona.",
"browserZoom": ""
"invalidEncryptionKey": "Klucz szyfrowania musi składać się z 22 znaków. Współpraca na żywo jest wyłączona."
},
"errors": {
"unsupportedFileType": "Nieobsługiwany typ pliku.",

View File

@@ -120,13 +120,7 @@
"lockAll": "",
"unlockAll": ""
},
"statusPublished": "",
"sidebarLock": ""
},
"library": {
"noItems": "",
"hint_emptyLibrary": "",
"hint_emptyPrivateLibrary": ""
"statusPublished": ""
},
"buttons": {
"clearReset": "Limpar o canvas e redefinir a cor de fundo",
@@ -187,8 +181,7 @@
"invalidSceneUrl": "Não foi possível importar a cena da URL fornecida. Ela está incompleta ou não contém dados JSON válidos do Excalidraw.",
"resetLibrary": "Isto limpará a sua biblioteca. Você tem certeza?",
"removeItemsFromsLibrary": "Excluir {{count}} item(ns) da biblioteca?",
"invalidEncryptionKey": "A chave de encriptação deve ter 22 caracteres. A colaboração ao vivo está desabilitada.",
"browserZoom": ""
"invalidEncryptionKey": "A chave de encriptação deve ter 22 caracteres. A colaboração ao vivo está desabilitada."
},
"errors": {
"unsupportedFileType": "Tipo de arquivo não suportado.",

View File

@@ -120,13 +120,7 @@
"lockAll": "",
"unlockAll": ""
},
"statusPublished": "",
"sidebarLock": ""
},
"library": {
"noItems": "",
"hint_emptyLibrary": "",
"hint_emptyPrivateLibrary": ""
"statusPublished": ""
},
"buttons": {
"clearReset": "Limpar a área de desenho e redefinir a cor de fundo",
@@ -187,8 +181,7 @@
"invalidSceneUrl": "Não foi possível importar a cena a partir do URL fornecido. Ou está mal formado ou não contém dados JSON do Excalidraw válidos.",
"resetLibrary": "Isto irá limpar a sua biblioteca. Tem a certeza?",
"removeItemsFromsLibrary": "",
"invalidEncryptionKey": "Chave de encriptação deve ter 22 caracteres. A colaboração ao vivo está desativada.",
"browserZoom": ""
"invalidEncryptionKey": "Chave de encriptação deve ter 22 caracteres. A colaboração ao vivo está desativada."
},
"errors": {
"unsupportedFileType": "Tipo de ficheiro não suportado.",
@@ -317,12 +310,12 @@
"itemName": "",
"authorName": "",
"githubUsername": "",
"twitterUsername": "Nome de utilizador no Twitter",
"libraryName": "Nome da biblioteca",
"libraryDesc": "Descrição da biblioteca",
"twitterUsername": "",
"libraryName": "",
"libraryDesc": "",
"website": "",
"placeholder": {
"authorName": "Introduza o seu nome ou nome de utilizador",
"authorName": "",
"libraryName": "",
"libraryDesc": "",
"githubHandle": "",
@@ -340,7 +333,7 @@
},
"noteGuidelines": {
"pre": "",
"link": "orientações",
"link": "",
"post": ""
},
"noteLicense": {

View File

@@ -120,13 +120,7 @@
"lockAll": "Blocare toate",
"unlockAll": "Deblocare toate"
},
"statusPublished": "Publicat",
"sidebarLock": "Păstrează deschisă bara laterală"
},
"library": {
"noItems": "Niciun element adăugat încă...",
"hint_emptyLibrary": "Selectează un element de pe pânză pentru a-l adăuga aici sau instalează o bibliotecă din depozitul public, de mai jos.",
"hint_emptyPrivateLibrary": "Selectează un element de pe pânză pentru a-l adăuga aici."
"statusPublished": "Publicat"
},
"buttons": {
"clearReset": "Resetare pânză",
@@ -187,8 +181,7 @@
"invalidSceneUrl": "Scena nu a putut fi importată din URL-ul furnizat. Este fie incorect formată, fie nu conține date JSON Excalidraw valide.",
"resetLibrary": "Această opțiune va elimina conținutul din bibliotecă. Confirmi?",
"removeItemsFromsLibrary": "Ștergi {{count}} element(e) din bibliotecă?",
"invalidEncryptionKey": "Cheia de criptare trebuie să aibă 22 de caractere. Colaborarea în direct este dezactivată.",
"browserZoom": "Nivelul de transfocare al navigatorului tău nu este setat la 100% ceea ce poate face ca panoul să fie afișat incorect"
"invalidEncryptionKey": "Cheia de criptare trebuie să aibă 22 de caractere. Colaborarea în direct este dezactivată."
},
"errors": {
"unsupportedFileType": "Tip de fișier neacceptat.",

View File

@@ -120,13 +120,7 @@
"lockAll": "Заблокировать все",
"unlockAll": "Разблокировать все"
},
"statusPublished": "Опубликовано",
"sidebarLock": "Держать боковую панель открытой"
},
"library": {
"noItems": "Пока ничего не добавлено...",
"hint_emptyLibrary": "Выберите объект на холсте, чтобы добавить его сюда, или установите библиотеку из публичного репозитория ниже.",
"hint_emptyPrivateLibrary": "Выберите объект на холсте, чтобы добавить его сюда."
"statusPublished": "Опубликовано"
},
"buttons": {
"clearReset": "Очистить холст и сбросить цвет фона",
@@ -187,8 +181,7 @@
"invalidSceneUrl": "Невозможно импортировать сцену с предоставленного URL. Неверный формат, или не содержит верных Excalidraw JSON данных.",
"resetLibrary": "Это очистит вашу библиотеку. Вы уверены?",
"removeItemsFromsLibrary": "Удалить {{count}} объект(ов) из библиотеки?",
"invalidEncryptionKey": "Ключ шифрования должен состоять из 22 символов. Одновременное редактирование отключено.",
"browserZoom": "Масштаб вашего браузера не установлен на 100%, из-за этого доска может отображаться неправильно"
"invalidEncryptionKey": "Ключ шифрования должен состоять из 22 символов. Одновременное редактирование отключено."
},
"errors": {
"unsupportedFileType": "Неподдерживаемый тип файла.",

View File

@@ -120,13 +120,7 @@
"lockAll": "",
"unlockAll": ""
},
"statusPublished": "",
"sidebarLock": ""
},
"library": {
"noItems": "",
"hint_emptyLibrary": "",
"hint_emptyPrivateLibrary": ""
"statusPublished": ""
},
"buttons": {
"clearReset": "",
@@ -187,8 +181,7 @@
"invalidSceneUrl": "",
"resetLibrary": "",
"removeItemsFromsLibrary": "",
"invalidEncryptionKey": "",
"browserZoom": ""
"invalidEncryptionKey": ""
},
"errors": {
"unsupportedFileType": "",

View File

@@ -120,13 +120,7 @@
"lockAll": "Zamknúť všetko",
"unlockAll": "Odomknúť všetko"
},
"statusPublished": "Zverejnené",
"sidebarLock": "Nechať bočný panel otvorený"
},
"library": {
"noItems": "Zatiaľ neboli pridané žiadne položky...",
"hint_emptyLibrary": "Vyberte položku z plátna pre jej pridanie do knižnice alebo použite knižnicu z verejného zoznamu knižníc nižšie.",
"hint_emptyPrivateLibrary": "Vyberte položku z plátna pre jej pridanie do knižnice."
"statusPublished": ""
},
"buttons": {
"clearReset": "Obnoviť plátno",
@@ -187,8 +181,7 @@
"invalidSceneUrl": "Nepodarilo sa načítať scénu z poskytnutej URL. Je nevalidná alebo neobsahuje žiadne validné Excalidraw JSON dáta.",
"resetLibrary": "Týmto vyprázdnite vašu knižnicu. Ste si istý?",
"removeItemsFromsLibrary": "Odstrániť {{count}} položiek z knižnice?",
"invalidEncryptionKey": "Šifrovací kľúč musí mať 22 znakov. Živá spolupráca je vypnutá.",
"browserZoom": "Priblíženie vášho prehliadača nie je nastavené na 100%, čo môže spôsobiť nesprávne zobrazenie plátna"
"invalidEncryptionKey": "Šifrovací kľúč musí mať 22 znakov. Živá spolupráca je vypnutá."
},
"errors": {
"unsupportedFileType": "Nepodporovaný typ súboru.",
@@ -350,7 +343,7 @@
},
"noteItems": "Každá položka v knižnici musí mať svoje vlastné meno, aby sa dala vyhľadať. Súčasťou knižnice budú nasledujúce položky:",
"atleastOneLibItem": "Začnite prosím zvolením aspoň jednej položky z knižnice",
"republishWarning": "Poznámka: Niektoré z vybraných položiek sú už označené ako zverejnené. Ich znovu uverejnenie by ste mali vykovať iba vtedy ak aktualizujete už existujúcu knižnicu alebo požiadavku na uverejnenie."
"republishWarning": ""
},
"publishSuccessDialog": {
"title": "Knižnica uverejnená",

View File

@@ -120,13 +120,7 @@
"lockAll": "Zakleni vse",
"unlockAll": "Odkleni vse"
},
"statusPublished": "Objavljeno",
"sidebarLock": "Obdrži stransko vrstico odprto"
},
"library": {
"noItems": "Dodan še ni noben element...",
"hint_emptyLibrary": "Izberite element na platnu, da ga dodate sem, ali namestite knjižnico iz javnega skladišča spodaj.",
"hint_emptyPrivateLibrary": "Izberite element na platnu, da ga dodate sem."
"statusPublished": "Objavljeno"
},
"buttons": {
"clearReset": "Ponastavi platno",
@@ -187,8 +181,7 @@
"invalidSceneUrl": "S priloženega URL-ja ni bilo mogoče uvoziti scene. Je napačno oblikovana ali pa ne vsebuje veljavnih podatkov Excalidraw JSON.",
"resetLibrary": "To bo počistilo vašo knjižnico. Ali ste prepričani?",
"removeItemsFromsLibrary": "Izbriši elemente ({{count}}) iz knjižnice?",
"invalidEncryptionKey": "Ključ za šifriranje mora vsebovati 22 znakov. Sodelovanje v živo je onemogočeno.",
"browserZoom": "Stopnja povečave vašega brskalnika ni nastavljena na 100 %, kar lahko povzroči nepravilen prikaz table"
"invalidEncryptionKey": "Ključ za šifriranje mora vsebovati 22 znakov. Sodelovanje v živo je onemogočeno."
},
"errors": {
"unsupportedFileType": "Nepodprt tip datoteke.",

View File

@@ -120,13 +120,7 @@
"lockAll": "Lås alla",
"unlockAll": "Lås upp alla"
},
"statusPublished": "Publicerad",
"sidebarLock": "Håll sidofältet öppet"
},
"library": {
"noItems": "Inga objekt tillagda ännu...",
"hint_emptyLibrary": "Välj ett objekt på canvasen för att lägga till det här, eller installera ett bibliotek från det publika arkivet, nedan.",
"hint_emptyPrivateLibrary": "Välj ett objekt på canvasen för att lägga till det här."
"statusPublished": "Publicerad"
},
"buttons": {
"clearReset": "Återställ canvasen",
@@ -187,8 +181,7 @@
"invalidSceneUrl": "Det gick inte att importera skiss från den angivna webbadressen. Antingen har den fel format, eller så innehåller den ingen giltig Excalidraw JSON data.",
"resetLibrary": "Detta kommer att rensa ditt bibliotek. Är du säker?",
"removeItemsFromsLibrary": "Ta bort {{count}} objekt från biblioteket?",
"invalidEncryptionKey": "Krypteringsnyckeln måste vara 22 tecken. Livesamarbetet är inaktiverat.",
"browserZoom": "Din webbläsares zoomnivå är inte satt till 100% vilket kan orsaka att tavlan visas felaktigt"
"invalidEncryptionKey": "Krypteringsnyckeln måste vara 22 tecken. Livesamarbetet är inaktiverat."
},
"errors": {
"unsupportedFileType": "Filtypen stöds inte.",

View File

@@ -120,13 +120,7 @@
"lockAll": "எல்லாம் பூட்டு",
"unlockAll": "எல்லாம் பூட்டவிழ்"
},
"statusPublished": "வெளியிடப்பட்டது",
"sidebarLock": "பக்கப்பட்டையைத் திறந்தே வை"
},
"library": {
"noItems": "இதுவரை உருப்படிகள் சேரக்கப்படவில்லை...",
"hint_emptyLibrary": "கித்தானிலுள்ள உருப்படியை இங்குச் சேர்க்க தேர்ந்தெடு, அல்லது கீழுள்ள பொது களஞ்சியத்திலிருந்து நூலகத்தை நிறுவு.",
"hint_emptyPrivateLibrary": "கித்தானிலுள்ள உருப்படியை இங்குச் சேர்க்க தேர்ந்தெடு."
"statusPublished": ""
},
"buttons": {
"clearReset": "கித்தானை அகரமாக்கு",
@@ -187,8 +181,7 @@
"invalidSceneUrl": "வழங்கப்பட்ட உரலியிலிருந்து காட்சியை இறக்கவியலா. இது தவறான வடிவத்தில் உள்ளது, அ செல்லத்தக்க எக்ஸ்கேலிட்ரா JSON தரவைக் கொண்டில்லை.",
"resetLibrary": "இது உங்கள் நுலகத்தைத் துடைக்கும். நீங்கள் உறுதியா?",
"removeItemsFromsLibrary": "{{count}} உருப்படி(கள்)-ஐ உம் நூலகத்திலிருந்து அழிக்கவா?",
"invalidEncryptionKey": "மறையாக்க விசை 22 வரியுருக்கள் கொண்டிருக்கவேண்டும். நேரடி கூட்டுப்பணி முடக்கப்பட்டது.",
"browserZoom": ""
"invalidEncryptionKey": "மறையாக்க விசை 22 வரியுருக்கள் கொண்டிருக்கவேண்டும். நேரடி கூட்டுப்பணி முடக்கப்பட்டது."
},
"errors": {
"unsupportedFileType": "ஆதரிக்கப்படா கோப்பு வகை.",

View File

@@ -89,8 +89,8 @@
"align": "Hizala",
"alignTop": "Yukarı hizala",
"alignBottom": "Aşağı hizala",
"alignLeft": "Sola hizala",
"alignRight": "Sağa hizala",
"alignLeft": "Sola yasla",
"alignRight": "Sağa yasla",
"centerVertically": "Dikeyde ortala",
"centerHorizontally": "Yatayda ortala",
"distributeHorizontally": "Yatay dağıt",
@@ -120,13 +120,7 @@
"lockAll": "Hepsini kilitle",
"unlockAll": "Hepsinin kilidini kaldır"
},
"statusPublished": "Yayınlandı",
"sidebarLock": ""
},
"library": {
"noItems": "Öğe eklenmedi...",
"hint_emptyLibrary": "",
"hint_emptyPrivateLibrary": "Tuvalden bir eleman seçerek sayfaya ekleyin."
"statusPublished": "Yayınlandı"
},
"buttons": {
"clearReset": "Tuvali sıfırla",
@@ -187,8 +181,7 @@
"invalidSceneUrl": "Verilen bağlantıdan ç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?",
"removeItemsFromsLibrary": "{{count}} öğe(ler) kitaplıktan kaldırılsın mı?",
"invalidEncryptionKey": "Şifreleme anahtarı 22 karakter olmalı. Canlı işbirliği devre dışı bırakıldı.",
"browserZoom": "Tarayıcınızın yaklaştırma seviyesi %100 değil. Bu durum, tablonun yanlış görünmesine sebep olabilir"
"invalidEncryptionKey": "Şifreleme anahtarı 22 karakter olmalı. Canlı işbirliği devre dışı bırakıldı."
},
"errors": {
"unsupportedFileType": "Desteklenmeyen dosya türü.",

View File

@@ -115,18 +115,12 @@
"label": "Посилання"
},
"elementLock": {
"lock": "Блокувати",
"unlock": "Розблокувати",
"lockAll": "Заблокувати все",
"unlockAll": "Розблокувати все"
"lock": "",
"unlock": "",
"lockAll": "",
"unlockAll": ""
},
"statusPublished": "Опубліковано",
"sidebarLock": "Не закривати бокове меню"
},
"library": {
"noItems": "Тут поки пусто...",
"hint_emptyLibrary": "Виберіть об'єкт на полотні, щоб додати його сюди або встановіть бібліотеку з публічного репозиторію, що нижче.",
"hint_emptyPrivateLibrary": "Виберіть елемент на полотні, щоб додати його сюди."
"statusPublished": ""
},
"buttons": {
"clearReset": "Очистити полотно",
@@ -182,13 +176,12 @@
"errorAddingToLibrary": "Не вдалося додати елемент до бібліотеки",
"errorRemovingFromLibrary": "Не вдалося видалити елемент з бібліотеки",
"confirmAddLibrary": "Це призведе до додавання {{numShapes}} фігур до вашої бібліотеки. Ви впевнені?",
"imageDoesNotContainScene": "Виглядає ніби зображення не містить корисної інформації. Ви увімкнули вбудовування сцени при експорті?",
"imageDoesNotContainScene": "",
"cannotRestoreFromImage": "Сцена не може бути відновлена з цього файлу зображення",
"invalidSceneUrl": "Не вдалося імпортувати сцену з наданого URL. Він або недоформований, або не містить дійсних даних Excalidraw JSON.",
"resetLibrary": "Це призведе до очищення бібліотеки. Ви впевнені?",
"removeItemsFromsLibrary": "Видалити {{count}} елемент(ів) з бібліотеки?",
"invalidEncryptionKey": "Ключ шифрування повинен бути довжиною до 22 символів. Спільну роботу вимкнено.",
"browserZoom": ""
"removeItemsFromsLibrary": "",
"invalidEncryptionKey": ""
},
"errors": {
"unsupportedFileType": "Непідтримуваний тип файлу.",
@@ -196,8 +189,8 @@
"fileTooBig": "Занадто великий розмір файлу, максимальний розмір файлу {{maxSize}}.",
"svgImageInsertError": "Не вдалося вставити SVG-зображення. Помилка розмітки SVG.",
"invalidSVGString": "Недійсний SVG.",
"cannotResolveCollabServer": "Не вдалося приєднатися до сервера. Перезавантажте сторінку та повторіть спробу.",
"importLibraryError": "Не вдалося завантажити бібліотеку"
"cannotResolveCollabServer": "",
"importLibraryError": ""
},
"toolBar": {
"selection": "Виділення",
@@ -211,7 +204,7 @@
"text": "Текст",
"library": "Бібліотека",
"lock": "Залишити обраний інструмент після креслення",
"penMode": "Вимкнути масштабування через мультитач та малювати тільки з допомогою пера",
"penMode": "",
"link": "Додати/Оновити посилання для вибраної форми",
"eraser": "Очищувач"
},
@@ -226,20 +219,20 @@
"freeDraw": "Натисніть і потягніть, відпустіть коли завершите",
"text": "Порада: можна також додати текст, двічі клацнувши по будь-якому місці інструментом вибору",
"text_selected": "Подвійний клік або натисніть клавішу ENTER, щоб редагувати текст",
"text_editing": "Натисніть клавішу Escape або Ctrl/Cmd+ENTER, щоб завершити редагування",
"text_editing": "",
"linearElementMulti": "Натисніть на останню точку, клацніть Esc або Enter щоб завершити",
"lockAngle": "Ви можете обмежити кут, утримуюючи SHIFT",
"resize": "Ви можете зберегти пропорції, утримуючи SHIFT під час зміни розміру,\nутримуйте ALT для змінення розміру від центру",
"resizeImage": "Ви можете змінювати розміри утримуючи клавішу SHIFT, втримуйте клавішу ALT щоб змінювати розмір відносно центру",
"resizeImage": "",
"rotate": "Ви можете обмежити кути, утримуючи SHIFT під час обертання",
"lineEditor_info": "Двічі клацніть або натисніть Enter щоб редагувати точки",
"lineEditor_pointSelected": "Натисніть Delete для видалення точку (точок), або Ctrl/Cmd+D для дублювання, перетаскування працює як звично",
"lineEditor_nothingSelected": "Виберіть точку для редагування (втримуйте клавішу SHIFT для вибору кількох точок), або клавішу Alt для додавання нових точок",
"placeImage": "Клацніть, щоб розмістити зображення, або натисніть та потягніть щоб змінити його розмір",
"lineEditor_pointSelected": "",
"lineEditor_nothingSelected": "",
"placeImage": "",
"publishLibrary": "Опублікувати свою власну бібліотеку",
"bindTextToElement": "Натисніть Enter, щоб додати текст",
"deepBoxSelect": "Втримуйте Ctrl/Cmd для глибокого виділення та щоб попередити перетягування",
"eraserRevert": "Втримуйте клавішу Alt, щоб повернути елементи позначені для видалення"
"deepBoxSelect": "",
"eraserRevert": ""
},
"canvasError": {
"cannotShowPreview": "Не вдається показати попередній перегляд",
@@ -279,22 +272,22 @@
"link_title": "Доступ за посиланням",
"link_details": "Експортувати як посилання тільки для читання.",
"link_button": "Експортувати у посилання",
"excalidrawplus_description": "Збережіть сцену у вашому обліковому записі Excalidraw+.",
"excalidrawplus_description": "",
"excalidrawplus_button": "Експортувати",
"excalidrawplus_exportError": "Не вдалося експортувати у Excalidraw+..."
"excalidrawplus_exportError": ""
},
"helpDialog": {
"blog": "Наш блог",
"click": "натиснути",
"deepSelect": "Глибокий вибір",
"deepBoxSelect": "Глибоке виділення в межах рамки, та обмеження перетягування",
"deepBoxSelect": "",
"curvedArrow": "Крива стрілка",
"curvedLine": "Крива лінія",
"documentation": "Документація",
"doubleClick": "подвійний клік",
"drag": "перетягнути",
"editor": "Редактор",
"editSelectedShape": "Змінити вибрану фігуру (текст/стрілку/рядок)",
"editSelectedShape": "",
"github": "Знайшли помилку? Повідомте",
"howto": "Дотримуйтесь наших інструкцій",
"or": "або",
@@ -307,14 +300,14 @@
"view": "Вигляд",
"zoomToFit": "Збільшити щоб умістити всі елементи",
"zoomToSelection": "Наблизити вибране",
"toggleElementLock": "Заблокувати/розблокувати вибране"
"toggleElementLock": ""
},
"clearCanvasDialog": {
"title": "Очистити полотно"
},
"publishDialog": {
"title": "Опублікувати бібліотеку",
"itemName": "Назва елементу",
"itemName": "",
"authorName": "Ім'я автора",
"githubUsername": "Ім'я користувача Github",
"twitterUsername": "Ім'я користувача Твитер",
@@ -325,32 +318,32 @@
"authorName": "Ваше ім'я або ім'я користувача",
"libraryName": "Назва вашої бібліотеки",
"libraryDesc": "Опис вашої бібліотеки, щоб допомогти людям зрозуміти її використання",
"githubHandle": "Ім'я користувача в GitHub (необов'язково), щоб ви могли редагувати бібліотеку під час перевірки",
"twitterHandle": "Ім'я користувача у Twitter (необов'язково), щоб ми могли згадати вас під час промоції у Twitter",
"website": "Посилання на ваш особистий сайт або інший сайт (опціонально)"
"githubHandle": "",
"twitterHandle": "",
"website": ""
},
"errors": {
"required": "Обов’язково",
"required": "",
"website": "Введіть дійсну URL-адресу"
},
"noteDescription": {
"pre": "Подати бібліотеку, щоб вона була включена до ",
"link": "публічного репозиторія бібліотек",
"post": "для інших людей, для використання у їхніх полотнах."
"post": ""
},
"noteGuidelines": {
"pre": "Спочатку бібліотека повинна бути підтверджена. Будь ласка, прочитайте ",
"pre": "",
"link": "настанови",
"post": " перед відправкою. Вам знадобиться обліковий запис на GitHub, щоб колаборувати та вносити зміни, але це не обов'язково."
"post": ""
},
"noteLicense": {
"pre": "Публікуючи, ви погоджуєтеся, що бібліотека буде опублікована під ",
"pre": "",
"link": "Ліцензія MIT, ",
"post": ", простими словами, це означає що нею зможе користуватися будь-хто без обмежень."
"post": ""
},
"noteItems": "Кожен об'єкт в бібліотеці повинен мати назву, це потрібно для пошуку та фільтрування. Наступні об'єкти бібліотеки будуть включені:",
"atleastOneLibItem": "Будь ласка, виберіть принаймні один елемент бібліотеки, щоб почати",
"republishWarning": "Зауважте, деякі з вибраних елементів позначені як вже опубліковані/надіслані. Ви повинні повторно надсилати елементи тільки при оновленні вже опублікованої бібліотеки чи при публікації бібліотеки."
"noteItems": "",
"atleastOneLibItem": "",
"republishWarning": ""
},
"publishSuccessDialog": {
"title": "Бібліотека відправлена",

View File

@@ -1,440 +0,0 @@
{
"labels": {
"paste": "Dán",
"pasteCharts": "",
"selectAll": "Chọn tất cả",
"multiSelect": "",
"moveCanvas": "",
"cut": "Cắt",
"copy": "Sao chép",
"copyAsPng": "Sao chép vào bộ nhớ tạm dưới dạng PNG",
"copyAsSvg": "Sao chép vào bộ nhớ tạm dưới dạng SVG",
"copyText": "Sao chép vào bộ nhớ tạm dưới dạng chữ",
"bringForward": "Đưa ra trước",
"sendToBack": "Hạ xuống dưới",
"bringToFront": "Đưa ra đầu tiên",
"sendBackward": "Hạ xuống cuối",
"delete": "Xóa",
"copyStyles": "Sao chép định dạng",
"pasteStyles": "Dán định dạng",
"stroke": "Nét",
"background": "Nền",
"fill": "Fill",
"strokeWidth": "Độ dày nét",
"strokeStyle": "Kiểu nét",
"strokeStyle_solid": "Khối",
"strokeStyle_dashed": "Gạch ngang",
"strokeStyle_dotted": "Nhiều chấm",
"sloppiness": "Hoa văn nét",
"opacity": "Độ trong suốt",
"textAlign": "Căn chỉnh văn bản",
"edges": "Cạnh",
"sharp": "Nhọn",
"round": "Tròn",
"arrowheads": "Đầu mũi tên",
"arrowhead_none": "Không",
"arrowhead_arrow": "Mũi tên",
"arrowhead_bar": "Thanh",
"arrowhead_dot": "Chấm",
"arrowhead_triangle": "Tam giác",
"fontSize": "Cỡ chữ",
"fontFamily": "Phông chữ",
"onlySelected": "Chỉ lựa chọn",
"withBackground": "Nền",
"exportEmbedScene": "",
"exportEmbedScene_details": "",
"addWatermark": "",
"handDrawn": "",
"normal": "Bình thường",
"code": "Mã",
"small": "Nhỏ",
"medium": "Vừa",
"large": "Lớn",
"veryLarge": "",
"solid": "",
"hachure": "",
"crossHatch": "",
"thin": "",
"bold": "",
"left": "",
"center": "",
"right": "",
"extraBold": "",
"architect": "",
"artist": "",
"cartoonist": "",
"fileTitle": "",
"colorPicker": "",
"canvasColors": "",
"canvasBackground": "",
"drawingCanvas": "",
"layers": "",
"actions": "",
"language": "",
"liveCollaboration": "",
"duplicateSelection": "",
"untitled": "",
"name": "",
"yourName": "",
"madeWithExcalidraw": "Làm với Excalidraw",
"group": "",
"ungroup": "",
"collaborators": "",
"showGrid": "",
"addToLibrary": "",
"removeFromLibrary": "",
"libraryLoadingMessage": "",
"libraries": "",
"loadingScene": "",
"align": "",
"alignTop": "",
"alignBottom": "",
"alignLeft": "",
"alignRight": "",
"centerVertically": "",
"centerHorizontally": "",
"distributeHorizontally": "Phân bố theo chiều ngang",
"distributeVertically": "Phân bố theo chiều dọc",
"flipHorizontal": "Lật ngang",
"flipVertical": "Lật dọc",
"viewMode": "Chế độ xem",
"toggleExportColorScheme": "",
"share": "Chia sẻ",
"showStroke": "Hiển thị chọn màu",
"showBackground": "Hiện thị chọn màu nền",
"toggleTheme": "",
"personalLib": "",
"excalidrawLib": "",
"decreaseFontSize": "",
"increaseFontSize": "",
"unbindText": "",
"bindText": "",
"link": {
"edit": "",
"create": "",
"label": ""
},
"elementLock": {
"lock": "",
"unlock": "",
"lockAll": "",
"unlockAll": ""
},
"statusPublished": "",
"sidebarLock": ""
},
"library": {
"noItems": "",
"hint_emptyLibrary": "",
"hint_emptyPrivateLibrary": ""
},
"buttons": {
"clearReset": "",
"exportJSON": "",
"exportImage": "",
"export": "",
"exportToPng": "",
"exportToSvg": "",
"copyToClipboard": "",
"copyPngToClipboard": "",
"scale": "",
"save": "",
"saveAs": "",
"load": "",
"getShareableLink": "",
"close": "",
"selectLanguage": "",
"scrollBackToContent": "",
"zoomIn": "",
"zoomOut": "",
"resetZoom": "",
"menu": "",
"done": "",
"edit": "",
"undo": "",
"redo": "",
"resetLibrary": "",
"createNewRoom": "",
"fullScreen": "",
"darkMode": "",
"lightMode": "",
"zenMode": "",
"exitZenMode": "",
"cancel": "",
"clear": "",
"remove": "",
"publishLibrary": "",
"submit": "",
"confirm": ""
},
"alerts": {
"clearReset": "",
"couldNotCreateShareableLink": "",
"couldNotCreateShareableLinkTooBig": "",
"couldNotLoadInvalidFile": "",
"importBackendFailed": "",
"cannotExportEmptyCanvas": "",
"couldNotCopyToClipboard": "",
"decryptFailed": "",
"uploadedSecurly": "",
"loadSceneOverridePrompt": "",
"collabStopOverridePrompt": "",
"errorAddingToLibrary": "",
"errorRemovingFromLibrary": "",
"confirmAddLibrary": "",
"imageDoesNotContainScene": "",
"cannotRestoreFromImage": "",
"invalidSceneUrl": "",
"resetLibrary": "",
"removeItemsFromsLibrary": "",
"invalidEncryptionKey": "",
"browserZoom": ""
},
"errors": {
"unsupportedFileType": "",
"imageInsertError": "",
"fileTooBig": "",
"svgImageInsertError": "",
"invalidSVGString": "",
"cannotResolveCollabServer": "",
"importLibraryError": ""
},
"toolBar": {
"selection": "",
"image": "",
"rectangle": "",
"diamond": "",
"ellipse": "",
"arrow": "",
"line": "",
"freedraw": "",
"text": "",
"library": "",
"lock": "",
"penMode": "",
"link": "",
"eraser": ""
},
"headings": {
"canvasActions": "",
"selectedShapeActions": "",
"shapes": ""
},
"hints": {
"canvasPanning": "",
"linearElement": "",
"freeDraw": "",
"text": "",
"text_selected": "",
"text_editing": "",
"linearElementMulti": "",
"lockAngle": "",
"resize": "",
"resizeImage": "",
"rotate": "",
"lineEditor_info": "",
"lineEditor_pointSelected": "",
"lineEditor_nothingSelected": "",
"placeImage": "",
"publishLibrary": "",
"bindTextToElement": "",
"deepBoxSelect": "",
"eraserRevert": ""
},
"canvasError": {
"cannotShowPreview": "",
"canvasTooBig": "",
"canvasTooBigTip": ""
},
"errorSplash": {
"headingMain_pre": "",
"headingMain_button": "",
"clearCanvasMessage": "",
"clearCanvasMessage_button": "",
"clearCanvasCaveat": "",
"trackedToSentry_pre": "",
"trackedToSentry_post": "",
"openIssueMessage_pre": "",
"openIssueMessage_button": "",
"openIssueMessage_post": "",
"sceneContent": ""
},
"roomDialog": {
"desc_intro": "",
"desc_privacy": "",
"button_startSession": "",
"button_stopSession": "",
"desc_inProgressIntro": "",
"desc_shareLink": "",
"desc_exitSession": "",
"shareTitle": ""
},
"errorDialog": {
"title": ""
},
"exportDialog": {
"disk_title": "",
"disk_details": "",
"disk_button": "",
"link_title": "",
"link_details": "",
"link_button": "",
"excalidrawplus_description": "",
"excalidrawplus_button": "",
"excalidrawplus_exportError": ""
},
"helpDialog": {
"blog": "",
"click": "",
"deepSelect": "",
"deepBoxSelect": "",
"curvedArrow": "",
"curvedLine": "",
"documentation": "",
"doubleClick": "",
"drag": "",
"editor": "",
"editSelectedShape": "",
"github": "",
"howto": "",
"or": "",
"preventBinding": "",
"tools": "",
"shortcuts": "",
"textFinish": "",
"textNewLine": "",
"title": "",
"view": "",
"zoomToFit": "",
"zoomToSelection": "",
"toggleElementLock": ""
},
"clearCanvasDialog": {
"title": ""
},
"publishDialog": {
"title": "",
"itemName": "",
"authorName": "",
"githubUsername": "",
"twitterUsername": "",
"libraryName": "",
"libraryDesc": "",
"website": "",
"placeholder": {
"authorName": "",
"libraryName": "",
"libraryDesc": "",
"githubHandle": "",
"twitterHandle": "",
"website": ""
},
"errors": {
"required": "",
"website": ""
},
"noteDescription": {
"pre": "",
"link": "",
"post": ""
},
"noteGuidelines": {
"pre": "",
"link": "",
"post": ""
},
"noteLicense": {
"pre": "",
"link": "",
"post": ""
},
"noteItems": "",
"atleastOneLibItem": "",
"republishWarning": ""
},
"publishSuccessDialog": {
"title": "",
"content": "",
"link": ""
},
"confirmDialog": {
"resetLibrary": "",
"removeItemsFromLib": ""
},
"encrypted": {
"tooltip": "",
"link": ""
},
"stats": {
"angle": "",
"element": "",
"elements": "",
"height": "",
"scene": "",
"selected": "",
"storage": "",
"title": "",
"total": "",
"version": "",
"versionCopy": "",
"versionNotAvailable": "",
"width": ""
},
"toast": {
"addedToLibrary": "",
"copyStyles": "",
"copyToClipboard": "",
"copyToClipboardAsPng": "",
"fileSaved": "",
"fileSavedToFilename": "",
"canvas": "",
"selection": ""
},
"colors": {
"ffffff": "",
"f8f9fa": "",
"f1f3f5": "",
"fff5f5": "",
"fff0f6": "",
"f8f0fc": "",
"f3f0ff": "",
"edf2ff": "",
"e7f5ff": "",
"e3fafc": "",
"e6fcf5": "",
"ebfbee": "",
"f4fce3": "",
"fff9db": "",
"fff4e6": "",
"transparent": "",
"ced4da": "",
"868e96": "",
"fa5252": "",
"e64980": "",
"be4bdb": "",
"7950f2": "",
"4c6ef5": "",
"228be6": "",
"15aabf": "",
"12b886": "",
"40c057": "",
"82c91e": "",
"fab005": "",
"fd7e14": "",
"000000": "",
"343a40": "",
"495057": "",
"c92a2a": "",
"a61e4d": "",
"862e9c": "",
"5f3dc4": "",
"364fc7": "",
"1864ab": "",
"0b7285": "",
"087f5b": "",
"2b8a3e": "",
"5c940d": "",
"e67700": "",
"d9480f": ""
}
}

View File

@@ -120,13 +120,7 @@
"lockAll": "全部锁定",
"unlockAll": "全部解锁"
},
"statusPublished": "已发布",
"sidebarLock": "侧边栏常驻"
},
"library": {
"noItems": "尚未添加任何项目……",
"hint_emptyLibrary": "选中画布上的项目添加到此处,或从下方的公共素材库中导入。",
"hint_emptyPrivateLibrary": "选中画布上的项目添加到此处。"
"statusPublished": ""
},
"buttons": {
"clearReset": "重置画布",
@@ -187,8 +181,7 @@
"invalidSceneUrl": "无法从提供的 URL 导入场景。它或者格式不正确,或者不包含有效的 Excalidraw JSON 数据。",
"resetLibrary": "这将会清除你的素材库。你确定要这么做吗?",
"removeItemsFromsLibrary": "确定要从素材库中删除 {{count}} 个项目吗?",
"invalidEncryptionKey": "密钥必须包含22个字符。实时协作已被禁用。",
"browserZoom": "您的浏览器缩放程度没有设置为 100%,这可能会导致画板显示不正确"
"invalidEncryptionKey": "密钥必须包含22个字符。实时协作已被禁用。"
},
"errors": {
"unsupportedFileType": "不支持的文件格式。",
@@ -350,7 +343,7 @@
},
"noteItems": "素材库中每个项目都有各自的名称以供筛选。以下项目将被包含:",
"atleastOneLibItem": "请选择至少一个素材库以开始",
"republishWarning": "注意:部分选中的项目已经发布或提交。请仅在更新已有或已提交的素材库时重复提交项目。"
"republishWarning": ""
},
"publishSuccessDialog": {
"title": "素材库已提交",

View File

@@ -120,13 +120,7 @@
"lockAll": "",
"unlockAll": ""
},
"statusPublished": "",
"sidebarLock": ""
},
"library": {
"noItems": "",
"hint_emptyLibrary": "",
"hint_emptyPrivateLibrary": ""
"statusPublished": ""
},
"buttons": {
"clearReset": "清空畫布",
@@ -187,8 +181,7 @@
"invalidSceneUrl": "",
"resetLibrary": "",
"removeItemsFromsLibrary": "",
"invalidEncryptionKey": "",
"browserZoom": ""
"invalidEncryptionKey": ""
},
"errors": {
"unsupportedFileType": "",

View File

@@ -120,13 +120,7 @@
"lockAll": "全部鎖定",
"unlockAll": "全部解鎖"
},
"statusPublished": "已發布",
"sidebarLock": "側欄維持開啟"
},
"library": {
"noItems": "尚未加入任何物件...",
"hint_emptyLibrary": "選取畫布上的物件以加入,或從下方的公開 repository 中安裝資料庫",
"hint_emptyPrivateLibrary": "選擇畫布上的物件以在此加入"
"statusPublished": "已發布"
},
"buttons": {
"clearReset": "重置 canvas",
@@ -187,8 +181,7 @@
"invalidSceneUrl": "無法由提供的 URL 匯入場景。可能是發生異常,或未包含有效的 Excalidraw JSON 資料。",
"resetLibrary": "這會清除您的資料庫,是否確定?",
"removeItemsFromsLibrary": "從資料庫刪除 {{count}} 項?",
"invalidEncryptionKey": "加密鍵必須為22字元。即時協作已停用。",
"browserZoom": "因瀏覽器之縮放值目前並非 100%,可能造成顯示錯誤"
"invalidEncryptionKey": "加密鍵必須為22字元。即時協作已停用。"
},
"errors": {
"unsupportedFileType": "不支援的檔案類型。",

View File

@@ -15,17 +15,9 @@ Please add the latest change on the top under the correct section.
### Excalidraw API
#### Breaking Changes
- `setToastMessage` API is now renamed to `setToast` API and the function signature is also updated [#5427](https://github.com/excalidraw/excalidraw/pull/5427). You can also pass `duration` and `closable` attributes along with `message`.
## 0.12.0 (2022-07-07)
### Excalidraw API
#### Features
- Add [`UIOptions.dockedSidebarBreakpoint`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#dockedSidebarBreakpoint) to customize at which point to break from the docked sidebar [#5274](https://github.com/excalidraw/excalidraw/pull/5274).
- Add `[UIOptions.dockedSidebarBreakpoint]`(https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#dockedSidebarBreakpoint) to customize at which point to break from the docked sidebar [#5274](https://github.com/excalidraw/excalidraw/pull/5274).
- Added support for supplying user `id` in the Collaborator object (see `collaborators` in [`updateScene()`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#updateScene)), which will be used to deduplicate users when rendering collaborator avatar list. Cursors will still be rendered for every user. [#5309](https://github.com/excalidraw/excalidraw/pull/5309)
@@ -67,8 +59,6 @@ Please add the latest change on the top under the correct section.
- Allow returning `null ` in [`renderFooter`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#renderFooter) prop [#5282](https://github.com/excalidraw/excalidraw/pull/5282).
- Transpile `browser-fs-access` dependency so that its `for await` syntax doesn't force `es2018` requirement onto dependent projects [#5041](https://github.com/excalidraw/excalidraw/pull/5041).
- Use `window.EXCALIDRAW_ASSET_PATH` for fonts when exporting to svg [#5065](https://github.com/excalidraw/excalidraw/pull/5065).
- Library menu now properly rerenders if open when library is updated using `updateScene({ libraryItems })` [#4995](https://github.com/excalidraw/excalidraw/pull/4995).
@@ -107,243 +97,11 @@ In Browser :point_down:
React.createElement(ExcalidrawLib.Excalidraw, opts);
```
## Excalidraw Library
### Excalidraw Library
**_This section lists the updates made to the excalidraw library and will not affect the integration._**
#### Chore
### Features
- Throttle scene rendering to animation framerate [#5422](https://github.com/excalidraw/excalidraw/pull/5422)
- Make toast closable and allow custom duration [#5308](https://github.com/excalidraw/excalidraw/pull/5308)
- Collab component state handling rewrite & fixes [#5046](https://github.com/excalidraw/excalidraw/pull/5046)
- Support debugging PWA in dev [#4853](https://github.com/excalidraw/excalidraw/pull/4853)
- Redirect vscode.excalidraw.com to vscode marketplace [#5285](https://github.com/excalidraw/excalidraw/pull/5285)
- Go-to-excalidrawplus button [#5202](https://github.com/excalidraw/excalidraw/pull/5202)
- Autoredirect to Excalidraw+ if special cookie is present [#5183](https://github.com/excalidraw/excalidraw/pull/5183)
- Support resubmitting published library items [#5174](https://github.com/excalidraw/excalidraw/pull/5174)
- Support adding multiple library items on canvas [#5116](https://github.com/excalidraw/excalidraw/pull/5116)
- Support customType in activeTool [#5144](https://github.com/excalidraw/excalidraw/pull/5144)
- Stop event propagation when key handled [#5091](https://github.com/excalidraw/excalidraw/pull/5091)
- Rewrite library state management & related refactor [#5067](https://github.com/excalidraw/excalidraw/pull/5067)
- Delay initial loading message & tweak design [#5049](https://github.com/excalidraw/excalidraw/pull/5049)
- Reconcile when saving to firebase [#4991](https://github.com/excalidraw/excalidraw/pull/4991)
- Hide trash button during collaboration [#5037](https://github.com/excalidraw/excalidraw/pull/5037)
- Refactor local persistence & fix race condition on SW reload [#5032](https://github.com/excalidraw/excalidraw/pull/5032)
- Element locking [#4964](https://github.com/excalidraw/excalidraw/pull/4964)
- Copy to clipboard all text nodes as text [#5013](https://github.com/excalidraw/excalidraw/pull/5013)
- Create and expose serializeLibraryAsJSON [#5009](https://github.com/excalidraw/excalidraw/pull/5009)
- Hide penMode button on reload if not enabled [#4992](https://github.com/excalidraw/excalidraw/pull/4992)
- Eraser toggle to switch back to the previous tool [#4981](https://github.com/excalidraw/excalidraw/pull/4981)
- Save penDetected and penMode, and detect pen already on ToolButton click [#4955](https://github.com/excalidraw/excalidraw/pull/4955)
- Support binding text to container via context menu [#4935](https://github.com/excalidraw/excalidraw/pull/4935)
- Map shortcut O to ellipse and Add eraser shortcut E [#4930](https://github.com/excalidraw/excalidraw/pull/4930)
- Update eraser cursor [#4922](https://github.com/excalidraw/excalidraw/pull/4922)
- Add Eraser 🎉 [#4887](https://github.com/excalidraw/excalidraw/pull/4887)
- Added optional REACT_APP_WS_SERVER_URL for forks usecases [#4889](https://github.com/excalidraw/excalidraw/pull/4889)
- Rewrite collab server connecting [#4881](https://github.com/excalidraw/excalidraw/pull/4881)
- Support vertical text align for bound containers [#4852](https://github.com/excalidraw/excalidraw/pull/4852)
- Support custom colors 🎉 [#4843](https://github.com/excalidraw/excalidraw/pull/4843)
- Support Links in Exported SVG [#4791](https://github.com/excalidraw/excalidraw/pull/4791)
- Scale font size when bound text containers resized with shift pressed [#4828](https://github.com/excalidraw/excalidraw/pull/4828)
### Fixes
- Autorelease job name [#5412](https://github.com/excalidraw/excalidraw/pull/5412)
- Action name for autorelease [#5411](https://github.com/excalidraw/excalidraw/pull/5411)
- Typecast file to fix the build [#5410](https://github.com/excalidraw/excalidraw/pull/5410)
- File handle not persisted when importing excalidraw files [#5372](https://github.com/excalidraw/excalidraw/pull/5372)
- Library not scrollable when no published items installed [#5352](https://github.com/excalidraw/excalidraw/pull/5352)
- Focus traps inside popovers [#5317](https://github.com/excalidraw/excalidraw/pull/5317)
- Unable to use cmd/ctrl-delete/backspace in inputs [#5348](https://github.com/excalidraw/excalidraw/pull/5348)
- Delay loading until language imported [#5344](https://github.com/excalidraw/excalidraw/pull/5344)
- Command to trigger release [#5347](https://github.com/excalidraw/excalidraw/pull/5347)
- Remove unnecessary options passed to language detector [#5336](https://github.com/excalidraw/excalidraw/pull/5336)
- Stale `appState.pendingImageElement` [#5322](https://github.com/excalidraw/excalidraw/pull/5322)
- Non-letter shortcuts being swallowed by color picker [#5316](https://github.com/excalidraw/excalidraw/pull/5316)
- Bind text to correct container when nested [#5307](https://github.com/excalidraw/excalidraw/pull/5307)
- Copy bound text style when copying element having bound text [#5305](https://github.com/excalidraw/excalidraw/pull/5305)
- Copy arrow head when using copy styles [#5303](https://github.com/excalidraw/excalidraw/pull/5303)
- Unsafely accessing draggingElement [#5216](https://github.com/excalidraw/excalidraw/pull/5216)
- Library load button does not work [#5205](https://github.com/excalidraw/excalidraw/pull/5205)
- Do not deselect when not zooming using touchscreen pinch [#5181](https://github.com/excalidraw/excalidraw/pull/5181)
- Wheel zoom normalization [#5165](https://github.com/excalidraw/excalidraw/pull/5165)
- Hide sidebar when `custom` tool active [#5179](https://github.com/excalidraw/excalidraw/pull/5179)
- Don't save deleted ExcalidrawElements to Firebase [#5108](https://github.com/excalidraw/excalidraw/pull/5108)
- Eraser removed deleted elements [#5155](https://github.com/excalidraw/excalidraw/pull/5155)
- Handle `ColorPicker` parentSelector being undefined [#5152](https://github.com/excalidraw/excalidraw/pull/5152)
- Library multiselect not accounting for published state [#5132](https://github.com/excalidraw/excalidraw/pull/5132)
- Chart display fix [#5154](https://github.com/excalidraw/excalidraw/pull/5154)
- Update opacity of bound text when opacity of container updated [#5142](https://github.com/excalidraw/excalidraw/pull/5142)
- Jumping of text when typing single line in bound text [#5139](https://github.com/excalidraw/excalidraw/pull/5139)
- Remove opacity scroll wheel interaction [#5111](https://github.com/excalidraw/excalidraw/pull/5111)
- Propagate keydown events from excalidraw-wysiwyg inputs [#5099](https://github.com/excalidraw/excalidraw/pull/5099)
- Don't bind text to container if double clicked else instead of center [#5105](https://github.com/excalidraw/excalidraw/pull/5105)
- ToolIcon height not using rem [#5092](https://github.com/excalidraw/excalidraw/pull/5092)
- Excalidraw named export type [#5078](https://github.com/excalidraw/excalidraw/pull/5078)
- BoundElementIds when arrows bound to elements are deleted [#5077](https://github.com/excalidraw/excalidraw/pull/5077)
- Don't merge libraryItems on updateScene [#5076](https://github.com/excalidraw/excalidraw/pull/5076)
- SVG metadata extraction regex on multiline elements [#5074](https://github.com/excalidraw/excalidraw/pull/5074)
- Eraser cursor showing on theme change when not using eraser [#4990](https://github.com/excalidraw/excalidraw/pull/4990)
- Update `storage.rules` [#5020](https://github.com/excalidraw/excalidraw/pull/5020)
- Add image button not working on iPad [#5038](https://github.com/excalidraw/excalidraw/pull/5038)
- Ensure svg image dimensions are always set [#5044](https://github.com/excalidraw/excalidraw/pull/5044)
- Pinch zoom in view mode [#5001](https://github.com/excalidraw/excalidraw/pull/5001)
- Select whole group on righclick & few lock-related fixes [#5022](https://github.com/excalidraw/excalidraw/pull/5022)
- Export serializeLibraryAsJSON from the package [#5017](https://github.com/excalidraw/excalidraw/pull/5017)
- Support copying PNG to clipboard on Safari [#3746](https://github.com/excalidraw/excalidraw/pull/3746)
- More copyText fixes [#5016](https://github.com/excalidraw/excalidraw/pull/5016)
- Copy to clipboard all text nodes as text [#5014](https://github.com/excalidraw/excalidraw/pull/5014)
- Update cursorButton once freedraw is released [#4996](https://github.com/excalidraw/excalidraw/pull/4996)
- Decouple actionFinalize and actionErase [#4984](https://github.com/excalidraw/excalidraw/pull/4984)
- Using stale state when switching tools [#4989](https://github.com/excalidraw/excalidraw/pull/4989)
- UpdateWysiwygStyle updatedElement is undefined TypeError [#4980](https://github.com/excalidraw/excalidraw/pull/4980)
- Adding check for link length to prevent early return [#4982](https://github.com/excalidraw/excalidraw/pull/4982)
- Show link icon for bound text containers [#4960](https://github.com/excalidraw/excalidraw/pull/4960)
- Cancel erase elements on pointer up if eraser is not active on pointer up [#4956](https://github.com/excalidraw/excalidraw/pull/4956)
- Restore original opacities when alt pressed while erasing [#4954](https://github.com/excalidraw/excalidraw/pull/4954)
- Don't bind text to container if already present [#4946](https://github.com/excalidraw/excalidraw/pull/4946)
- Erase all elements which are hit with single point click [#4934](https://github.com/excalidraw/excalidraw/pull/4934)
- Add multiElement-edit finalize action to Desktop (currently only visible in Mobile view) [#4764](https://github.com/excalidraw/excalidraw/pull/4764)
- Hide eraser in view mode in desktop [#4929](https://github.com/excalidraw/excalidraw/pull/4929)
- Undo when erasing elements by clicking [#4921](https://github.com/excalidraw/excalidraw/pull/4921)
- Undo when erasing [#4900](https://github.com/excalidraw/excalidraw/pull/4900)
- Incorrectly erasing on mobile [#4899](https://github.com/excalidraw/excalidraw/pull/4899)
- Don't crash on drop highlighted text onto canvas [#4890](https://github.com/excalidraw/excalidraw/pull/4890)
- Paste styles shortcut [#4886](https://github.com/excalidraw/excalidraw/pull/4886)
- Freedraw element's background fill color missing from SVG when exporting with package API exportToSvg() [#4871](https://github.com/excalidraw/excalidraw/pull/4871)
- Improve pointer syncing performance [#4883](https://github.com/excalidraw/excalidraw/pull/4883)
- Collab room initialization [#4882](https://github.com/excalidraw/excalidraw/pull/4882)
- Ensure verticalAlign properties not shown when no element selected [#4860](https://github.com/excalidraw/excalidraw/pull/4860)
- Binding text to non-bindable containers and not always preferring selection [#4655](https://github.com/excalidraw/excalidraw/pull/4655)
- Don't show align icons for single bound container element [#4846](https://github.com/excalidraw/excalidraw/pull/4846)
- Redraw text bounding box when pasting styles [#4845](https://github.com/excalidraw/excalidraw/pull/4845)
- Restore cursor position after bound text container value updated [#4836](https://github.com/excalidraw/excalidraw/pull/4836)
- Support resizing multiple bound text containers [#4824](https://github.com/excalidraw/excalidraw/pull/4824)
- Also check overflowY: overlay in detectScroll [#4806](https://github.com/excalidraw/excalidraw/pull/4806)
- Stuck resizing when resizing bound text container very fast beyond threshold [#4804](https://github.com/excalidraw/excalidraw/pull/4804)
### Refactor
- Don't pass array to handleBindTextResize [#4826](https://github.com/excalidraw/excalidraw/pull/4826)
### Build
- Extract all i18n files into locales folder [#5419](https://github.com/excalidraw/excalidraw/pull/5419)
- Automate release step fully [#5414](https://github.com/excalidraw/excalidraw/pull/5414)
- Use next and preview tags instead of separate packages for next and preview release [#5346](https://github.com/excalidraw/excalidraw/pull/5346)
- Support runtime React Jsx in @excalidraw/utils [#4866](https://github.com/excalidraw/excalidraw/pull/4866)
- Release @excalidraw/utils 0.1.1 [#4862](https://github.com/excalidraw/excalidraw/pull/4862)
- Remove build packages workflow [#4835](https://github.com/excalidraw/excalidraw/pull/4835)
---
- Transpile `browser-fs-access` dependency so that its `for await` syntax doesn't force es2018 requirement onto dependent projects [#5041](https://github.com/excalidraw/excalidraw/pull/5041).
## 0.11.0 (2022-02-17)

View File

@@ -1,7 +1,3 @@
#### Note
⚠️ ⚠️ ⚠️ You are viewing the docs for the **next** release, in case you want to check the docs for the stable release, you can view it [here](https://www.npmjs.com/package/@excalidraw/excalidraw).
### Excalidraw
Excalidraw exported as a component to directly embed in your projects.
@@ -30,7 +26,7 @@ If you want to load assets from a different path you can set a variable `window.
#### Note
**If you don't want to wait for the next stable release and try out the unreleased changes you can use `@excalidraw/excalidraw@next`.**
**If you don't want to wait for the next stable release and try out the unreleased changes you can use [@excalidraw/excalidraw-next](https://www.npmjs.com/package/@excalidraw/excalidraw-next).**
### Demo
@@ -46,7 +42,7 @@ If you are using a Web bundler (for instance, Webpack), you can import it as an
```js
import React, { useEffect, useState, useRef } from "react";
import { Excalidraw } from "@excalidraw/excalidraw";
import Excalidraw from "@excalidraw/excalidraw";
import InitialData from "./initialData";
import "./styles.scss";
@@ -326,7 +322,7 @@ const App = () => {
className: "excalidraw-wrapper",
ref: excalidrawWrapperRef,
},
React.createElement(ExcalidrawLib.Excalidraw, {
React.createElement(Excalidraw.default, {
initialData: InitialData,
onChange: (elements, state) =>
console.log("Elements :", elements, "State : ", state),
@@ -381,7 +377,7 @@ For a complete list of variables, check [theme.scss](https://github.com/excalidr
| 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#L106">ExcalidrawElement[]</a>, appState?: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L79">AppState<a> } </pre> | null | The initial data with which app loads. |
| [`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#L42">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) &#124; [`useRef`](https://reactjs.org/docs/hooks-reference.html#useref) &#124; [`callbackRef`](https://reactjs.org/docs/refs-and-the-dom.html#callback-refs) &#124; <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 |
@@ -403,9 +399,7 @@ For a complete list of variables, check [theme.scss](https://github.com/excalidr
| [`onLibraryChange`](#onLibraryChange) | <pre>(items: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L200">LibraryItems</a>) => void &#124; Promise&lt;any&gt; </pre> | | The callback if supplied is triggered when the library is updated and receives the library items. |
| [`autoFocus`](#autoFocus) | boolean | false | Implies whether to focus the Excalidraw component on page load |
| [`generateIdForFile`](#generateIdForFile) | `(file: File) => string | Promise<string>` | Allows you to override `id` generation for files added on canvas |
| [`onLinkOpen`](#onLinkOpen) | <pre>(element: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L106">NonDeletedExcalidrawElement</a>, event: CustomEvent) </pre> | | This prop if passed will be triggered when link of an element is clicked. |
| [`onPointerDown`](#onPointerDown) | <pre>(activeTool: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L93"> AppState["activeTool"]</a>, pointerDownState: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L365">PointerDownState</a>) => void</pre> | | This prop if passed gets triggered on pointer down evenets |
| [`onScrollChange`](#onScrollChange) | (scrollX: number, scrollY: number) | | This prop if passed gets triggered when scrolling the canvas. |
| [`onLinkOpen`](#onLinkOpen) | <pre>(element: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L78">NonDeletedExcalidrawElement</a>, event: CustomEvent) </pre> | | This prop if passed will be triggered when link of an element is clicked |
### Dimensions of Excalidraw
@@ -419,9 +413,9 @@ Every time component updates, this callback if passed will get triggered and has
(excalidrawElements, appState, files) => void;
```
1.`excalidrawElements`: Array of [excalidrawElements](https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L106) in the scene.
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#L79) of the scene.
2.`appState`: [AppState](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L42) of the scene.
3. `files`: The [`BinaryFiles`]([BinaryFiles](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L64) which are added to the scene.
@@ -433,10 +427,10 @@ This helps to load Excalidraw with `initialData`. It must be an object or a [pro
| Name | Type | Description |
| --- | --- | --- |
| `elements` | [ExcalidrawElement[]](https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L106) | The elements with which Excalidraw should be mounted. |
| `appState` | [AppState](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L79) | The App state with which Excalidraw should be mounted. |
| `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#L42) | 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 |
| `libraryItems` | [LibraryItems](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L200) &#124; Promise<[LibraryItems](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L200)> | This library items with which Excalidraw should be mounted. |
| `libraryItems` | [LibraryItems](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L151) | This library items with which Excalidraw should be mounted. |
| `files` | [BinaryFiles](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L64) | The files added to the scene. |
```json
@@ -475,26 +469,22 @@ You might want to use this when you want to load excalidraw with some initial el
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](#updateScene) | <code>(scene: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L207">sceneData</a>) => void </code> | updates the scene with the sceneData |
| [updateLibrary](#updateLibrary) | <code>(<a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/library.ts#L136">opts</a>) => Promise<<a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L200">LibraryItems</a>> </code> | updates the scene with the sceneData |
| [addFiles](#addFiles) | <code>(files: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts">BinaryFileData</a>) => void </code> | add files data to the appState |
| [updateScene](#updateScene) | <pre>(scene: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L207">sceneData</a>) => void </pre> | updates the scene with the sceneData |
| [addFiles](#addFiles) | <pre>(files: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts">BinaryFileData</a>) => void </pre> | add files data to the appState |
| 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 | <code> () => <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L106">ExcalidrawElement[]</a></code> | Returns all the elements including the deleted in the scene |
| getSceneElements | <code> () => <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L106">ExcalidrawElement[]</a></code> | Returns all the elements excluding the deleted in the scene |
| getAppState | <code> () => <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L66">AppState</a></code> | Returns current appState |
| 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#L42">AppState</a></pre> | Returns current appState |
| history | `{ clear: () => void }` | This is the history API. `history.clear()` will clear the history |
| scrollToContent | <code> (target?: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L106">ExcalidrawElement</a> &#124; <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L106">ExcalidrawElement</a>[]) => void </code> | Scroll the nearest element out of the elements supplied to the center. Defaults to the elements on the scene. |
| scrollToContent | <pre> (target?: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L78">ExcalidrawElement</a> &#124; <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L78">ExcalidrawElement</a>[]) => void </pre> | Scroll the nearest element out of the elements supplied to the center. Defaults to the elements on the scene. |
| 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 |
| [setToast](#setToast) | `({message: string, closable?:boolean, duration?:number} | null) => void` | This API can be used to show the toast with custom message. |
| setToastMessage | `(message: string) => void` | This API can be used to show the toast with custom message. |
| [id](#id) | string | Unique ID for the excalidraw component. |
| [getFiles](#getFiles) | <code>() => <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L64">files</a> </code> | This API can be used to get the files present in the scene. It may contain files that aren't referenced by any element, so if you're persisting the files to a storage, you should compare them against stored elements. |
| [setActiveTool](#setActiveTool) | <code>(tool: { type: typeof <a href="https://github.com/excalidraw/excalidraw/blob/master/src/shapes.tsx#L4">SHAPES</a>[number]["value"] &#124; "eraser" } &#124; { type: "custom"; customType: string }) => void</code> | This API can be used to set the active tool |
| [setCursor](#setCursor) | <code>(cursor: string) => void </code> | This API can be used to set customise the mouse cursor on the canvas |
| [resetCursor](#resetCursor) | <code>() => void </code> | This API can be used to reset to default mouse cursor on the canvas |
| [getFiles](#getFiles) | <pre>() => <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L64">files</a> </pre> | This API can be used to get the files present in the scene. It may contain files that aren't referenced by any element, so if you're persisting the files to a storage, you should compare them against stored elements. |
#### `readyPromise`
@@ -516,31 +506,9 @@ You can use this function to update the scene with the sceneData. It accepts the
| --- | --- | --- |
| `elements` | [`ImportedDataState["elements"]`](https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L17) | The `elements` to be updated in the scene |
| `appState` | [`ImportedDataState["appState"]`](https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L18) | The `appState` to be updated in the scene. |
| `collaborators` | <pre>Map<string, <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L35">Collaborator></a></pre> | The list of collaborators to be updated in the scene. |
| `collaborators` | <pre>Map<string, <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L29">Collaborator></a></pre> | The list of collaborators to be updated in the scene. |
| `commitToHistory` | `boolean` | Implies if the `history (undo/redo)` should be recorded. Defaults to `false`. |
| `libraryItems` | [LibraryItems](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L200) &#124; Promise<[LibraryItems](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L200)> &#124; ((currentItems: [LibraryItems](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L200)>) => [LibraryItems](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L200) &#124; Promise<[LibraryItems](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L200)>) | The `libraryItems` to be update in the scene. |
### `updateLibrary`
<pre>
(opts: {
libraryItems: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L224">LibraryItemsSource</a>;
merge?: boolean;
prompt?: boolean;
openLibraryMenu?: boolean;
defaultStatus?: "unpublished" | "published";
}) => Promise<<a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L200">LibraryItems</a>>
</pre>
You can use this function to update the library. It accepts the below attributes.
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `libraryItems` | | [LibraryItems](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L224) | The `libraryItems` to be replaced/merged with current library |
| `merge` | boolean | `false` | Whether to merge with existing library items. |
| `prompt` | boolean | `false` | Whether to prompt user for confirmation. |
| `openLibraryMenu` | boolean | `false` | Whether to open the library menu before importing. |
| `defaultStatus` | <code>"unpublished" &#124; "published"</code> | `"unpublished"` | Default library item's `status` if not present. |
| `libraryItems` | [LibraryItems](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L258) | The `libraryItems` to be update in the scene. |
### `addFiles`
@@ -575,7 +543,7 @@ This callback is triggered when mouse pointer is updated.
```
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#L79) of the scene.
2. `appState`: [AppState](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L42) of the scene.
3. `canvas`: The `HTMLCanvasElement` of the scene.
#### `langCode`
@@ -594,7 +562,7 @@ import { defaultLang, languages } from "@excalidraw/excalidraw";
#### `renderTopRightUI`
<pre>
(isMobile: boolean, appState: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L79">AppState</a>) => JSX | null
(isMobile: boolean, appState: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L42">AppState</a>) => JSX
</pre>
A function returning JSX to render custom UI in the top right corner of the app.
@@ -602,7 +570,7 @@ A function returning JSX to render custom UI in the top right corner of the app.
#### `renderFooter`
<pre>
(isMobile: boolean, appState: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L79">AppState</a>) => JSX | null
(isMobile: boolean, appState: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L42">AppState</a>) => JSX
</pre>
A function returning JSX to render 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).
@@ -637,7 +605,7 @@ This prop sets the name of the drawing which will be used when exporting the dra
#### `UIOptions`
This prop can be used to customise UI of Excalidraw. Currently we support customising [`canvasActions`](#canvasActions) and [`dockedSidebarBreakpoint`](dockedSidebarBreakpoint). It accepts the below parameters
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/> }
@@ -655,12 +623,6 @@ This prop can be used to customise UI of Excalidraw. Currently we support custom
| `theme` | boolean | true | Implies whether to show `Theme toggle` |
| `saveAsImage` | boolean | true | Implies whether to show `Save as image button` |
##### `dockedSidebarBreakpoint`
This prop indicates at what point should we break to a docked, permanent sidebar. If not passed it defaults to [`MQ_RIGHT_SIDEBAR_MAX_WIDTH_PORTRAIT`](https://github.com/excalidraw/excalidraw/blob/master/src/constants.ts#L167). If the `width` of the `excalidraw` container exceeds `dockedSidebarBreakpoint`, the sidebar will be dockable. If user choses to dock the sidebar, it will push the right part of the UI towards the left, making space for the sidebar as shown below.
![image](https://user-images.githubusercontent.com/11256141/174664866-c698c3fa-197b-43ff-956c-d79852c7b326.png)
#### `exportOpts`
The below attributes can be set in `UIOptions.canvasActions.export` to customize the export dialog. If `UIOptions.canvasActions.export` is `false` the export button will not be rendered.
@@ -705,48 +667,6 @@ useEffect(() => {
Try out the [Demo](#Demo) to see it in action.
#### `setToast`
This API can be used to show the toast with custom message.
<pre>
({ message: string,
closable?:boolean,
duration?:number } | null) => void
</pre>
| Attribute | type | Description |
| --- | --- | --- |
| message | string | The message to be shown on the toast. |
| closable | boolean | Indicates whether to show the closable button on toast to dismiss the toast. |
| duration | number | Determines the duration after which the toast should auto dismiss. To prevent autodimiss you can pass `Infinity`. |
To dismiss an existing toast you can simple pass `null`
```js
setToast(null);
```
#### `setActiveTool`
This API has the below signature. It sets the `tool` passed in param as the active tool.
<pre>
(tool: { type: typeof <a href="https://github.com/excalidraw/excalidraw/blob/master/src/shapes.tsx#L4">SHAPES</a>[number]["value"] &#124; "eraser" } &#124; { type: "custom"; customType: string }) => void
</pre>
#### `setCursor`
This API can be used to customise the mouse cursor on the canvas and has the below signature. It sets the mouse cursor to the cursor passed in param.
<pre>
(cursor: string) => void
</pre>
#### `resetCursor`
This API can be used to reset to default mouse cursor.
#### `detectScroll`
Indicates whether Excalidraw should listen for `scroll` event on the nearest scrollable container in the DOM tree and recompute the coordinates (e.g. to correctly handle the cursor) when the component's position changes. You can disable this when you either know this doesn't affect your app or you want to take care of it yourself (calling the [`refresh()`](#ref) method).
@@ -816,22 +736,6 @@ const onLinkOpen: ExcalidrawProps["onLinkOpen"] = useCallback(
);
```
#### `onPointerDown`
This prop if passed will be triggered on pointer down events and has the below signature.
<pre>
(activeTool: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L93"> AppState["activeTool"]</a>, pointerDownState: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L365">PointerDownState</a>) => void
</pre>
#### `onScrollChange`
This prop if passed will be triggered when canvas is scrolled and has the below signature.
```ts
(scrollX: number, scrollY: number) => void
```
### Does it support collaboration ?
No, Excalidraw package doesn't come with collaboration built in, since the implementation is specific to each host app. We expose APIs which you can use to communicate with Excalidraw which you can use to implement it. You can check our own implementation [here](https://github.com/excalidraw/excalidraw/blob/master/src/excalidraw-app/index.tsx).
@@ -843,7 +747,7 @@ No, Excalidraw package doesn't come with collaboration built in, since the imple
**_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#L79">AppState</a>> | null): <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L79">AppState</a>
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#L42">AppState</a>> | null): <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L42">AppState</a>
</pre>
**_How to use_**
@@ -852,7 +756,7 @@ restoreAppState(appState: <a href="https://github.com/excalidraw/excalidraw/blob
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#L79) and if any key is missing, it will be set to default value.
This function will make sure all the keys have appropriate values in [appState](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L42) and if any key is missing, it will be set to default value.
When `localAppState` is supplied, it's used in place of values that are missing (`undefined`) in `appState` instead of defaults. Use this as a way to not override user's defaults if you persist them. Required: supply `null`/`undefined` if not applicable.
@@ -861,7 +765,7 @@ When `localAppState` is supplied, it's used in place of values that are missing
**_Signature_**
<pre>
restoreElements(elements: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L16">ImportedDataState["elements"]</a>, localElements: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L16">ExcalidrawElement[]</a> | null | undefined): <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L106">ExcalidrawElement[]</a>
restoreElements(elements: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L16">ImportedDataState["elements"]</a>, localElements: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L16">ExcalidrawElement[]</a> | null | undefined): <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L78">ExcalidrawElement[]</a>
</pre>
**_How to use_**
@@ -879,7 +783,7 @@ When `localElements` are supplied, they are used to ensure that existing restore
**_Signature_**
<pre>
restoreElements(data: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L12">ImportedDataState</a>, localAppState: Partial<<a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L79">AppState</a>> | null | undefined, localElements: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L16">ExcalidrawElement[]</a> | null | undefined): <a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L4">DataState</a>
restoreElements(data: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L12">ImportedDataState</a>, localAppState: Partial<<a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L42">AppState</a>> | null | undefined, localElements: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L16">ExcalidrawElement[]</a> | null | undefined): <a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L4">DataState</a>
</pre>
See [`restoreAppState()`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#restoreAppState) about `localAppState`, and [`restoreElements()`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#restoreElements) about `localElements`.
@@ -892,24 +796,6 @@ 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 a combination of [restoreElements](#restoreElements) and [restoreAppState](#restoreAppState).
#### `restoreLibraryItems`
**_Signature_**
<pre>
restoreLibraryItems(libraryItems: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L22">ImportedDataState["libraryItems"]</a>, defaultStatus: "published" | "unpublished")
</pre>
**_How to use_**
```js
import { restoreLibraryItems } from "@excalidraw/excalidraw";
restoreLibraryItems(libraryItems, "unpublished");
```
This function normalizes library items elements, adding missing values when needed.
### Export utilities
#### `exportToCanvas`
@@ -947,7 +833,7 @@ This function returns the canvas with the exported elements, appState and dimens
<pre>
exportToBlob(
opts: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/packages/utils.ts#L14">ExportOpts</a> & {
opts: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/packages/utils.ts#L10">ExportOpts</a> & {
mimeType?: string,
quality?: number;
})
@@ -973,8 +859,8 @@ Returns a promise which resolves with a [blob](https://developer.mozilla.org/en-
<pre>
exportToSvg({
elements: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L106">ExcalidrawElement[]</a>,
appState: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L79">AppState</a>,
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,
files?: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L64">BinaryFiles</a>
@@ -983,41 +869,13 @@ exportToSvg({
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| elements | [Excalidraw Element []](https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L106) | | The elements to exported as svg |
| appState | [AppState](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L79) | [defaultAppState](https://github.com/excalidraw/excalidraw/blob/master/src/appState.ts#L11) | The app state of the scene |
| 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 |
| files | [BinaryFiles](The [`BinaryFiles`](<[BinaryFiles](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L64)>) | undefined | The files added to the scene. |
This function returns a promise which resolves to svg of the exported drawing.
#### `exportToClipboard`
**_Signature_**
<pre>
exportToClipboard(
opts: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/packages/utils.ts#L14">ExportOpts</a> & {
mimeType?: string,
quality?: number;
type: 'png' | 'svg' |'json'
})
</pre>
| Name | Type | Default | Description |
| --- | --- | --- | --- | --- | --- |
| opts | | | This param is same as the params passed to `exportToCanvas`. You can refer to [`exportToCanvas`](#exportToCanvas). |
| mimeType | string | "image/png" | Indicates the image format, this will be used when exporting as `png`. |
| 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. This will be used when exporting as `png`. |
| type | 'png' | 'svg' | 'json' | | This determines the format to which the scene data should be exported. |
**How to use**
```js
import { exportToClipboard } from "@excalidraw/excalidraw";
```
Copies the scene data in the specified format (determined by `type`) to clipboard.
##### Additional attributes of appState for `export\*` APIs
| Name | Type | Default | Description |
@@ -1025,7 +883,7 @@ Copies the scene data in the specified format (determined by `type`) to clipboar
| exportBackground | boolean | true | Indicates whether background should be exported |
| viewBackgroundColor | string | #fff | The default background color |
| exportWithDarkMode | boolean | false | Indicates whether to export with dark mode |
| exportEmbedScene | boolean | false | Indicates whether scene data should be embedded in svg/png. This will increase the image size. |
| exportEmbedScene | boolean | false | Indicates whether scene data should be embedded in svg. This will increase the svg size. |
### Extra API's
@@ -1035,35 +893,20 @@ Copies the scene data in the specified format (determined by `type`) to clipboar
<pre>
serializeAsJSON({
elements: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L106">ExcalidrawElement[]</a>,
appState: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L79">AppState</a>,
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>,
}): string
</pre>
Takes the scene elements and state and returns a JSON string. Deleted `elements`as well as most properties from `AppState` are removed from the resulting JSON. (see [`serializeAsJSON()`](https://github.com/excalidraw/excalidraw/blob/master/src/data/json.ts#L16) source for details).
If you want to overwrite the source field in the JSON string, you can set `window.EXCALIDRAW_EXPORT_SOURCE` to the desired value.
#### `serializeLibraryAsJSON`
**_Signature_**
<pre>
serializeLibraryAsJSON({
libraryItems: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L200">LibraryItems[]</a>,
</pre>
Takes the library items and returns a JSON string.
If you want to overwrite the source field in the JSON string, you can set `window.EXCALIDRAW_EXPORT_SOURCE` to the desired value.
#### `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#L106">ExcalidrawElement[]</a>)
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.
@@ -1073,7 +916,7 @@ This function returns the current scene version.
**_Signature_**
<pre>
isInvisiblySmallElement(element: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L106">ExcalidrawElement</a>): boolean
isInvisiblySmallElement(element: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L78">ExcalidrawElement</a>): boolean
</pre>
**How to use**
@@ -1104,51 +947,15 @@ This function loads the library from the blob.
```js
import { loadFromBlob } from "@excalidraw/excalidraw";
const scene = await loadFromBlob(file, null, null);
excalidrawAPI.updateScene(scene);
```
**Signature**
<pre>
loadFromBlob(
blob: <a href="https://developer.mozilla.org/en-US/docs/Web/API/Blob">Blob</a>,
localAppState: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L79">AppState</a> | null,
localElements: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L106">ExcalidrawElement[]</a> | null,
fileHandle?: FileSystemHandle | null
) => Promise<<a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/restore.ts#L53">RestoredDataState</a>>
loadFromBlob(blob: <a href="https://developer.mozilla.org/en-US/docs/Web/API/Blob">Blob</a>, localAppState: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L42">AppState</a> | null)
</pre>
This function loads the scene data from the blob (or file). If you pass `localAppState`, `localAppState` value will be preferred over the `appState` derived from `blob`. Throws if blob doesn't contain valid scene data.
#### `loadSceneOrLibraryFromBlob`
**How to use**
```js
import { loadSceneOrLibraryFromBlob, MIME_TYPES } from "@excalidraw/excalidraw";
const contents = await loadSceneOrLibraryFromBlob(file, null, null);
if (contents.type === MIME_TYPES.excalidraw) {
excalidrawAPI.updateScene(contents.data);
} else if (contents.type === MIME_TYPES.excalidrawlib) {
excalidrawAPI.updateLibrary(contents.data);
}
```
**Signature**
<pre>
loadSceneOrLibraryFromBlob(
blob: <a href="https://developer.mozilla.org/en-US/docs/Web/API/Blob">Blob</a>,
localAppState: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L79">AppState</a> | null,
localElements: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L106">ExcalidrawElement[]</a> | null,
fileHandle?: FileSystemHandle | null
) => Promise<{ type: string, data: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/restore.ts#L53">RestoredDataState</a> | <a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L33">ImportedLibraryState</a>}>
</pre>
This function loads either scene or library data from the supplied blob. If the blob contains scene data, and you pass `localAppState`, `localAppState` value will be preferred over the `appState` derived from `blob`. Throws if blob doesn't contain neither valid scene data or library data.
This function loads the scene data from the blob. If you pass `localAppState`, `localAppState` value will be preferred over the `appState` derived from `blob`
#### `getFreeDrawSvgPath`
@@ -1198,93 +1005,6 @@ getNonDeletedElements(elements: <a href="https://github.com/excalidraw/excalidra
This function returns an array of deleted elements.
#### `mergeLibraryItems`
```js
import { mergeLibraryItems } from "@excalidraw/excalidraw";
```
**_Signature_**
<pre>
mergeLibraryItems(localItems: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L200">LibraryItems</a>, otherItems: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L200">LibraryItems</a>) => <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L200">LibraryItems</a>
</pre>
This function merges two `LibraryItems` arrays, where unique items from `otherItems` are sorted first in the returned array.
#### `parseLibraryTokensFromUrl`
**How to use**
```js
import { parseLibraryTokensFromUrl } from "@excalidraw/excalidraw";
```
**Signature**
<pre>
parseLibraryTokensFromUrl(): {
libraryUrl: string;
idToken: string | null;
} | null
</pre>
Parses library parameters from URL if present (expects the `#addLibrary` hash key), and returns an object with the `libraryUrl` and `idToken`. Returns `null` if `#addLibrary` hash key not found.
#### `useHandleLibrary`
**How to use**
```js
import { useHandleLibrary } from "@excalidraw/excalidraw";
export const App = () => {
// ...
useHandleLibrary({ excalidrawAPI });
};
```
**Signature**
<pre>
useHandleLibrary(opts: {
excalidrawAPI: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L432">ExcalidrawAPI</a>,
getInitialLibraryItems?: () => <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L224">LibraryItemsSource</a>
});
</pre>
A hook that automatically imports library from url if `#addLibrary` hash key exists on initial load, or when it changes during the editing session (e.g. when a user installs a new library), and handles initial library load if `getInitialLibraryItems` getter is supplied.
In the future, we will be adding support for handling library persistence to browser storage (or elsewhere).
#### `sceneCoordsToViewportCoords`
```js
import { sceneCoordsToViewportCoords } from "@excalidraw/excalidraw";
```
**_Signature_**
<pre>
sceneCoordsToViewportCoords({sceneX: number, sceneY: number}, appState: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L79">AppState</a>): {x: number, y: number}
</pre>
This function returns equivalent viewport coords for the provided scene coords in params.
#### `viewportCoordsToSceneCoords`
```js
import { viewportCoordsToSceneCoords } from "@excalidraw/excalidraw";
```
**_Signature_**
<pre>
viewportCoordsToSceneCoords({clientX: number, clientY: number}, appState: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L79">AppState</a>): {x: number, y: number}
</pre>
This function returns equivalent scene coords for the provided viewport coords in params.
### Exported constants
#### `FONT_FAMILY`
@@ -1322,16 +1042,6 @@ import { THEME } from "@excalidraw/excalidraw";
Defaults to `THEME.LIGHT` unless passed in `initialData.appState.theme`
### `MIME_TYPES`
**How to use **
```js
import { MIME_TYPES } from "@excalidraw/excalidraw";
```
[`MIME_TYPES`](https://github.com/excalidraw/excalidraw/blob/master/src/constants.ts#L92) contains all the mime types supported by `Excalidraw`.
## Need help?
Check out the existing [Q&A](https://github.com/excalidraw/excalidraw/discussions?discussions_q=label%3Apackage%3Aexcalidraw). If you have any queries or need help, ask us [here](https://github.com/excalidraw/excalidraw/discussions?discussions_q=label%3Apackage%3Aexcalidraw).
@@ -1359,27 +1069,7 @@ The example is same as the [codesandbox example](https://ehlz3.csb.app/)
You can create a test release by posting the below comment in your pull request
```
@excalibot trigger release
@excalibot release package
```
Once the version is released `@excalibot` will post a comment with the release version.
#### Creating a production release
To release the next stable version follow the below steps
```
yarn prerelease version
```
You need to pass the `version` for which you want to create the release. This will make the changes needed before making the release like updating `package.json`, `changelog` and more.
The next step is to run the `release` script
```
yarn release
```
This will publish the package.
Right now there are two steps to create a production release but once this works fine these scripts will be combined and more automation will be done.

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