Compare commits
104 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 88691b1c3c | |||
| 146c510faa | |||
| ff29780760 | |||
| 463857ad9a | |||
| be2da9539e | |||
| bb7829ef90 | |||
| 1104f6891e | |||
| a97e172070 | |||
| 39d45afc06 | |||
| 00c6940851 | |||
| 982cba2035 | |||
| 54739cd2df | |||
| 75aeaa6c38 | |||
| bea4a1e066 | |||
| e8b462cc31 | |||
| c86c176e10 | |||
| b09c11bb14 | |||
| 7199d13f48 | |||
| 7d1fddc144 | |||
| 5da3207633 | |||
| 8c9786e026 | |||
| f0f13ed694 | |||
| 850d8eb47e | |||
| f287f9c002 | |||
| 78df5bc852 | |||
| f0073c7e26 | |||
| fa7a313412 | |||
| 8b3f236cd8 | |||
| 621812d0eb | |||
| d607249205 | |||
| df28c3299f | |||
| b00a57b4be | |||
| 9277e839db | |||
| 0d5d60944f | |||
| 489a652d73 | |||
| 2b85d96121 | |||
| 6ce535d3a4 | |||
| da43cf5635 | |||
| 603ecfba34 | |||
| a589708737 | |||
| 4df401d012 | |||
| b2c4552416 | |||
| 5cae218f1b | |||
| 4be726d405 | |||
| 99623334d1 | |||
| 685abac81a | |||
| 9581c45522 | |||
| 0749d2c1f3 | |||
| 8787f3dc60 | |||
| 5fabc57277 | |||
| e7cbb859f0 | |||
| aa860251c7 | |||
| 380aaa30e6 | |||
| 2e61fec7a6 | |||
| 3c295559c7 | |||
| 55d3287abf | |||
| e3e967421e | |||
| 77aae63006 | |||
| ee64a7e264 | |||
| 097362662d | |||
| 038e9c13dd | |||
| bc8ba08ad0 | |||
| f861a9fdd0 | |||
| 62303b8a22 | |||
| 9cc741ab3a | |||
| 2d279cbb02 | |||
| 57ea4fdf9a | |||
| 44402f42bf | |||
| bdead4d164 | |||
| bfc0656475 | |||
| a33a3334f7 | |||
| 969d3c694a | |||
| 5cd921549a | |||
| 437afcbea4 | |||
| 6dee02e320 | |||
| 74a2f16501 | |||
| fd4460be37 | |||
| e82d0493cf | |||
| 083cb4c656 | |||
| d067365c1d | |||
| 273cac6b60 | |||
| b9337b8a36 | |||
| 0e0025921b | |||
| efc01ddab1 | |||
| 7bce22b114 | |||
| aab4965bbb | |||
| 486a9a3da8 | |||
| 2425c06082 | |||
| 79ea844901 | |||
| 6690215cd1 | |||
| 7f5e783fe8 | |||
| 9325109836 | |||
| 69b6fbb3f4 | |||
| 6b6002baae | |||
| 54dcb73701 | |||
| b595d3fcba | |||
| d0867d1c3b | |||
| 0d19e9210c | |||
| 4249de41d4 | |||
| 15f02ba191 | |||
| a2e1199907 | |||
| c08e9c4172 | |||
| abfc58eb24 | |||
| 035c7affff |
@@ -5,9 +5,11 @@
|
|||||||
.env.test.local
|
.env.test.local
|
||||||
.envrc
|
.envrc
|
||||||
.eslintcache
|
.eslintcache
|
||||||
|
.history
|
||||||
.idea
|
.idea
|
||||||
.vercel
|
.vercel
|
||||||
.vscode
|
.vscode
|
||||||
|
.yarn
|
||||||
*.log
|
*.log
|
||||||
*.tgz
|
*.tgz
|
||||||
build
|
build
|
||||||
|
|||||||
+1
-1
@@ -10,7 +10,7 @@ ARG NODE_ENV=production
|
|||||||
COPY . .
|
COPY . .
|
||||||
RUN yarn build:app:docker
|
RUN yarn build:app:docker
|
||||||
|
|
||||||
FROM nginx:1.17-alpine
|
FROM nginx:1.21-alpine
|
||||||
|
|
||||||
COPY --from=build /opt/node_app/build /usr/share/nginx/html
|
COPY --from=build /opt/node_app/build /usr/share/nginx/html
|
||||||
|
|
||||||
|
|||||||
@@ -70,6 +70,8 @@ The first set of digits is the room. This is visible from the server that’s go
|
|||||||
|
|
||||||
The second set of digits is the encryption key. The Excalidraw server doesn’t know about it. This is what all the participants use to encrypt/decrypt the messages.
|
The second set of digits is the encryption key. The Excalidraw server doesn’t know about it. This is what all the participants use to encrypt/decrypt the messages.
|
||||||
|
|
||||||
|
> Note: Please ensure that the encryption key is 22 characters long.
|
||||||
|
|
||||||
## Shape libraries
|
## Shape libraries
|
||||||
|
|
||||||
Find a growing list of libraries containing assets for your drawings at [libraries.excalidraw.com](https://libraries.excalidraw.com).
|
Find a growing list of libraries containing assets for your drawings at [libraries.excalidraw.com](https://libraries.excalidraw.com).
|
||||||
@@ -93,7 +95,7 @@ These instructions will get you a copy of the project up and running on your loc
|
|||||||
#### Requirements
|
#### Requirements
|
||||||
|
|
||||||
- [Node.js](https://nodejs.org/en/)
|
- [Node.js](https://nodejs.org/en/)
|
||||||
- [Yarn](https://yarnpkg.com/getting-started/install)
|
- [Yarn](https://yarnpkg.com/getting-started/install) (v1 or v2.4.2+)
|
||||||
- [Git](https://git-scm.com/downloads)
|
- [Git](https://git-scm.com/downloads)
|
||||||
|
|
||||||
#### Clone the repo
|
#### Clone the repo
|
||||||
|
|||||||
@@ -2,5 +2,8 @@
|
|||||||
"firestore": {
|
"firestore": {
|
||||||
"rules": "firestore.rules",
|
"rules": "firestore.rules",
|
||||||
"indexes": "firestore.indexes.json"
|
"indexes": "firestore.indexes.json"
|
||||||
|
},
|
||||||
|
"storage": {
|
||||||
|
"rules": "storage.rules"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
rules_version = '2';
|
||||||
|
service firebase.storage {
|
||||||
|
match /b/{bucket}/o {
|
||||||
|
match /{migrations} {
|
||||||
|
match /{scenes}/{scene} {
|
||||||
|
allow get, write: if true;
|
||||||
|
// redundant, but let's be explicit'
|
||||||
|
allow list: if false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+5
-3
@@ -19,15 +19,17 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@dwelle/browser-fs-access": "0.21.1",
|
||||||
|
"@excalidraw/random-username": "1.0.0",
|
||||||
"@sentry/browser": "6.2.5",
|
"@sentry/browser": "6.2.5",
|
||||||
"@sentry/integrations": "6.2.5",
|
"@sentry/integrations": "6.2.5",
|
||||||
"@testing-library/jest-dom": "5.11.10",
|
"@testing-library/jest-dom": "5.11.10",
|
||||||
"@testing-library/react": "11.2.6",
|
"@testing-library/react": "11.2.6",
|
||||||
|
"@tldraw/vec": "0.0.106",
|
||||||
"@types/jest": "26.0.22",
|
"@types/jest": "26.0.22",
|
||||||
"@types/react": "17.0.3",
|
"@types/react": "17.0.3",
|
||||||
"@types/react-dom": "17.0.3",
|
"@types/react-dom": "17.0.3",
|
||||||
"@types/socket.io-client": "1.4.36",
|
"@types/socket.io-client": "1.4.36",
|
||||||
"browser-fs-access": "0.16.4",
|
|
||||||
"clsx": "1.1.1",
|
"clsx": "1.1.1",
|
||||||
"firebase": "8.3.3",
|
"firebase": "8.3.3",
|
||||||
"i18next-browser-languagedetector": "6.1.0",
|
"i18next-browser-languagedetector": "6.1.0",
|
||||||
@@ -35,7 +37,7 @@
|
|||||||
"nanoid": "3.1.22",
|
"nanoid": "3.1.22",
|
||||||
"open-color": "1.8.0",
|
"open-color": "1.8.0",
|
||||||
"pako": "1.0.11",
|
"pako": "1.0.11",
|
||||||
"perfect-freehand": "0.4.7",
|
"perfect-freehand": "1.0.15",
|
||||||
"png-chunk-text": "1.0.0",
|
"png-chunk-text": "1.0.0",
|
||||||
"png-chunks-encode": "1.0.0",
|
"png-chunks-encode": "1.0.0",
|
||||||
"png-chunks-extract": "1.0.0",
|
"png-chunks-extract": "1.0.0",
|
||||||
@@ -76,7 +78,7 @@
|
|||||||
},
|
},
|
||||||
"jest": {
|
"jest": {
|
||||||
"transformIgnorePatterns": [
|
"transformIgnorePatterns": [
|
||||||
"node_modules/(?!(roughjs|points-on-curve|path-data-parser|points-on-path|browser-fs-access)/)"
|
"node_modules/(?!(roughjs|points-on-curve|path-data-parser|points-on-path|@dwelle/browser-fs-access)/)"
|
||||||
],
|
],
|
||||||
"resetMocks": false
|
"resetMocks": false
|
||||||
},
|
},
|
||||||
|
|||||||
Binary file not shown.
@@ -13,6 +13,18 @@
|
|||||||
|
|
||||||
<meta name="theme-color" content="#000" />
|
<meta name="theme-color" content="#000" />
|
||||||
|
|
||||||
|
<!-- Declarative Link Capturing (https://web.dev/declarative-link-capturing/) -->
|
||||||
|
<meta
|
||||||
|
http-equiv="origin-trial"
|
||||||
|
content="Ak3VyzTheARtX2CnxBZ3ZKxImB0mNyvDakmMxeAChgxrWFMZ3IGN64VP+uj36VxM5OegsbLmrP258b1xvqp7+Q8AAABbeyJvcmlnaW4iOiJodHRwczovL2V4Y2FsaWRyYXcuY29tOjQ0MyIsImZlYXR1cmUiOiJXZWJBcHBMaW5rQ2FwdHVyaW5nIiwiZXhwaXJ5IjoxNjM0MDgzMTk5fQ=="
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- File Handling (https://web.dev/file-handling/) -->
|
||||||
|
<meta
|
||||||
|
http-equiv="origin-trial"
|
||||||
|
content="AkMQsAnFmKfRfPKQHNCv2WmZREqgwkqhyt2M7aOwQiCStB+hPYnGnv+mNbkPDAsGXrwsj/waFi76wPzTDUaEeQ0AAABUeyJvcmlnaW4iOiJodHRwczovL2V4Y2FsaWRyYXcuY29tOjQ0MyIsImZlYXR1cmUiOiJGaWxlSGFuZGxpbmciLCJleHBpcnkiOjE2MzQwODMxOTl9"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- General tags -->
|
<!-- General tags -->
|
||||||
<meta
|
<meta
|
||||||
name="description"
|
name="description"
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"capture_links": "new_client",
|
"capture_links": "new-client",
|
||||||
"share_target": {
|
"share_target": {
|
||||||
"action": "/web-share-target",
|
"action": "/web-share-target",
|
||||||
"method": "POST",
|
"method": "POST",
|
||||||
|
|||||||
@@ -38,7 +38,8 @@ const crowdinMap = {
|
|||||||
"zh-CN": "en-zhcn",
|
"zh-CN": "en-zhcn",
|
||||||
"zh-TW": "en-zhtw",
|
"zh-TW": "en-zhtw",
|
||||||
"lv-LV": "en-lv",
|
"lv-LV": "en-lv",
|
||||||
"cs-CZ": "cs-cz",
|
"cs-CZ": "en-cs",
|
||||||
|
"kk-KZ": "en-kk",
|
||||||
};
|
};
|
||||||
|
|
||||||
const flags = {
|
const flags = {
|
||||||
@@ -78,6 +79,7 @@ const flags = {
|
|||||||
"zh-TW": "🇹🇼",
|
"zh-TW": "🇹🇼",
|
||||||
"lv-LV": "🇱🇻",
|
"lv-LV": "🇱🇻",
|
||||||
"cs-CZ": "🇨🇿",
|
"cs-CZ": "🇨🇿",
|
||||||
|
"kk-KZ": "🇰🇿",
|
||||||
};
|
};
|
||||||
|
|
||||||
const languages = {
|
const languages = {
|
||||||
@@ -117,6 +119,7 @@ const languages = {
|
|||||||
"zh-TW": "繁體中文",
|
"zh-TW": "繁體中文",
|
||||||
"lv-LV": "Latviešu",
|
"lv-LV": "Latviešu",
|
||||||
"cs-CZ": "Česky",
|
"cs-CZ": "Česky",
|
||||||
|
"kk-KZ": "Қазақ тілі",
|
||||||
};
|
};
|
||||||
|
|
||||||
const percentages = fs.readFileSync(
|
const percentages = fs.readFileSync(
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
const fs = require("fs");
|
||||||
|
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 updatePackageVersion = (nextVersion) => {
|
||||||
|
const pkg = require(excalidrawPackage);
|
||||||
|
pkg.version = nextVersion;
|
||||||
|
const content = `${JSON.stringify(pkg, null, 2)}\n`;
|
||||||
|
fs.writeFileSync(excalidrawPackage, content, "utf-8");
|
||||||
|
};
|
||||||
|
|
||||||
|
const release = async (nextVersion) => {
|
||||||
|
try {
|
||||||
|
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 (e) {
|
||||||
|
console.error(e);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const nextVersion = process.argv.slice(2)[0];
|
||||||
|
if (!nextVersion) {
|
||||||
|
console.error("Pass the next version to release!");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
release(nextVersion);
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
const fs = require("fs");
|
||||||
|
const util = require("util");
|
||||||
|
const exec = util.promisify(require("child_process").exec);
|
||||||
|
|
||||||
|
const excalidrawDir = `${__dirname}/../src/packages/excalidraw`;
|
||||||
|
const excalidrawPackage = `${excalidrawDir}/package.json`;
|
||||||
|
const pkg = require(excalidrawPackage);
|
||||||
|
const lastVersion = pkg.version;
|
||||||
|
const existingChangeLog = fs.readFileSync(
|
||||||
|
`${excalidrawDir}/CHANGELOG.md`,
|
||||||
|
"utf8",
|
||||||
|
);
|
||||||
|
|
||||||
|
const supportedTypes = ["feat", "fix", "style", "refactor", "perf", "build"];
|
||||||
|
const headerForType = {
|
||||||
|
feat: "Features",
|
||||||
|
fix: "Fixes",
|
||||||
|
style: "Styles",
|
||||||
|
refactor: " Refactor",
|
||||||
|
perf: "Performance",
|
||||||
|
build: "Build",
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCommitHashForLastVersion = async () => {
|
||||||
|
try {
|
||||||
|
const commitMessage = `"release @excalidraw/excalidraw@${lastVersion}"`;
|
||||||
|
const { stdout } = await exec(
|
||||||
|
`git log --format=format:"%H" --grep=${commitMessage}`,
|
||||||
|
);
|
||||||
|
return stdout;
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getLibraryCommitsSinceLastRelease = async () => {
|
||||||
|
const commitHash = await getCommitHashForLastVersion();
|
||||||
|
const { stdout } = await exec(
|
||||||
|
`git log --pretty=format:%s ${commitHash}...master`,
|
||||||
|
);
|
||||||
|
const commitsSinceLastRelease = stdout.split("\n");
|
||||||
|
const commitList = {};
|
||||||
|
supportedTypes.forEach((type) => {
|
||||||
|
commitList[type] = [];
|
||||||
|
});
|
||||||
|
|
||||||
|
commitsSinceLastRelease.forEach((commit) => {
|
||||||
|
const indexOfColon = commit.indexOf(":");
|
||||||
|
const type = commit.slice(0, indexOfColon);
|
||||||
|
if (!supportedTypes.includes(type)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const messageWithoutType = commit.slice(indexOfColon + 1).trim();
|
||||||
|
const messageWithCapitalizeFirst =
|
||||||
|
messageWithoutType.charAt(0).toUpperCase() + messageWithoutType.slice(1);
|
||||||
|
const prNumber = commit.match(/\(#([0-9]*)\)/)[1];
|
||||||
|
|
||||||
|
// return if the changelog already contains the pr number which would happen for package updates
|
||||||
|
if (existingChangeLog.includes(prNumber)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const prMarkdown = `[#${prNumber}](https://github.com/excalidraw/excalidraw/pull/${prNumber})`;
|
||||||
|
const messageWithPRLink = messageWithCapitalizeFirst.replace(
|
||||||
|
/\(#[0-9]*\)/,
|
||||||
|
prMarkdown,
|
||||||
|
);
|
||||||
|
commitList[type].push(messageWithPRLink);
|
||||||
|
});
|
||||||
|
return commitList;
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateChangelog = async (nextVersion) => {
|
||||||
|
const commitList = await getLibraryCommitsSinceLastRelease();
|
||||||
|
let changelogForLibrary =
|
||||||
|
"## Excalidraw Library\n\n**_This section lists the updates made to the excalidraw library and will not affect the integration._**\n\n";
|
||||||
|
supportedTypes.forEach((type) => {
|
||||||
|
if (commitList[type].length) {
|
||||||
|
changelogForLibrary += `### ${headerForType[type]}\n\n`;
|
||||||
|
const commits = commitList[type];
|
||||||
|
commits.forEach((commit) => {
|
||||||
|
changelogForLibrary += `- ${commit}\n\n`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
changelogForLibrary += "---\n";
|
||||||
|
const lastVersionIndex = existingChangeLog.indexOf(`## ${lastVersion}`);
|
||||||
|
let updatedContent =
|
||||||
|
existingChangeLog.slice(0, lastVersionIndex) +
|
||||||
|
changelogForLibrary +
|
||||||
|
existingChangeLog.slice(lastVersionIndex);
|
||||||
|
const currentDate = new Date().toISOString().slice(0, 10);
|
||||||
|
const newVersion = `## ${nextVersion} (${currentDate})`;
|
||||||
|
updatedContent = updatedContent.replace(`## Unreleased`, newVersion);
|
||||||
|
fs.writeFileSync(`${excalidrawDir}/CHANGELOG.md`, updatedContent, "utf8");
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = updateChangelog;
|
||||||
@@ -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;
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import React from "react";
|
|
||||||
import { alignElements, Alignment } from "../align";
|
import { alignElements, Alignment } from "../align";
|
||||||
import {
|
import {
|
||||||
AlignBottomIcon,
|
AlignBottomIcon,
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import React from "react";
|
|
||||||
import { getDefaultAppState } from "../appState";
|
import { getDefaultAppState } from "../appState";
|
||||||
import { ColorPicker } from "../components/ColorPicker";
|
import { ColorPicker } from "../components/ColorPicker";
|
||||||
import { resetZoom, trash, zoomIn, zoomOut } from "../components/icons";
|
import { trash, zoomIn, zoomOut } from "../components/icons";
|
||||||
import { ToolButton } from "../components/ToolButton";
|
import { ToolButton } from "../components/ToolButton";
|
||||||
import { DarkModeToggle } from "../components/DarkModeToggle";
|
import { DarkModeToggle } from "../components/DarkModeToggle";
|
||||||
import { ZOOM_STEP } from "../constants";
|
import { THEME, ZOOM_STEP } from "../constants";
|
||||||
import { getCommonBounds, getNonDeletedElements } from "../element";
|
import { getCommonBounds, getNonDeletedElements } from "../element";
|
||||||
import { newElementWith } from "../element/mutateElement";
|
import { newElementWith } from "../element/mutateElement";
|
||||||
import { ExcalidrawElement } from "../element/types";
|
import { ExcalidrawElement } from "../element/types";
|
||||||
@@ -17,6 +16,7 @@ import { getNewZoom } from "../scene/zoom";
|
|||||||
import { AppState, NormalizedZoomValue } from "../types";
|
import { AppState, NormalizedZoomValue } from "../types";
|
||||||
import { getShortcutKey } from "../utils";
|
import { getShortcutKey } from "../utils";
|
||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
|
import { Tooltip } from "../components/Tooltip";
|
||||||
|
|
||||||
export const actionChangeViewBackgroundColor = register({
|
export const actionChangeViewBackgroundColor = register({
|
||||||
name: "changeViewBackgroundColor",
|
name: "changeViewBackgroundColor",
|
||||||
@@ -34,9 +34,9 @@ export const actionChangeViewBackgroundColor = register({
|
|||||||
type="canvasBackground"
|
type="canvasBackground"
|
||||||
color={appState.viewBackgroundColor}
|
color={appState.viewBackgroundColor}
|
||||||
onChange={(color) => updateData({ viewBackgroundColor: color })}
|
onChange={(color) => updateData({ viewBackgroundColor: color })}
|
||||||
isActive={appState.openMenu === "canvasColorPicker"}
|
isActive={appState.openPopup === "canvasColorPicker"}
|
||||||
setActive={(active) =>
|
setActive={(active) =>
|
||||||
updateData({ openMenu: active ? "canvasColorPicker" : null })
|
updateData({ openPopup: active ? "canvasColorPicker" : null })
|
||||||
}
|
}
|
||||||
data-testid="canvas-background-picker"
|
data-testid="canvas-background-picker"
|
||||||
/>
|
/>
|
||||||
@@ -108,6 +108,7 @@ export const actionZoomIn = register({
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
updateData(null);
|
updateData(null);
|
||||||
}}
|
}}
|
||||||
|
size="small"
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
keyTest: (event) =>
|
keyTest: (event) =>
|
||||||
@@ -142,6 +143,7 @@ export const actionZoomOut = register({
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
updateData(null);
|
updateData(null);
|
||||||
}}
|
}}
|
||||||
|
size="small"
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
keyTest: (event) =>
|
keyTest: (event) =>
|
||||||
@@ -168,16 +170,21 @@ export const actionResetZoom = register({
|
|||||||
commitToHistory: false,
|
commitToHistory: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
PanelComponent: ({ updateData }) => (
|
PanelComponent: ({ updateData, appState }) => (
|
||||||
|
<Tooltip label={t("buttons.resetZoom")}>
|
||||||
<ToolButton
|
<ToolButton
|
||||||
type="button"
|
type="button"
|
||||||
icon={resetZoom}
|
className="reset-zoom-button"
|
||||||
title={t("buttons.resetZoom")}
|
title={t("buttons.resetZoom")}
|
||||||
aria-label={t("buttons.resetZoom")}
|
aria-label={t("buttons.resetZoom")}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
updateData(null);
|
updateData(null);
|
||||||
}}
|
}}
|
||||||
/>
|
size="small"
|
||||||
|
>
|
||||||
|
{(appState.zoom.value * 100).toFixed(0)}%
|
||||||
|
</ToolButton>
|
||||||
|
</Tooltip>
|
||||||
),
|
),
|
||||||
keyTest: (event) =>
|
keyTest: (event) =>
|
||||||
(event.code === CODES.ZERO || event.code === CODES.NUM_ZERO) &&
|
(event.code === CODES.ZERO || event.code === CODES.NUM_ZERO) &&
|
||||||
@@ -271,7 +278,8 @@ export const actionToggleTheme = register({
|
|||||||
return {
|
return {
|
||||||
appState: {
|
appState: {
|
||||||
...appState,
|
...appState,
|
||||||
theme: value || (appState.theme === "light" ? "dark" : "light"),
|
theme:
|
||||||
|
value || (appState.theme === THEME.LIGHT ? THEME.DARK : THEME.LIGHT),
|
||||||
},
|
},
|
||||||
commitToHistory: false,
|
commitToHistory: false,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { isSomeElementSelected } from "../scene";
|
import { isSomeElementSelected } from "../scene";
|
||||||
import { KEYS } from "../keys";
|
import { KEYS } from "../keys";
|
||||||
import { ToolButton } from "../components/ToolButton";
|
import { ToolButton } from "../components/ToolButton";
|
||||||
import React from "react";
|
|
||||||
import { trash } from "../components/icons";
|
import { trash } from "../components/icons";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import React from "react";
|
|
||||||
import {
|
import {
|
||||||
DistributeHorizontallyIcon,
|
DistributeHorizontallyIcon,
|
||||||
DistributeVerticallyIcon,
|
DistributeVerticallyIcon,
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import React from "react";
|
|
||||||
import { KEYS } from "../keys";
|
import { KEYS } from "../keys";
|
||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
import { ExcalidrawElement } from "../element/types";
|
import { ExcalidrawElement } from "../element/types";
|
||||||
|
|||||||
@@ -1,18 +1,25 @@
|
|||||||
import React from "react";
|
|
||||||
import { trackEvent } from "../analytics";
|
import { trackEvent } from "../analytics";
|
||||||
import { load, questionCircle, save, saveAs } from "../components/icons";
|
import { load, questionCircle, saveAs } from "../components/icons";
|
||||||
import { ProjectName } from "../components/ProjectName";
|
import { ProjectName } from "../components/ProjectName";
|
||||||
import { ToolButton } from "../components/ToolButton";
|
import { ToolButton } from "../components/ToolButton";
|
||||||
import "../components/ToolIcon.scss";
|
import "../components/ToolIcon.scss";
|
||||||
import { Tooltip } from "../components/Tooltip";
|
import { Tooltip } from "../components/Tooltip";
|
||||||
import { DarkModeToggle, Appearence } from "../components/DarkModeToggle";
|
import { DarkModeToggle } from "../components/DarkModeToggle";
|
||||||
import { loadFromJSON, saveAsJSON } from "../data";
|
import { loadFromJSON, saveAsJSON } from "../data";
|
||||||
|
import { resaveAsImageWithScene } from "../data/resave";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { useIsMobile } from "../components/App";
|
import { useIsMobile } from "../components/App";
|
||||||
import { KEYS } from "../keys";
|
import { KEYS } from "../keys";
|
||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
import { supported as fsSupported } from "browser-fs-access";
|
|
||||||
import { CheckboxItem } from "../components/CheckboxItem";
|
import { CheckboxItem } from "../components/CheckboxItem";
|
||||||
|
import { getExportSize } from "../scene/export";
|
||||||
|
import { DEFAULT_EXPORT_PADDING, EXPORT_SCALES, THEME } from "../constants";
|
||||||
|
import { getSelectedElements, isSomeElementSelected } from "../scene";
|
||||||
|
import { getNonDeletedElements } from "../element";
|
||||||
|
import { ActiveFile } from "../components/ActiveFile";
|
||||||
|
import { isImageFileHandle } from "../data/blob";
|
||||||
|
import { nativeFileSystemSupported } from "../data/filesystem";
|
||||||
|
import { Theme } from "../element/types";
|
||||||
|
|
||||||
export const actionChangeProjectName = register({
|
export const actionChangeProjectName = register({
|
||||||
name: "changeProjectName",
|
name: "changeProjectName",
|
||||||
@@ -32,6 +39,54 @@ export const actionChangeProjectName = register({
|
|||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const actionChangeExportScale = register({
|
||||||
|
name: "changeExportScale",
|
||||||
|
perform: (_elements, appState, value) => {
|
||||||
|
return {
|
||||||
|
appState: { ...appState, exportScale: value },
|
||||||
|
commitToHistory: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
PanelComponent: ({ elements: allElements, appState, updateData }) => {
|
||||||
|
const elements = getNonDeletedElements(allElements);
|
||||||
|
const exportSelected = isSomeElementSelected(elements, appState);
|
||||||
|
const exportedElements = exportSelected
|
||||||
|
? getSelectedElements(elements, appState)
|
||||||
|
: elements;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{EXPORT_SCALES.map((s) => {
|
||||||
|
const [width, height] = getExportSize(
|
||||||
|
exportedElements,
|
||||||
|
DEFAULT_EXPORT_PADDING,
|
||||||
|
s,
|
||||||
|
);
|
||||||
|
|
||||||
|
const scaleButtonTitle = `${t(
|
||||||
|
"buttons.scale",
|
||||||
|
)} ${s}x (${width}x${height})`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ToolButton
|
||||||
|
key={s}
|
||||||
|
size="small"
|
||||||
|
type="radio"
|
||||||
|
icon={`${s}x`}
|
||||||
|
name="export-canvas-scale"
|
||||||
|
title={scaleButtonTitle}
|
||||||
|
aria-label={scaleButtonTitle}
|
||||||
|
id="export-canvas-scale"
|
||||||
|
checked={s === appState.exportScale}
|
||||||
|
onChange={() => updateData(s)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
export const actionChangeExportBackground = register({
|
export const actionChangeExportBackground = register({
|
||||||
name: "changeExportBackground",
|
name: "changeExportBackground",
|
||||||
perform: (_elements, appState, value) => {
|
perform: (_elements, appState, value) => {
|
||||||
@@ -65,7 +120,7 @@ export const actionChangeExportEmbedScene = register({
|
|||||||
>
|
>
|
||||||
{t("labels.exportEmbedScene")}
|
{t("labels.exportEmbedScene")}
|
||||||
<Tooltip label={t("labels.exportEmbedScene_details")} long={true}>
|
<Tooltip label={t("labels.exportEmbedScene_details")} long={true}>
|
||||||
<div className="Tooltip-icon">{questionCircle}</div>
|
<div className="excalidraw-tooltip-icon">{questionCircle}</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</CheckboxItem>
|
</CheckboxItem>
|
||||||
),
|
),
|
||||||
@@ -75,15 +130,19 @@ export const actionSaveToActiveFile = register({
|
|||||||
name: "saveToActiveFile",
|
name: "saveToActiveFile",
|
||||||
perform: async (elements, appState, value) => {
|
perform: async (elements, appState, value) => {
|
||||||
const fileHandleExists = !!appState.fileHandle;
|
const fileHandleExists = !!appState.fileHandle;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { fileHandle } = await saveAsJSON(elements, appState);
|
const { fileHandle } = isImageFileHandle(appState.fileHandle)
|
||||||
|
? await resaveAsImageWithScene(elements, appState)
|
||||||
|
: await saveAsJSON(elements, appState);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
commitToHistory: false,
|
commitToHistory: false,
|
||||||
appState: {
|
appState: {
|
||||||
...appState,
|
...appState,
|
||||||
fileHandle,
|
fileHandle,
|
||||||
toastMessage: fileHandleExists
|
toastMessage: fileHandleExists
|
||||||
? fileHandle.name
|
? fileHandle?.name
|
||||||
? t("toast.fileSavedToFilename").replace(
|
? t("toast.fileSavedToFilename").replace(
|
||||||
"{filename}",
|
"{filename}",
|
||||||
`"${fileHandle.name}"`,
|
`"${fileHandle.name}"`,
|
||||||
@@ -101,14 +160,10 @@ export const actionSaveToActiveFile = register({
|
|||||||
},
|
},
|
||||||
keyTest: (event) =>
|
keyTest: (event) =>
|
||||||
event.key === KEYS.S && event[KEYS.CTRL_OR_CMD] && !event.shiftKey,
|
event.key === KEYS.S && event[KEYS.CTRL_OR_CMD] && !event.shiftKey,
|
||||||
PanelComponent: ({ updateData }) => (
|
PanelComponent: ({ updateData, appState }) => (
|
||||||
<ToolButton
|
<ActiveFile
|
||||||
type="icon"
|
onSave={() => updateData(null)}
|
||||||
icon={save}
|
fileName={appState.fileHandle?.name}
|
||||||
title={t("buttons.save")}
|
|
||||||
aria-label={t("buttons.save")}
|
|
||||||
onClick={() => updateData(null)}
|
|
||||||
data-testid="save-button"
|
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
@@ -138,7 +193,7 @@ export const actionSaveFileToDisk = register({
|
|||||||
title={t("buttons.saveAs")}
|
title={t("buttons.saveAs")}
|
||||||
aria-label={t("buttons.saveAs")}
|
aria-label={t("buttons.saveAs")}
|
||||||
showAriaLabel={useIsMobile()}
|
showAriaLabel={useIsMobile()}
|
||||||
hidden={!fsSupported}
|
hidden={!nativeFileSystemSupported}
|
||||||
onClick={() => updateData(null)}
|
onClick={() => updateData(null)}
|
||||||
data-testid="save-as-button"
|
data-testid="save-as-button"
|
||||||
/>
|
/>
|
||||||
@@ -152,7 +207,7 @@ export const actionLoadScene = register({
|
|||||||
const {
|
const {
|
||||||
elements: loadedElements,
|
elements: loadedElements,
|
||||||
appState: loadedAppState,
|
appState: loadedAppState,
|
||||||
} = await loadFromJSON(appState);
|
} = await loadFromJSON(appState, elements);
|
||||||
return {
|
return {
|
||||||
elements: loadedElements,
|
elements: loadedElements,
|
||||||
appState: loadedAppState,
|
appState: loadedAppState,
|
||||||
@@ -201,9 +256,9 @@ export const actionExportWithDarkMode = register({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DarkModeToggle
|
<DarkModeToggle
|
||||||
value={appState.exportWithDarkMode ? "dark" : "light"}
|
value={appState.exportWithDarkMode ? THEME.DARK : THEME.LIGHT}
|
||||||
onChange={(theme: Appearence) => {
|
onChange={(theme: Theme) => {
|
||||||
updateData(theme === "dark");
|
updateData(theme === THEME.DARK);
|
||||||
}}
|
}}
|
||||||
title={t("labels.toggleExportColorScheme")}
|
title={t("labels.toggleExportColorScheme")}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { KEYS } from "../keys";
|
import { KEYS } from "../keys";
|
||||||
import { isInvisiblySmallElement } from "../element";
|
import { isInvisiblySmallElement } from "../element";
|
||||||
import { resetCursor } from "../utils";
|
import { resetCursor } from "../utils";
|
||||||
import React from "react";
|
|
||||||
import { ToolButton } from "../components/ToolButton";
|
import { ToolButton } from "../components/ToolButton";
|
||||||
import { done } from "../components/icons";
|
import { done } from "../components/icons";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import React from "react";
|
|
||||||
import { CODES, KEYS } from "../keys";
|
import { CODES, KEYS } from "../keys";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { getShortcutKey } from "../utils";
|
import { getShortcutKey } from "../utils";
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { Action, ActionResult } from "./types";
|
import { Action, ActionResult } from "./types";
|
||||||
import React from "react";
|
|
||||||
import { undo, redo } from "../components/icons";
|
import { undo, redo } from "../components/icons";
|
||||||
import { ToolButton } from "../components/ToolButton";
|
import { ToolButton } from "../components/ToolButton";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
@@ -69,12 +68,13 @@ export const createUndoAction: ActionCreator = (history) => ({
|
|||||||
event[KEYS.CTRL_OR_CMD] &&
|
event[KEYS.CTRL_OR_CMD] &&
|
||||||
event.key.toLowerCase() === KEYS.Z &&
|
event.key.toLowerCase() === KEYS.Z &&
|
||||||
!event.shiftKey,
|
!event.shiftKey,
|
||||||
PanelComponent: ({ updateData }) => (
|
PanelComponent: ({ updateData, data }) => (
|
||||||
<ToolButton
|
<ToolButton
|
||||||
type="button"
|
type="button"
|
||||||
icon={undo}
|
icon={undo}
|
||||||
aria-label={t("buttons.undo")}
|
aria-label={t("buttons.undo")}
|
||||||
onClick={updateData}
|
onClick={updateData}
|
||||||
|
size={data?.size || "medium"}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
commitToHistory: () => false,
|
commitToHistory: () => false,
|
||||||
@@ -89,12 +89,13 @@ export const createRedoAction: ActionCreator = (history) => ({
|
|||||||
event.shiftKey &&
|
event.shiftKey &&
|
||||||
event.key.toLowerCase() === KEYS.Z) ||
|
event.key.toLowerCase() === KEYS.Z) ||
|
||||||
(isWindows && event.ctrlKey && !event.shiftKey && event.key === KEYS.Y),
|
(isWindows && event.ctrlKey && !event.shiftKey && event.key === KEYS.Y),
|
||||||
PanelComponent: ({ updateData }) => (
|
PanelComponent: ({ updateData, data }) => (
|
||||||
<ToolButton
|
<ToolButton
|
||||||
type="button"
|
type="button"
|
||||||
icon={redo}
|
icon={redo}
|
||||||
aria-label={t("buttons.redo")}
|
aria-label={t("buttons.redo")}
|
||||||
onClick={updateData}
|
onClick={updateData}
|
||||||
|
size={data?.size || "medium"}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
commitToHistory: () => false,
|
commitToHistory: () => false,
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import React from "react";
|
|
||||||
import { menu, palette } from "../components/icons";
|
import { menu, palette } from "../components/icons";
|
||||||
import { ToolButton } from "../components/ToolButton";
|
import { ToolButton } from "../components/ToolButton";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import React from "react";
|
|
||||||
import { getClientColors, getClientInitials } from "../clients";
|
import { getClientColors, getClientInitials } from "../clients";
|
||||||
import { Avatar } from "../components/Avatar";
|
import { Avatar } from "../components/Avatar";
|
||||||
import { centerScrollOn } from "../scene/scroll";
|
import { centerScrollOn } from "../scene/scroll";
|
||||||
@@ -30,8 +29,8 @@ export const actionGoToCollaborator = register({
|
|||||||
commitToHistory: false,
|
commitToHistory: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
PanelComponent: ({ appState, updateData, id }) => {
|
PanelComponent: ({ appState, updateData, data }) => {
|
||||||
const clientId = id;
|
const clientId: string | undefined = data?.id;
|
||||||
if (!clientId) {
|
if (!clientId) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import React from "react";
|
|
||||||
import { AppState } from "../../src/types";
|
import { AppState } from "../../src/types";
|
||||||
import { ButtonIconSelect } from "../components/ButtonIconSelect";
|
import { ButtonIconSelect } from "../components/ButtonIconSelect";
|
||||||
import { ColorPicker } from "../components/ColorPicker";
|
import { ColorPicker } from "../components/ColorPicker";
|
||||||
@@ -13,6 +12,13 @@ import {
|
|||||||
FillCrossHatchIcon,
|
FillCrossHatchIcon,
|
||||||
FillHachureIcon,
|
FillHachureIcon,
|
||||||
FillSolidIcon,
|
FillSolidIcon,
|
||||||
|
FontFamilyCodeIcon,
|
||||||
|
FontFamilyHandDrawnIcon,
|
||||||
|
FontFamilyNormalIcon,
|
||||||
|
FontSizeExtraLargeIcon,
|
||||||
|
FontSizeLargeIcon,
|
||||||
|
FontSizeMediumIcon,
|
||||||
|
FontSizeSmallIcon,
|
||||||
SloppinessArchitectIcon,
|
SloppinessArchitectIcon,
|
||||||
SloppinessArtistIcon,
|
SloppinessArtistIcon,
|
||||||
SloppinessCartoonistIcon,
|
SloppinessCartoonistIcon,
|
||||||
@@ -20,18 +26,15 @@ import {
|
|||||||
StrokeStyleDottedIcon,
|
StrokeStyleDottedIcon,
|
||||||
StrokeStyleSolidIcon,
|
StrokeStyleSolidIcon,
|
||||||
StrokeWidthIcon,
|
StrokeWidthIcon,
|
||||||
FontSizeSmallIcon,
|
|
||||||
FontSizeMediumIcon,
|
|
||||||
FontSizeLargeIcon,
|
|
||||||
FontSizeExtraLargeIcon,
|
|
||||||
FontFamilyHandDrawnIcon,
|
|
||||||
FontFamilyNormalIcon,
|
|
||||||
FontFamilyCodeIcon,
|
|
||||||
TextAlignLeftIcon,
|
|
||||||
TextAlignCenterIcon,
|
TextAlignCenterIcon,
|
||||||
|
TextAlignLeftIcon,
|
||||||
TextAlignRightIcon,
|
TextAlignRightIcon,
|
||||||
} from "../components/icons";
|
} from "../components/icons";
|
||||||
import { DEFAULT_FONT_FAMILY, DEFAULT_FONT_SIZE } from "../constants";
|
import {
|
||||||
|
DEFAULT_FONT_FAMILY,
|
||||||
|
DEFAULT_FONT_SIZE,
|
||||||
|
FONT_FAMILY,
|
||||||
|
} from "../constants";
|
||||||
import {
|
import {
|
||||||
getNonDeletedElements,
|
getNonDeletedElements,
|
||||||
isTextElement,
|
isTextElement,
|
||||||
@@ -44,7 +47,7 @@ import {
|
|||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
ExcalidrawLinearElement,
|
ExcalidrawLinearElement,
|
||||||
ExcalidrawTextElement,
|
ExcalidrawTextElement,
|
||||||
FontFamily,
|
FontFamilyValues,
|
||||||
TextAlign,
|
TextAlign,
|
||||||
} from "../element/types";
|
} from "../element/types";
|
||||||
import { getLanguage, t } from "../i18n";
|
import { getLanguage, t } from "../i18n";
|
||||||
@@ -126,9 +129,9 @@ export const actionChangeStrokeColor = register({
|
|||||||
appState.currentItemStrokeColor,
|
appState.currentItemStrokeColor,
|
||||||
)}
|
)}
|
||||||
onChange={(color) => updateData({ currentItemStrokeColor: color })}
|
onChange={(color) => updateData({ currentItemStrokeColor: color })}
|
||||||
isActive={appState.openMenu === "strokeColorPicker"}
|
isActive={appState.openPopup === "strokeColorPicker"}
|
||||||
setActive={(active) =>
|
setActive={(active) =>
|
||||||
updateData({ openMenu: active ? "strokeColorPicker" : null })
|
updateData({ openPopup: active ? "strokeColorPicker" : null })
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
@@ -166,9 +169,9 @@ export const actionChangeBackgroundColor = register({
|
|||||||
appState.currentItemBackgroundColor,
|
appState.currentItemBackgroundColor,
|
||||||
)}
|
)}
|
||||||
onChange={(color) => updateData({ currentItemBackgroundColor: color })}
|
onChange={(color) => updateData({ currentItemBackgroundColor: color })}
|
||||||
isActive={appState.openMenu === "backgroundColorPicker"}
|
isActive={appState.openPopup === "backgroundColorPicker"}
|
||||||
setActive={(active) =>
|
setActive={(active) =>
|
||||||
updateData({ openMenu: active ? "backgroundColorPicker" : null })
|
updateData({ openPopup: active ? "backgroundColorPicker" : null })
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
@@ -499,19 +502,23 @@ export const actionChangeFontFamily = register({
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
PanelComponent: ({ elements, appState, updateData }) => {
|
PanelComponent: ({ elements, appState, updateData }) => {
|
||||||
const options: { value: FontFamily; text: string; icon: JSX.Element }[] = [
|
const options: {
|
||||||
|
value: FontFamilyValues;
|
||||||
|
text: string;
|
||||||
|
icon: JSX.Element;
|
||||||
|
}[] = [
|
||||||
{
|
{
|
||||||
value: 1,
|
value: FONT_FAMILY.Virgil,
|
||||||
text: t("labels.handDrawn"),
|
text: t("labels.handDrawn"),
|
||||||
icon: <FontFamilyHandDrawnIcon theme={appState.theme} />,
|
icon: <FontFamilyHandDrawnIcon theme={appState.theme} />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 2,
|
value: FONT_FAMILY.Helvetica,
|
||||||
text: t("labels.normal"),
|
text: t("labels.normal"),
|
||||||
icon: <FontFamilyNormalIcon theme={appState.theme} />,
|
icon: <FontFamilyNormalIcon theme={appState.theme} />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 3,
|
value: FONT_FAMILY.Cascadia,
|
||||||
text: t("labels.code"),
|
text: t("labels.code"),
|
||||||
icon: <FontFamilyCodeIcon theme={appState.theme} />,
|
icon: <FontFamilyCodeIcon theme={appState.theme} />,
|
||||||
},
|
},
|
||||||
@@ -520,7 +527,7 @@ export const actionChangeFontFamily = register({
|
|||||||
return (
|
return (
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>{t("labels.fontFamily")}</legend>
|
<legend>{t("labels.fontFamily")}</legend>
|
||||||
<ButtonIconSelect<FontFamily | false>
|
<ButtonIconSelect<FontFamilyValues | false>
|
||||||
group="font-family"
|
group="font-family"
|
||||||
options={options}
|
options={options}
|
||||||
value={getFormValue(
|
value={getFormValue(
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ export const actionToggleViewMode = register({
|
|||||||
appState: {
|
appState: {
|
||||||
...appState,
|
...appState,
|
||||||
viewModeEnabled: !this.checked!(appState),
|
viewModeEnabled: !this.checked!(appState),
|
||||||
selectedElementIds: {},
|
|
||||||
},
|
},
|
||||||
commitToHistory: false,
|
commitToHistory: false,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
UpdaterFn,
|
UpdaterFn,
|
||||||
ActionName,
|
ActionName,
|
||||||
ActionResult,
|
ActionResult,
|
||||||
|
PanelComponentProps,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import { ExcalidrawElement } from "../element/types";
|
import { ExcalidrawElement } from "../element/types";
|
||||||
import { AppProps, AppState } from "../types";
|
import { AppProps, AppState } from "../types";
|
||||||
@@ -107,11 +108,10 @@ export class ActionManager implements ActionsManagerInterface {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Id is an attribute that we can use to pass in data like keys.
|
/**
|
||||||
// This is needed for dynamically generated action components
|
* @param data additional data sent to the PanelComponent
|
||||||
// like the user list. We can use this key to extract more
|
*/
|
||||||
// data from app state. This is an alternative to generic prop hell!
|
renderAction = (name: ActionName, data?: PanelComponentProps["data"]) => {
|
||||||
renderAction = (name: ActionName, id?: string) => {
|
|
||||||
const canvasActions = this.app.props.UIOptions.canvasActions;
|
const canvasActions = this.app.props.UIOptions.canvasActions;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -139,8 +139,8 @@ export class ActionManager implements ActionsManagerInterface {
|
|||||||
elements={this.getElementsIncludingDeleted()}
|
elements={this.getElementsIncludingDeleted()}
|
||||||
appState={this.getAppState()}
|
appState={this.getAppState()}
|
||||||
updateData={updateData}
|
updateData={updateData}
|
||||||
id={id}
|
|
||||||
appProps={this.app.props}
|
appProps={this.app.props}
|
||||||
|
data={data}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import React from "react";
|
|||||||
import { ExcalidrawElement } from "../element/types";
|
import { ExcalidrawElement } from "../element/types";
|
||||||
import { AppState, ExcalidrawProps } from "../types";
|
import { AppState, ExcalidrawProps } from "../types";
|
||||||
import Library from "../data/library";
|
import Library from "../data/library";
|
||||||
|
import { ToolButtonSize } from "../components/ToolButton";
|
||||||
|
|
||||||
/** if false, the action should be prevented */
|
/** if false, the action should be prevented */
|
||||||
export type ActionResult =
|
export type ActionResult =
|
||||||
@@ -66,6 +67,7 @@ export type ActionName =
|
|||||||
| "changeProjectName"
|
| "changeProjectName"
|
||||||
| "changeExportBackground"
|
| "changeExportBackground"
|
||||||
| "changeExportEmbedScene"
|
| "changeExportEmbedScene"
|
||||||
|
| "changeExportScale"
|
||||||
| "saveToActiveFile"
|
| "saveToActiveFile"
|
||||||
| "saveFileToDisk"
|
| "saveFileToDisk"
|
||||||
| "loadScene"
|
| "loadScene"
|
||||||
@@ -101,15 +103,17 @@ export type ActionName =
|
|||||||
| "exportWithDarkMode"
|
| "exportWithDarkMode"
|
||||||
| "toggleTheme";
|
| "toggleTheme";
|
||||||
|
|
||||||
export interface Action {
|
export type PanelComponentProps = {
|
||||||
name: ActionName;
|
|
||||||
PanelComponent?: React.FC<{
|
|
||||||
elements: readonly ExcalidrawElement[];
|
elements: readonly ExcalidrawElement[];
|
||||||
appState: AppState;
|
appState: AppState;
|
||||||
updateData: (formData?: any) => void;
|
updateData: (formData?: any) => void;
|
||||||
appProps: ExcalidrawProps;
|
appProps: ExcalidrawProps;
|
||||||
id?: string;
|
data?: Partial<{ id: string; size: ToolButtonSize }>;
|
||||||
}>;
|
};
|
||||||
|
|
||||||
|
export interface Action {
|
||||||
|
name: ActionName;
|
||||||
|
PanelComponent?: React.FC<PanelComponentProps>;
|
||||||
perform: ActionFn;
|
perform: ActionFn;
|
||||||
keyPriority?: number;
|
keyPriority?: number;
|
||||||
keyTest?: (
|
keyTest?: (
|
||||||
|
|||||||
+11
-1
@@ -3,17 +3,23 @@ import {
|
|||||||
DEFAULT_FONT_FAMILY,
|
DEFAULT_FONT_FAMILY,
|
||||||
DEFAULT_FONT_SIZE,
|
DEFAULT_FONT_SIZE,
|
||||||
DEFAULT_TEXT_ALIGN,
|
DEFAULT_TEXT_ALIGN,
|
||||||
|
EXPORT_SCALES,
|
||||||
|
THEME,
|
||||||
} from "./constants";
|
} from "./constants";
|
||||||
import { t } from "./i18n";
|
import { t } from "./i18n";
|
||||||
import { AppState, NormalizedZoomValue } from "./types";
|
import { AppState, NormalizedZoomValue } from "./types";
|
||||||
import { getDateTime } from "./utils";
|
import { getDateTime } from "./utils";
|
||||||
|
|
||||||
|
const defaultExportScale = EXPORT_SCALES.includes(devicePixelRatio)
|
||||||
|
? devicePixelRatio
|
||||||
|
: 1;
|
||||||
|
|
||||||
export const getDefaultAppState = (): Omit<
|
export const getDefaultAppState = (): Omit<
|
||||||
AppState,
|
AppState,
|
||||||
"offsetTop" | "offsetLeft" | "width" | "height"
|
"offsetTop" | "offsetLeft" | "width" | "height"
|
||||||
> => {
|
> => {
|
||||||
return {
|
return {
|
||||||
theme: "light",
|
theme: THEME.LIGHT,
|
||||||
collaborators: new Map(),
|
collaborators: new Map(),
|
||||||
currentChartType: "bar",
|
currentChartType: "bar",
|
||||||
currentItemBackgroundColor: "transparent",
|
currentItemBackgroundColor: "transparent",
|
||||||
@@ -39,6 +45,7 @@ export const getDefaultAppState = (): Omit<
|
|||||||
elementType: "selection",
|
elementType: "selection",
|
||||||
errorMessage: null,
|
errorMessage: null,
|
||||||
exportBackground: true,
|
exportBackground: true,
|
||||||
|
exportScale: defaultExportScale,
|
||||||
exportEmbedScene: false,
|
exportEmbedScene: false,
|
||||||
exportWithDarkMode: false,
|
exportWithDarkMode: false,
|
||||||
fileHandle: null,
|
fileHandle: null,
|
||||||
@@ -52,6 +59,7 @@ export const getDefaultAppState = (): Omit<
|
|||||||
multiElement: null,
|
multiElement: null,
|
||||||
name: `${t("labels.untitled")}-${getDateTime()}`,
|
name: `${t("labels.untitled")}-${getDateTime()}`,
|
||||||
openMenu: null,
|
openMenu: null,
|
||||||
|
openPopup: null,
|
||||||
pasteDialog: { shown: false, data: null },
|
pasteDialog: { shown: false, data: null },
|
||||||
previousSelectedElementIds: {},
|
previousSelectedElementIds: {},
|
||||||
resizingElement: null,
|
resizingElement: null,
|
||||||
@@ -116,6 +124,7 @@ const APP_STATE_STORAGE_CONF = (<
|
|||||||
errorMessage: { browser: false, export: false },
|
errorMessage: { browser: false, export: false },
|
||||||
exportBackground: { browser: true, export: false },
|
exportBackground: { browser: true, export: false },
|
||||||
exportEmbedScene: { browser: true, export: false },
|
exportEmbedScene: { browser: true, export: false },
|
||||||
|
exportScale: { browser: true, export: false },
|
||||||
exportWithDarkMode: { browser: true, export: false },
|
exportWithDarkMode: { browser: true, export: false },
|
||||||
fileHandle: { browser: false, export: false },
|
fileHandle: { browser: false, export: false },
|
||||||
gridSize: { browser: true, export: true },
|
gridSize: { browser: true, export: true },
|
||||||
@@ -131,6 +140,7 @@ const APP_STATE_STORAGE_CONF = (<
|
|||||||
offsetLeft: { browser: false, export: false },
|
offsetLeft: { browser: false, export: false },
|
||||||
offsetTop: { browser: false, export: false },
|
offsetTop: { browser: false, export: false },
|
||||||
openMenu: { browser: true, export: false },
|
openMenu: { browser: true, export: false },
|
||||||
|
openPopup: { browser: false, export: false },
|
||||||
pasteDialog: { browser: false, export: false },
|
pasteDialog: { browser: false, export: false },
|
||||||
previousSelectedElementIds: { browser: true, export: false },
|
previousSelectedElementIds: { browser: true, export: false },
|
||||||
resizingElement: { browser: false, export: false },
|
resizingElement: { browser: false, export: false },
|
||||||
|
|||||||
@@ -151,23 +151,14 @@ export const SelectedShapeActions = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const LIBRARY_ICON = (
|
|
||||||
// fa-th-large
|
|
||||||
<svg viewBox="0 0 512 512">
|
|
||||||
<path d="M296 32h192c13.255 0 24 10.745 24 24v160c0 13.255-10.745 24-24 24H296c-13.255 0-24-10.745-24-24V56c0-13.255 10.745-24 24-24zm-80 0H24C10.745 32 0 42.745 0 56v160c0 13.255 10.745 24 24 24h192c13.255 0 24-10.745 24-24V56c0-13.255-10.745-24-24-24zM0 296v160c0 13.255 10.745 24 24 24h192c13.255 0 24-10.745 24-24V296c0-13.255-10.745-24-24-24H24c-13.255 0-24 10.745-24 24zm296 184h192c13.255 0 24-10.745 24-24V296c0-13.255-10.745-24-24-24H296c-13.255 0-24 10.745-24 24v160c0 13.255 10.745 24 24 24z" />
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
|
|
||||||
export const ShapesSwitcher = ({
|
export const ShapesSwitcher = ({
|
||||||
canvas,
|
canvas,
|
||||||
elementType,
|
elementType,
|
||||||
setAppState,
|
setAppState,
|
||||||
isLibraryOpen,
|
|
||||||
}: {
|
}: {
|
||||||
canvas: HTMLCanvasElement | null;
|
canvas: HTMLCanvasElement | null;
|
||||||
elementType: ExcalidrawElement["type"];
|
elementType: ExcalidrawElement["type"];
|
||||||
setAppState: React.Component<any, AppState>["setState"];
|
setAppState: React.Component<any, AppState>["setState"];
|
||||||
isLibraryOpen: boolean;
|
|
||||||
}) => (
|
}) => (
|
||||||
<>
|
<>
|
||||||
{SHAPES.map(({ value, icon, key }, index) => {
|
{SHAPES.map(({ value, icon, key }, index) => {
|
||||||
@@ -201,19 +192,6 @@ export const ShapesSwitcher = ({
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
<ToolButton
|
|
||||||
className="Shape ToolIcon_type_button__library"
|
|
||||||
type="button"
|
|
||||||
icon={LIBRARY_ICON}
|
|
||||||
name="editor-library"
|
|
||||||
keyBindingLabel="9"
|
|
||||||
aria-keyshortcuts="9"
|
|
||||||
title={`${capitalizeString(t("toolBar.library"))} — 9`}
|
|
||||||
aria-label={capitalizeString(t("toolBar.library"))}
|
|
||||||
onClick={() => {
|
|
||||||
setAppState({ isLibraryOpen: !isLibraryOpen });
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -226,12 +204,9 @@ export const ZoomActions = ({
|
|||||||
}) => (
|
}) => (
|
||||||
<Stack.Col gap={1}>
|
<Stack.Col gap={1}>
|
||||||
<Stack.Row gap={1} align="center">
|
<Stack.Row gap={1} align="center">
|
||||||
{renderAction("zoomIn")}
|
|
||||||
{renderAction("zoomOut")}
|
{renderAction("zoomOut")}
|
||||||
|
{renderAction("zoomIn")}
|
||||||
{renderAction("resetZoom")}
|
{renderAction("resetZoom")}
|
||||||
<div style={{ marginInlineStart: 4 }}>
|
|
||||||
{(zoom.value * 100).toFixed(0)}%
|
|
||||||
</div>
|
|
||||||
</Stack.Row>
|
</Stack.Row>
|
||||||
</Stack.Col>
|
</Stack.Col>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
.excalidraw {
|
||||||
|
.ActiveFile {
|
||||||
|
.ActiveFile__fileName {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
span {
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
width: 9.3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 1.15em;
|
||||||
|
margin-inline-end: 0.3em;
|
||||||
|
transform: scaleY(0.9);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import Stack from "../components/Stack";
|
||||||
|
import { ToolButton } from "../components/ToolButton";
|
||||||
|
import { save, file } from "../components/icons";
|
||||||
|
import { t } from "../i18n";
|
||||||
|
|
||||||
|
import "./ActiveFile.scss";
|
||||||
|
|
||||||
|
type ActiveFileProps = {
|
||||||
|
fileName?: string;
|
||||||
|
onSave: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ActiveFile = ({ fileName, onSave }: ActiveFileProps) => (
|
||||||
|
<Stack.Row className="ActiveFile" gap={1} align="center">
|
||||||
|
<span className="ActiveFile__fileName">
|
||||||
|
{file}
|
||||||
|
<span>{fileName}</span>
|
||||||
|
</span>
|
||||||
|
<ToolButton
|
||||||
|
type="icon"
|
||||||
|
icon={save}
|
||||||
|
title={t("buttons.save")}
|
||||||
|
aria-label={t("buttons.save")}
|
||||||
|
onClick={onSave}
|
||||||
|
data-testid="save-button"
|
||||||
|
/>
|
||||||
|
</Stack.Row>
|
||||||
|
);
|
||||||
+109
-128
@@ -2,7 +2,6 @@ import React, { useContext } from "react";
|
|||||||
import { RoughCanvas } from "roughjs/bin/canvas";
|
import { RoughCanvas } from "roughjs/bin/canvas";
|
||||||
import rough from "roughjs/bin/rough";
|
import rough from "roughjs/bin/rough";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { supported as fsSupported } from "browser-fs-access";
|
|
||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -61,6 +60,7 @@ import {
|
|||||||
SCROLL_TIMEOUT,
|
SCROLL_TIMEOUT,
|
||||||
TAP_TWICE_TIMEOUT,
|
TAP_TWICE_TIMEOUT,
|
||||||
TEXT_TO_CENTER_SNAP_THRESHOLD,
|
TEXT_TO_CENTER_SNAP_THRESHOLD,
|
||||||
|
THEME,
|
||||||
TOUCH_CTX_MENU_TIMEOUT,
|
TOUCH_CTX_MENU_TIMEOUT,
|
||||||
URL_HASH_KEYS,
|
URL_HASH_KEYS,
|
||||||
URL_QUERY_KEYS,
|
URL_QUERY_KEYS,
|
||||||
@@ -111,7 +111,6 @@ import {
|
|||||||
import { LinearElementEditor } from "../element/linearElementEditor";
|
import { LinearElementEditor } from "../element/linearElementEditor";
|
||||||
import { mutateElement } from "../element/mutateElement";
|
import { mutateElement } from "../element/mutateElement";
|
||||||
import { deepCopyElement, newFreeDrawElement } from "../element/newElement";
|
import { deepCopyElement, newFreeDrawElement } from "../element/newElement";
|
||||||
import { MaybeTransformHandleType } from "../element/transformHandles";
|
|
||||||
import {
|
import {
|
||||||
isBindingElement,
|
isBindingElement,
|
||||||
isBindingElementType,
|
isBindingElementType,
|
||||||
@@ -157,6 +156,7 @@ import {
|
|||||||
getElementsWithinSelection,
|
getElementsWithinSelection,
|
||||||
getNormalizedZoom,
|
getNormalizedZoom,
|
||||||
getSelectedElements,
|
getSelectedElements,
|
||||||
|
hasBackground,
|
||||||
isOverScrollBars,
|
isOverScrollBars,
|
||||||
isSomeElementSelected,
|
isSomeElementSelected,
|
||||||
} from "../scene";
|
} from "../scene";
|
||||||
@@ -167,9 +167,11 @@ import { findShapeByKey } from "../shapes";
|
|||||||
import {
|
import {
|
||||||
AppProps,
|
AppProps,
|
||||||
AppState,
|
AppState,
|
||||||
|
ExcalidrawImperativeAPI,
|
||||||
Gesture,
|
Gesture,
|
||||||
GestureEvent,
|
GestureEvent,
|
||||||
LibraryItems,
|
LibraryItems,
|
||||||
|
PointerDownState,
|
||||||
SceneData,
|
SceneData,
|
||||||
} from "../types";
|
} from "../types";
|
||||||
import {
|
import {
|
||||||
@@ -180,7 +182,6 @@ import {
|
|||||||
isToolIcon,
|
isToolIcon,
|
||||||
isWritableElement,
|
isWritableElement,
|
||||||
resetCursor,
|
resetCursor,
|
||||||
ResolvablePromise,
|
|
||||||
resolvablePromise,
|
resolvablePromise,
|
||||||
sceneCoordsToViewportCoords,
|
sceneCoordsToViewportCoords,
|
||||||
setCursor,
|
setCursor,
|
||||||
@@ -194,12 +195,14 @@ import LayerUI from "./LayerUI";
|
|||||||
import { Stats } from "./Stats";
|
import { Stats } from "./Stats";
|
||||||
import { Toast } from "./Toast";
|
import { Toast } from "./Toast";
|
||||||
import { actionToggleViewMode } from "../actions/actionToggleViewMode";
|
import { actionToggleViewMode } from "../actions/actionToggleViewMode";
|
||||||
|
import { nativeFileSystemSupported } from "../data/filesystem";
|
||||||
|
|
||||||
const IsMobileContext = React.createContext(false);
|
const IsMobileContext = React.createContext(false);
|
||||||
export const useIsMobile = () => useContext(IsMobileContext);
|
export const useIsMobile = () => useContext(IsMobileContext);
|
||||||
const ExcalidrawContainerContext = React.createContext<HTMLDivElement | null>(
|
const ExcalidrawContainerContext = React.createContext<{
|
||||||
null,
|
container: HTMLDivElement | null;
|
||||||
);
|
id: string | null;
|
||||||
|
}>({ container: null, id: null });
|
||||||
export const useExcalidrawContainer = () =>
|
export const useExcalidrawContainer = () =>
|
||||||
useContext(ExcalidrawContainerContext);
|
useContext(ExcalidrawContainerContext);
|
||||||
|
|
||||||
@@ -222,83 +225,6 @@ const gesture: Gesture = {
|
|||||||
initialScale: null,
|
initialScale: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PointerDownState = Readonly<{
|
|
||||||
// The first position at which pointerDown happened
|
|
||||||
origin: Readonly<{ x: number; y: number }>;
|
|
||||||
// Same as "origin" but snapped to the grid, if grid is on
|
|
||||||
originInGrid: Readonly<{ x: number; y: number }>;
|
|
||||||
// Scrollbar checks
|
|
||||||
scrollbars: ReturnType<typeof isOverScrollBars>;
|
|
||||||
// The previous pointer position
|
|
||||||
lastCoords: { x: number; y: number };
|
|
||||||
// map of original elements data
|
|
||||||
originalElements: Map<string, NonDeleted<ExcalidrawElement>>;
|
|
||||||
resize: {
|
|
||||||
// Handle when resizing, might change during the pointer interaction
|
|
||||||
handleType: MaybeTransformHandleType;
|
|
||||||
// This is determined on the initial pointer down event
|
|
||||||
isResizing: boolean;
|
|
||||||
// This is determined on the initial pointer down event
|
|
||||||
offset: { x: number; y: number };
|
|
||||||
// This is determined on the initial pointer down event
|
|
||||||
arrowDirection: "origin" | "end";
|
|
||||||
// This is a center point of selected elements determined on the initial pointer down event (for rotation only)
|
|
||||||
center: { x: number; y: number };
|
|
||||||
};
|
|
||||||
hit: {
|
|
||||||
// The element the pointer is "hitting", is determined on the initial
|
|
||||||
// pointer down event
|
|
||||||
element: NonDeleted<ExcalidrawElement> | null;
|
|
||||||
// The elements the pointer is "hitting", is determined on the initial
|
|
||||||
// pointer down event
|
|
||||||
allHitElements: NonDeleted<ExcalidrawElement>[];
|
|
||||||
// This is determined on the initial pointer down event
|
|
||||||
wasAddedToSelection: boolean;
|
|
||||||
// Whether selected element(s) were duplicated, might change during the
|
|
||||||
// pointer interaction
|
|
||||||
hasBeenDuplicated: boolean;
|
|
||||||
hasHitCommonBoundingBoxOfSelectedElements: boolean;
|
|
||||||
};
|
|
||||||
withCmdOrCtrl: boolean;
|
|
||||||
drag: {
|
|
||||||
// Might change during the pointer interation
|
|
||||||
hasOccurred: boolean;
|
|
||||||
// Might change during the pointer interation
|
|
||||||
offset: { x: number; y: number } | null;
|
|
||||||
};
|
|
||||||
// We need to have these in the state so that we can unsubscribe them
|
|
||||||
eventListeners: {
|
|
||||||
// It's defined on the initial pointer down event
|
|
||||||
onMove: null | ((event: PointerEvent) => void);
|
|
||||||
// It's defined on the initial pointer down event
|
|
||||||
onUp: null | ((event: PointerEvent) => void);
|
|
||||||
// It's defined on the initial pointer down event
|
|
||||||
onKeyDown: null | ((event: KeyboardEvent) => void);
|
|
||||||
// It's defined on the initial pointer down event
|
|
||||||
onKeyUp: null | ((event: KeyboardEvent) => void);
|
|
||||||
};
|
|
||||||
}>;
|
|
||||||
|
|
||||||
export type ExcalidrawImperativeAPI = {
|
|
||||||
updateScene: InstanceType<typeof App>["updateScene"];
|
|
||||||
resetScene: InstanceType<typeof App>["resetScene"];
|
|
||||||
getSceneElementsIncludingDeleted: InstanceType<
|
|
||||||
typeof App
|
|
||||||
>["getSceneElementsIncludingDeleted"];
|
|
||||||
history: {
|
|
||||||
clear: InstanceType<typeof App>["resetHistory"];
|
|
||||||
};
|
|
||||||
scrollToContent: InstanceType<typeof App>["scrollToContent"];
|
|
||||||
getSceneElements: InstanceType<typeof App>["getSceneElements"];
|
|
||||||
getAppState: () => InstanceType<typeof App>["state"];
|
|
||||||
refresh: InstanceType<typeof App>["refresh"];
|
|
||||||
importLibrary: InstanceType<typeof App>["importLibraryFromUrl"];
|
|
||||||
setToastMessage: InstanceType<typeof App>["setToastMessage"];
|
|
||||||
readyPromise: ResolvablePromise<ExcalidrawImperativeAPI>;
|
|
||||||
ready: true;
|
|
||||||
id: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
class App extends React.Component<AppProps, AppState> {
|
class App extends React.Component<AppProps, AppState> {
|
||||||
canvas: HTMLCanvasElement | null = null;
|
canvas: HTMLCanvasElement | null = null;
|
||||||
rc: RoughCanvas | null = null;
|
rc: RoughCanvas | null = null;
|
||||||
@@ -321,6 +247,10 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
public libraryItemsFromStorage: LibraryItems | undefined;
|
public libraryItemsFromStorage: LibraryItems | undefined;
|
||||||
private id: string;
|
private id: string;
|
||||||
private history: History;
|
private history: History;
|
||||||
|
private excalidrawContainerValue: {
|
||||||
|
container: HTMLDivElement | null;
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
|
||||||
constructor(props: AppProps) {
|
constructor(props: AppProps) {
|
||||||
super(props);
|
super(props);
|
||||||
@@ -377,6 +307,12 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
}
|
}
|
||||||
readyPromise.resolve(api);
|
readyPromise.resolve(api);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.excalidrawContainerValue = {
|
||||||
|
container: this.excalidrawContainerRef.current,
|
||||||
|
id: this.id,
|
||||||
|
};
|
||||||
|
|
||||||
this.scene = new Scene();
|
this.scene = new Scene();
|
||||||
this.library = new Library(this);
|
this.library = new Library(this);
|
||||||
this.history = new History();
|
this.history = new History();
|
||||||
@@ -404,11 +340,11 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
if (viewModeEnabled) {
|
if (viewModeEnabled) {
|
||||||
return (
|
return (
|
||||||
<canvas
|
<canvas
|
||||||
id="canvas"
|
className="excalidraw__canvas"
|
||||||
style={{
|
style={{
|
||||||
width: canvasDOMWidth,
|
width: canvasDOMWidth,
|
||||||
height: canvasDOMHeight,
|
height: canvasDOMHeight,
|
||||||
cursor: "grabbing",
|
cursor: CURSOR_TYPE.GRAB,
|
||||||
}}
|
}}
|
||||||
width={canvasWidth}
|
width={canvasWidth}
|
||||||
height={canvasHeight}
|
height={canvasHeight}
|
||||||
@@ -426,7 +362,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<canvas
|
<canvas
|
||||||
id="canvas"
|
className="excalidraw__canvas"
|
||||||
style={{
|
style={{
|
||||||
width: canvasDOMWidth,
|
width: canvasDOMWidth,
|
||||||
height: canvasDOMHeight,
|
height: canvasDOMHeight,
|
||||||
@@ -471,7 +407,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<ExcalidrawContainerContext.Provider
|
<ExcalidrawContainerContext.Provider
|
||||||
value={this.excalidrawContainerRef.current}
|
value={this.excalidrawContainerValue}
|
||||||
>
|
>
|
||||||
<IsMobileContext.Provider value={this.isMobile}>
|
<IsMobileContext.Provider value={this.isMobile}>
|
||||||
<LayerUI
|
<LayerUI
|
||||||
@@ -534,7 +470,9 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public focusContainer = () => {
|
public focusContainer = () => {
|
||||||
|
if (this.props.autoFocus) {
|
||||||
this.excalidrawContainerRef.current?.focus();
|
this.excalidrawContainerRef.current?.focus();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
public getSceneElementsIncludingDeleted = () => {
|
public getSceneElementsIncludingDeleted = () => {
|
||||||
@@ -576,7 +514,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
let viewModeEnabled = actionResult?.appState?.viewModeEnabled || false;
|
let viewModeEnabled = actionResult?.appState?.viewModeEnabled || false;
|
||||||
let zenModeEnabled = actionResult?.appState?.zenModeEnabled || false;
|
let zenModeEnabled = actionResult?.appState?.zenModeEnabled || false;
|
||||||
let gridSize = actionResult?.appState?.gridSize || null;
|
let gridSize = actionResult?.appState?.gridSize || null;
|
||||||
let theme = actionResult?.appState?.theme || "light";
|
let theme = actionResult?.appState?.theme || THEME.LIGHT;
|
||||||
let name = actionResult?.appState?.name ?? this.state.name;
|
let name = actionResult?.appState?.name ?? this.state.name;
|
||||||
if (typeof this.props.viewModeEnabled !== "undefined") {
|
if (typeof this.props.viewModeEnabled !== "undefined") {
|
||||||
viewModeEnabled = this.props.viewModeEnabled;
|
viewModeEnabled = this.props.viewModeEnabled;
|
||||||
@@ -718,7 +656,11 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
const fileHandle = launchParams.files[0];
|
const fileHandle = launchParams.files[0];
|
||||||
const blob: Blob = await fileHandle.getFile();
|
const blob: Blob = await fileHandle.getFile();
|
||||||
blob.handle = fileHandle;
|
blob.handle = fileHandle;
|
||||||
loadFromBlob(blob, this.state)
|
loadFromBlob(
|
||||||
|
blob,
|
||||||
|
this.state,
|
||||||
|
this.scene.getElementsIncludingDeleted(),
|
||||||
|
)
|
||||||
.then(({ elements, appState }) =>
|
.then(({ elements, appState }) =>
|
||||||
this.syncActionResult({
|
this.syncActionResult({
|
||||||
elements,
|
elements,
|
||||||
@@ -745,7 +687,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
if (initialData?.libraryItems) {
|
if (initialData?.libraryItems) {
|
||||||
this.libraryItemsFromStorage = initialData.libraryItems;
|
this.libraryItemsFromStorage = initialData.libraryItems;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
initialData = {
|
initialData = {
|
||||||
appState: {
|
appState: {
|
||||||
@@ -756,7 +698,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const scene = restore(initialData, null);
|
const scene = restore(initialData, null, null);
|
||||||
scene.appState = {
|
scene.appState = {
|
||||||
...scene.appState,
|
...scene.appState,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
@@ -800,6 +742,8 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
public async componentDidMount() {
|
public async componentDidMount() {
|
||||||
|
this.excalidrawContainerValue.container = this.excalidrawContainerRef.current;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
process.env.NODE_ENV === ENV.TEST ||
|
process.env.NODE_ENV === ENV.TEST ||
|
||||||
process.env.NODE_ENV === ENV.DEVELOPMENT
|
process.env.NODE_ENV === ENV.DEVELOPMENT
|
||||||
@@ -888,6 +832,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
private removeEventListeners() {
|
private removeEventListeners() {
|
||||||
|
document.removeEventListener(EVENT.POINTER_UP, this.removePointer);
|
||||||
document.removeEventListener(EVENT.COPY, this.onCopy);
|
document.removeEventListener(EVENT.COPY, this.onCopy);
|
||||||
document.removeEventListener(EVENT.PASTE, this.pasteFromClipboard);
|
document.removeEventListener(EVENT.PASTE, this.pasteFromClipboard);
|
||||||
document.removeEventListener(EVENT.CUT, this.onCut);
|
document.removeEventListener(EVENT.CUT, this.onCut);
|
||||||
@@ -929,6 +874,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
|
|
||||||
private addEventListeners() {
|
private addEventListeners() {
|
||||||
this.removeEventListeners();
|
this.removeEventListeners();
|
||||||
|
document.addEventListener(EVENT.POINTER_UP, this.removePointer); // #3553
|
||||||
document.addEventListener(EVENT.COPY, this.onCopy);
|
document.addEventListener(EVENT.COPY, this.onCopy);
|
||||||
if (this.props.handleKeyboardGlobally) {
|
if (this.props.handleKeyboardGlobally) {
|
||||||
document.addEventListener(EVENT.KEYDOWN, this.onKeyDown, false);
|
document.addEventListener(EVENT.KEYDOWN, this.onKeyDown, false);
|
||||||
@@ -984,14 +930,12 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (prevProps.viewModeEnabled !== this.props.viewModeEnabled) {
|
if (prevProps.viewModeEnabled !== this.props.viewModeEnabled) {
|
||||||
this.setState(
|
this.setState({ viewModeEnabled: !!this.props.viewModeEnabled });
|
||||||
{ viewModeEnabled: !!this.props.viewModeEnabled },
|
|
||||||
this.addEventListeners,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (prevState.viewModeEnabled !== this.state.viewModeEnabled) {
|
if (prevState.viewModeEnabled !== this.state.viewModeEnabled) {
|
||||||
this.addEventListeners();
|
this.addEventListeners();
|
||||||
|
this.deselectElements();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (prevProps.zenModeEnabled !== this.props.zenModeEnabled) {
|
if (prevProps.zenModeEnabled !== this.props.zenModeEnabled) {
|
||||||
@@ -1250,9 +1194,13 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
}
|
}
|
||||||
const data = await parseClipboard(event);
|
const data = await parseClipboard(event);
|
||||||
if (this.props.onPaste) {
|
if (this.props.onPaste) {
|
||||||
if (await this.props.onPaste(data, event)) {
|
try {
|
||||||
|
if ((await this.props.onPaste(data, event)) === false) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (data.errorMessage) {
|
if (data.errorMessage) {
|
||||||
this.setState({ errorMessage: data.errorMessage });
|
this.setState({ errorMessage: data.errorMessage });
|
||||||
@@ -1265,7 +1213,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
});
|
});
|
||||||
} else if (data.elements) {
|
} else if (data.elements) {
|
||||||
this.addElementsFromPasteOrLibrary({
|
this.addElementsFromPasteOrLibrary({
|
||||||
elements: restoreElements(data.elements),
|
elements: data.elements,
|
||||||
position: "cursor",
|
position: "cursor",
|
||||||
});
|
});
|
||||||
} else if (data.text) {
|
} else if (data.text) {
|
||||||
@@ -1280,7 +1228,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
elements: readonly ExcalidrawElement[];
|
elements: readonly ExcalidrawElement[];
|
||||||
position: { clientX: number; clientY: number } | "cursor" | "center";
|
position: { clientX: number; clientY: number } | "cursor" | "center";
|
||||||
}) => {
|
}) => {
|
||||||
const elements = restoreElements(opts.elements);
|
const elements = restoreElements(opts.elements, null);
|
||||||
const [minX, minY, maxX, maxY] = getCommonBounds(elements);
|
const [minX, minY, maxX, maxY] = getCommonBounds(elements);
|
||||||
|
|
||||||
const elementsCenterX = distance(minX, maxX) / 2;
|
const elementsCenterX = distance(minX, maxX) / 2;
|
||||||
@@ -1346,6 +1294,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.scene.getElements(),
|
this.scene.getElements(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
this.selectShapeTool("selection");
|
||||||
};
|
};
|
||||||
|
|
||||||
private addTextFromPaste(text: any) {
|
private addTextFromPaste(text: any) {
|
||||||
@@ -1386,7 +1335,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.setState(obj);
|
this.setState(obj);
|
||||||
};
|
};
|
||||||
|
|
||||||
removePointer = (event: React.PointerEvent<HTMLElement>) => {
|
removePointer = (event: React.PointerEvent<HTMLElement> | PointerEvent) => {
|
||||||
// remove touch handler for context menu on touch devices
|
// remove touch handler for context menu on touch devices
|
||||||
if (event.pointerType === "touch" && touchTimeout) {
|
if (event.pointerType === "touch" && touchTimeout) {
|
||||||
clearTimeout(touchTimeout);
|
clearTimeout(touchTimeout);
|
||||||
@@ -1452,7 +1401,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
await webShareTargetCache.delete("shared-file");
|
await webShareTargetCache.delete("shared-file");
|
||||||
window.history.replaceState(null, APP_NAME, window.location.pathname);
|
window.history.replaceState(null, APP_NAME, window.location.pathname);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
this.setState({ errorMessage: error.message });
|
this.setState({ errorMessage: error.message });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -1649,13 +1598,22 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.scene.getElements(),
|
this.scene.getElements(),
|
||||||
this.state,
|
this.state,
|
||||||
);
|
);
|
||||||
if (selectedElements.length) {
|
if (
|
||||||
if (event.key === KEYS.G) {
|
this.state.elementType === "selection" &&
|
||||||
this.setState({ openMenu: "backgroundColorPicker" });
|
!selectedElements.length
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
event.key === KEYS.G &&
|
||||||
|
(hasBackground(this.state.elementType) ||
|
||||||
|
selectedElements.some((element) => hasBackground(element.type)))
|
||||||
|
) {
|
||||||
|
this.setState({ openPopup: "backgroundColorPicker" });
|
||||||
}
|
}
|
||||||
if (event.key === KEYS.S) {
|
if (event.key === KEYS.S) {
|
||||||
this.setState({ openMenu: "strokeColorPicker" });
|
this.setState({ openPopup: "strokeColorPicker" });
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -1663,7 +1621,9 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
|
|
||||||
private onKeyUp = withBatchedUpdates((event: KeyboardEvent) => {
|
private onKeyUp = withBatchedUpdates((event: KeyboardEvent) => {
|
||||||
if (event.key === KEYS.SPACE) {
|
if (event.key === KEYS.SPACE) {
|
||||||
if (this.state.elementType === "selection") {
|
if (this.state.viewModeEnabled) {
|
||||||
|
setCursor(this.canvas, CURSOR_TYPE.GRAB);
|
||||||
|
} else if (this.state.elementType === "selection") {
|
||||||
resetCursor(this.canvas);
|
resetCursor(this.canvas);
|
||||||
} else {
|
} else {
|
||||||
setCursorForShape(this.canvas, this.state.elementType);
|
setCursorForShape(this.canvas, this.state.elementType);
|
||||||
@@ -1811,7 +1771,8 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
[element.id]: true,
|
[element.id]: true,
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
} else {
|
}
|
||||||
|
if (isDeleted) {
|
||||||
fixBindingsAfterDeletion(this.scene.getElements(), [element]);
|
fixBindingsAfterDeletion(this.scene.getElements(), [element]);
|
||||||
}
|
}
|
||||||
if (!isDeleted || isExistingElement) {
|
if (!isDeleted || isExistingElement) {
|
||||||
@@ -1832,15 +1793,19 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
excalidrawContainer: this.excalidrawContainerRef.current,
|
excalidrawContainer: this.excalidrawContainerRef.current,
|
||||||
});
|
});
|
||||||
// deselect all other elements when inserting text
|
// deselect all other elements when inserting text
|
||||||
|
this.deselectElements();
|
||||||
|
|
||||||
|
// do an initial update to re-initialize element position since we were
|
||||||
|
// modifying element's x/y for sake of editor (case: syncing to remote)
|
||||||
|
updateElement(element.text);
|
||||||
|
}
|
||||||
|
|
||||||
|
private deselectElements() {
|
||||||
this.setState({
|
this.setState({
|
||||||
selectedElementIds: {},
|
selectedElementIds: {},
|
||||||
selectedGroupIds: {},
|
selectedGroupIds: {},
|
||||||
editingGroupId: null,
|
editingGroupId: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
// do an initial update to re-initialize element position since we were
|
|
||||||
// modifying element's x/y for sake of editor (case: syncing to remote)
|
|
||||||
updateElement(element.text);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private getTextElementAtPosition(
|
private getTextElementAtPosition(
|
||||||
@@ -2283,6 +2248,8 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.canvas,
|
this.canvas,
|
||||||
isTextElement(hitElement) ? CURSOR_TYPE.TEXT : CURSOR_TYPE.CROSSHAIR,
|
isTextElement(hitElement) ? CURSOR_TYPE.TEXT : CURSOR_TYPE.CROSSHAIR,
|
||||||
);
|
);
|
||||||
|
} else if (this.state.viewModeEnabled) {
|
||||||
|
setCursor(this.canvas, CURSOR_TYPE.GRAB);
|
||||||
} else if (isOverScrollBar) {
|
} else if (isOverScrollBar) {
|
||||||
setCursor(this.canvas, CURSOR_TYPE.AUTO);
|
setCursor(this.canvas, CURSOR_TYPE.AUTO);
|
||||||
} else if (
|
} else if (
|
||||||
@@ -2308,8 +2275,6 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
private handleCanvasPointerDown = (
|
private handleCanvasPointerDown = (
|
||||||
event: React.PointerEvent<HTMLCanvasElement>,
|
event: React.PointerEvent<HTMLCanvasElement>,
|
||||||
) => {
|
) => {
|
||||||
event.persist();
|
|
||||||
|
|
||||||
// remove any active selection when we start to interact with canvas
|
// remove any active selection when we start to interact with canvas
|
||||||
// (mainly, we care about removing selection outside the component which
|
// (mainly, we care about removing selection outside the component which
|
||||||
// would prevent our copy handling otherwise)
|
// would prevent our copy handling otherwise)
|
||||||
@@ -2522,8 +2487,12 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
lastPointerUp = null;
|
lastPointerUp = null;
|
||||||
isPanning = false;
|
isPanning = false;
|
||||||
if (!isHoldingSpace) {
|
if (!isHoldingSpace) {
|
||||||
|
if (this.state.viewModeEnabled) {
|
||||||
|
setCursor(this.canvas, CURSOR_TYPE.GRAB);
|
||||||
|
} else {
|
||||||
setCursorForShape(this.canvas, this.state.elementType);
|
setCursorForShape(this.canvas, this.state.elementType);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
this.setState({
|
this.setState({
|
||||||
cursorButton: "up",
|
cursorButton: "up",
|
||||||
});
|
});
|
||||||
@@ -3526,6 +3495,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
mutateElement(draggingElement, {
|
mutateElement(draggingElement, {
|
||||||
points: [...points, [dx, dy]],
|
points: [...points, [dx, dy]],
|
||||||
pressures,
|
pressures,
|
||||||
|
lastCommittedPoint: [dx, dy],
|
||||||
});
|
});
|
||||||
|
|
||||||
this.actionManager.executeAction(actionFinalize);
|
this.actionManager.executeAction(actionFinalize);
|
||||||
@@ -3865,7 +3835,22 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
try {
|
try {
|
||||||
const file = event.dataTransfer.files[0];
|
const file = event.dataTransfer.files[0];
|
||||||
if (file?.type === "image/png" || file?.type === "image/svg+xml") {
|
if (file?.type === "image/png" || file?.type === "image/svg+xml") {
|
||||||
const { elements, appState } = await loadFromBlob(file, this.state);
|
if (nativeFileSystemSupported) {
|
||||||
|
try {
|
||||||
|
// This will only work as of Chrome 86,
|
||||||
|
// but can be safely ignored on older releases.
|
||||||
|
const item = event.dataTransfer.items[0];
|
||||||
|
(file as any).handle = await (item as any).getAsFileSystemHandle();
|
||||||
|
} catch (error: any) {
|
||||||
|
console.warn(error.name, error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const { elements, appState } = await loadFromBlob(
|
||||||
|
file,
|
||||||
|
this.state,
|
||||||
|
this.scene.getElementsIncludingDeleted(),
|
||||||
|
);
|
||||||
this.syncActionResult({
|
this.syncActionResult({
|
||||||
elements,
|
elements,
|
||||||
appState: {
|
appState: {
|
||||||
@@ -3876,7 +3861,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
return this.setState({
|
return this.setState({
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
errorMessage: error.message,
|
errorMessage: error.message,
|
||||||
@@ -3910,13 +3895,13 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
// default: assume an Excalidraw file regardless of extension/MimeType
|
// default: assume an Excalidraw file regardless of extension/MimeType
|
||||||
} else {
|
} else {
|
||||||
this.setState({ isLoading: true });
|
this.setState({ isLoading: true });
|
||||||
if (fsSupported) {
|
if (nativeFileSystemSupported) {
|
||||||
try {
|
try {
|
||||||
// This will only work as of Chrome 86,
|
// This will only work as of Chrome 86,
|
||||||
// but can be safely ignored on older releases.
|
// but can be safely ignored on older releases.
|
||||||
const item = event.dataTransfer.items[0];
|
const item = event.dataTransfer.items[0];
|
||||||
(file as any).handle = await (item as any).getAsFileSystemHandle();
|
(file as any).handle = await (item as any).getAsFileSystemHandle();
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
console.warn(error.name, error.message);
|
console.warn(error.name, error.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3925,7 +3910,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
loadFileToCanvas = (file: Blob) => {
|
loadFileToCanvas = (file: Blob) => {
|
||||||
loadFromBlob(file, this.state)
|
loadFromBlob(file, this.state, this.scene.getElementsIncludingDeleted())
|
||||||
.then(({ elements, appState }) =>
|
.then(({ elements, appState }) =>
|
||||||
this.syncActionResult({
|
this.syncActionResult({
|
||||||
elements,
|
elements,
|
||||||
@@ -4109,6 +4094,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
actionToggleStats,
|
actionToggleStats,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
if (this.state.viewModeEnabled) {
|
||||||
ContextMenu.push({
|
ContextMenu.push({
|
||||||
options: viewModeOptions,
|
options: viewModeOptions,
|
||||||
top,
|
top,
|
||||||
@@ -4117,11 +4103,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
appState: this.state,
|
appState: this.state,
|
||||||
container: this.excalidrawContainerRef.current!,
|
container: this.excalidrawContainerRef.current!,
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
if (this.state.viewModeEnabled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ContextMenu.push({
|
ContextMenu.push({
|
||||||
options: [
|
options: [
|
||||||
this.isMobile &&
|
this.isMobile &&
|
||||||
@@ -4161,9 +4143,8 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
appState: this.state,
|
appState: this.state,
|
||||||
container: this.excalidrawContainerRef.current!,
|
container: this.excalidrawContainerRef.current!,
|
||||||
});
|
});
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
} else if (type === "element") {
|
||||||
if (this.state.viewModeEnabled) {
|
if (this.state.viewModeEnabled) {
|
||||||
ContextMenu.push({
|
ContextMenu.push({
|
||||||
options: [navigator.clipboard && actionCopy, ...options],
|
options: [navigator.clipboard && actionCopy, ...options],
|
||||||
@@ -4173,9 +4154,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
appState: this.state,
|
appState: this.state,
|
||||||
container: this.excalidrawContainerRef.current!,
|
container: this.excalidrawContainerRef.current!,
|
||||||
});
|
});
|
||||||
return;
|
} else {
|
||||||
}
|
|
||||||
|
|
||||||
ContextMenu.push({
|
ContextMenu.push({
|
||||||
options: [
|
options: [
|
||||||
this.isMobile && actionCut,
|
this.isMobile && actionCut,
|
||||||
@@ -4219,6 +4198,8 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
appState: this.state,
|
appState: this.state,
|
||||||
container: this.excalidrawContainerRef.current!,
|
container: this.excalidrawContainerRef.current!,
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private handleWheel = withBatchedUpdates((event: WheelEvent) => {
|
private handleWheel = withBatchedUpdates((event: WheelEvent) => {
|
||||||
|
|||||||
@@ -16,10 +16,5 @@ export const BackgroundPickerAndDarkModeToggle = ({
|
|||||||
<div style={{ display: "flex" }}>
|
<div style={{ display: "flex" }}>
|
||||||
{actionManager.renderAction("changeViewBackgroundColor")}
|
{actionManager.renderAction("changeViewBackgroundColor")}
|
||||||
{showThemeBtn && actionManager.renderAction("toggleTheme")}
|
{showThemeBtn && actionManager.renderAction("toggleTheme")}
|
||||||
{appState.fileHandle && (
|
|
||||||
<div style={{ marginInlineStart: "0.25rem" }}>
|
|
||||||
{actionManager.renderAction("saveToActiveFile")}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import React from "react";
|
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
|
|
||||||
// TODO: It might be "clever" to add option.icon to the existing component <ButtonSelect />
|
// TODO: It might be "clever" to add option.icon to the existing component <ButtonSelect />
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import React from "react";
|
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
|
|
||||||
export const ButtonSelect = <T extends Object>({
|
export const ButtonSelect = <T extends Object>({
|
||||||
|
|||||||
@@ -81,7 +81,7 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.Tooltip-icon {
|
.excalidraw-tooltip-icon {
|
||||||
width: 1em;
|
width: 1em;
|
||||||
height: 1em;
|
height: 1em;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import React from "react";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { checkIcon } from "./icons";
|
import { checkIcon } from "./icons";
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import React from "react";
|
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { ToolButton } from "./ToolButton";
|
import { ToolButton } from "./ToolButton";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { Popover } from "./Popover";
|
import { Popover } from "./Popover";
|
||||||
|
import { isTransparent } from "../utils";
|
||||||
|
|
||||||
import "./ColorPicker.scss";
|
import "./ColorPicker.scss";
|
||||||
import { isArrowKey, KEYS } from "../keys";
|
import { isArrowKey, KEYS } from "../keys";
|
||||||
@@ -14,7 +15,7 @@ const isValidColor = (color: string) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getColor = (color: string): string | null => {
|
const getColor = (color: string): string | null => {
|
||||||
if (color === "transparent") {
|
if (isTransparent(color)) {
|
||||||
return color;
|
return color;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,15 +138,19 @@ const Picker = ({
|
|||||||
}}
|
}}
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
>
|
>
|
||||||
{colors.map((_color, i) => (
|
{colors.map((_color, i) => {
|
||||||
|
const _colorWithoutHash = _color.replace("#", "");
|
||||||
|
return (
|
||||||
<button
|
<button
|
||||||
className="color-picker-swatch"
|
className="color-picker-swatch"
|
||||||
onClick={(event) => {
|
onClick={(event) => {
|
||||||
(event.currentTarget as HTMLButtonElement).focus();
|
(event.currentTarget as HTMLButtonElement).focus();
|
||||||
onChange(_color);
|
onChange(_color);
|
||||||
}}
|
}}
|
||||||
title={`${_color} — ${keyBindings[i].toUpperCase()}`}
|
title={`${t(`colors.${_colorWithoutHash}`)}${
|
||||||
aria-label={_color}
|
!isTransparent(_color) ? ` (${_color})` : ""
|
||||||
|
} — ${keyBindings[i].toUpperCase()}`}
|
||||||
|
aria-label={t(`colors.${_colorWithoutHash}`)}
|
||||||
aria-keyshortcuts={keyBindings[i]}
|
aria-keyshortcuts={keyBindings[i]}
|
||||||
style={{ color: _color }}
|
style={{ color: _color }}
|
||||||
key={_color}
|
key={_color}
|
||||||
@@ -161,12 +166,13 @@ const Picker = ({
|
|||||||
onChange(_color);
|
onChange(_color);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{_color === "transparent" ? (
|
{isTransparent(_color) ? (
|
||||||
<div className="color-picker-transparent"></div>
|
<div className="color-picker-transparent"></div>
|
||||||
) : undefined}
|
) : undefined}
|
||||||
<span className="color-picker-keybinding">{keyBindings[i]}</span>
|
<span className="color-picker-keybinding">{keyBindings[i]}</span>
|
||||||
</button>
|
</button>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
{showInput && (
|
{showInput && (
|
||||||
<ColorInput
|
<ColorInput
|
||||||
color={color}
|
color={color}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import React from "react";
|
|
||||||
import { render, unmountComponentAtNode } from "react-dom";
|
import { render, unmountComponentAtNode } from "react-dom";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { Popover } from "./Popover";
|
import { Popover } from "./Popover";
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
import "./ToolIcon.scss";
|
import "./ToolIcon.scss";
|
||||||
|
|
||||||
import React from "react";
|
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { ToolButton } from "./ToolButton";
|
import { ToolButton } from "./ToolButton";
|
||||||
|
import { THEME } from "../constants";
|
||||||
export type Appearence = "light" | "dark";
|
import { Theme } from "../element/types";
|
||||||
|
|
||||||
// We chose to use only explicit toggle and not a third option for system value,
|
// We chose to use only explicit toggle and not a third option for system value,
|
||||||
// but this could be added in the future.
|
// but this could be added in the future.
|
||||||
export const DarkModeToggle = (props: {
|
export const DarkModeToggle = (props: {
|
||||||
value: Appearence;
|
value: Theme;
|
||||||
onChange: (value: Appearence) => void;
|
onChange: (value: Theme) => void;
|
||||||
title?: string;
|
title?: string;
|
||||||
}) => {
|
}) => {
|
||||||
const title =
|
const title =
|
||||||
@@ -20,10 +19,12 @@ export const DarkModeToggle = (props: {
|
|||||||
return (
|
return (
|
||||||
<ToolButton
|
<ToolButton
|
||||||
type="icon"
|
type="icon"
|
||||||
icon={props.value === "light" ? ICONS.MOON : ICONS.SUN}
|
icon={props.value === THEME.LIGHT ? ICONS.MOON : ICONS.SUN}
|
||||||
title={title}
|
title={title}
|
||||||
aria-label={title}
|
aria-label={title}
|
||||||
onClick={() => props.onChange(props.value === "dark" ? "light" : "dark")}
|
onClick={() =>
|
||||||
|
props.onChange(props.value === THEME.DARK ? THEME.LIGHT : THEME.DARK)
|
||||||
|
}
|
||||||
data-testid="toggle-dark-mode"
|
data-testid="toggle-dark-mode"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import clsx from "clsx";
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { useCallbackRefState } from "../hooks/useCallbackRefState";
|
import { useCallbackRefState } from "../hooks/useCallbackRefState";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { useIsMobile } from "../components/App";
|
import { useExcalidrawContainer, useIsMobile } from "../components/App";
|
||||||
import { KEYS } from "../keys";
|
import { KEYS } from "../keys";
|
||||||
import "./Dialog.scss";
|
import "./Dialog.scss";
|
||||||
import { back, close } from "./icons";
|
import { back, close } from "./icons";
|
||||||
@@ -21,6 +21,7 @@ export const Dialog = (props: {
|
|||||||
}) => {
|
}) => {
|
||||||
const [islandNode, setIslandNode] = useCallbackRefState<HTMLDivElement>();
|
const [islandNode, setIslandNode] = useCallbackRefState<HTMLDivElement>();
|
||||||
const [lastActiveElement] = useState(document.activeElement);
|
const [lastActiveElement] = useState(document.activeElement);
|
||||||
|
const { id } = useExcalidrawContainer();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!islandNode) {
|
if (!islandNode) {
|
||||||
@@ -82,7 +83,7 @@ export const Dialog = (props: {
|
|||||||
theme={props.theme}
|
theme={props.theme}
|
||||||
>
|
>
|
||||||
<Island ref={setIslandNode}>
|
<Island ref={setIslandNode}>
|
||||||
<h2 id="dialog-title" className="Dialog__title">
|
<h2 id={`${id}-dialog-title`} className="Dialog__title">
|
||||||
<span className="Dialog__titleContent">{props.title}</span>
|
<span className="Dialog__titleContent">{props.title}</span>
|
||||||
<button
|
<button
|
||||||
className="Modal__close"
|
className="Modal__close"
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export const ErrorDialog = ({
|
|||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
}) => {
|
}) => {
|
||||||
const [modalIsShown, setModalIsShown] = useState(!!message);
|
const [modalIsShown, setModalIsShown] = useState(!!message);
|
||||||
const excalidrawContainer = useExcalidrawContainer();
|
const { container: excalidrawContainer } = useExcalidrawContainer();
|
||||||
|
|
||||||
const handleClose = React.useCallback(() => {
|
const handleClose = React.useCallback(() => {
|
||||||
setModalIsShown(false);
|
setModalIsShown(false);
|
||||||
|
|||||||
@@ -97,7 +97,8 @@
|
|||||||
|
|
||||||
border-radius: 1rem;
|
border-radius: 1rem;
|
||||||
background-color: var(--button-color);
|
background-color: var(--button-color);
|
||||||
box-shadow: 0 3px 5px -1px rgb(0 0 0 / 28%), 0 6px 10px 0 rgb(0 0 0 / 14%);
|
box-shadow: 0 3px 5px -1px rgba(0, 0, 0, 0.28),
|
||||||
|
0 6px 10px 0 rgba(0, 0, 0, 0.14);
|
||||||
|
|
||||||
font-family: Cascadia;
|
font-family: Cascadia;
|
||||||
font-size: 1.8em;
|
font-size: 1.8em;
|
||||||
|
|||||||
@@ -157,6 +157,13 @@ export const HelpDialog = ({ onClose }: { onClose?: () => void }) => {
|
|||||||
shortcuts={["Shift+P", "7"]}
|
shortcuts={["Shift+P", "7"]}
|
||||||
/>
|
/>
|
||||||
<Shortcut label={t("toolBar.text")} shortcuts={["T", "8"]} />
|
<Shortcut label={t("toolBar.text")} shortcuts={["T", "8"]} />
|
||||||
|
<Shortcut
|
||||||
|
label={t("helpDialog.editSelectedShape")}
|
||||||
|
shortcuts={[
|
||||||
|
getShortcutKey("Enter"),
|
||||||
|
t("helpDialog.doubleClick"),
|
||||||
|
]}
|
||||||
|
/>
|
||||||
<Shortcut
|
<Shortcut
|
||||||
label={t("helpDialog.textNewLine")}
|
label={t("helpDialog.textNewLine")}
|
||||||
shortcuts={[
|
shortcuts={[
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import React from "react";
|
|
||||||
import { questionCircle } from "../components/icons";
|
import { questionCircle } from "../components/icons";
|
||||||
|
|
||||||
type HelpIconProps = {
|
type HelpIconProps = {
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import React from "react";
|
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { NonDeletedExcalidrawElement } from "../element/types";
|
import { NonDeletedExcalidrawElement } from "../element/types";
|
||||||
import { getSelectedElements } from "../scene";
|
import { getSelectedElements } from "../scene";
|
||||||
|
|
||||||
import "./HintViewer.scss";
|
import "./HintViewer.scss";
|
||||||
import { AppState } from "../types";
|
import { AppState } from "../types";
|
||||||
import { isLinearElement } from "../element/typeChecks";
|
import { isLinearElement, isTextElement } from "../element/typeChecks";
|
||||||
import { getShortcutKey } from "../utils";
|
import { getShortcutKey } from "../utils";
|
||||||
|
|
||||||
interface Hint {
|
interface Hint {
|
||||||
@@ -57,6 +56,14 @@ const getHints = ({ appState, elements }: Hint) => {
|
|||||||
return t("hints.lineEditor_info");
|
return t("hints.lineEditor_info");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (selectedElements.length === 1 && isTextElement(selectedElements[0])) {
|
||||||
|
return t("hints.text_selected");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (appState.editingElement && isTextElement(appState.editingElement)) {
|
||||||
|
return t("hints.text_editing");
|
||||||
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -8,20 +8,17 @@ import { CanvasError } from "../errors";
|
|||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { useIsMobile } from "./App";
|
import { useIsMobile } from "./App";
|
||||||
import { getSelectedElements, isSomeElementSelected } from "../scene";
|
import { getSelectedElements, isSomeElementSelected } from "../scene";
|
||||||
import { exportToCanvas, getExportSize } from "../scene/export";
|
import { exportToCanvas } from "../scene/export";
|
||||||
import { AppState } from "../types";
|
import { AppState } from "../types";
|
||||||
import { Dialog } from "./Dialog";
|
import { Dialog } from "./Dialog";
|
||||||
import { clipboard, exportImage } from "./icons";
|
import { clipboard, exportImage } from "./icons";
|
||||||
import Stack from "./Stack";
|
import Stack from "./Stack";
|
||||||
import { ToolButton } from "./ToolButton";
|
import { ToolButton } from "./ToolButton";
|
||||||
|
|
||||||
import "./ExportDialog.scss";
|
import "./ExportDialog.scss";
|
||||||
import { supported as fsSupported } from "browser-fs-access";
|
|
||||||
import OpenColor from "open-color";
|
import OpenColor from "open-color";
|
||||||
import { CheckboxItem } from "./CheckboxItem";
|
import { CheckboxItem } from "./CheckboxItem";
|
||||||
|
import { DEFAULT_EXPORT_PADDING } from "../constants";
|
||||||
const scales = [1, 2, 3];
|
import { nativeFileSystemSupported } from "../data/filesystem";
|
||||||
const defaultScale = scales.includes(devicePixelRatio) ? devicePixelRatio : 1;
|
|
||||||
|
|
||||||
const supportsContextFilters =
|
const supportsContextFilters =
|
||||||
"filter" in document.createElement("canvas").getContext("2d")!;
|
"filter" in document.createElement("canvas").getContext("2d")!;
|
||||||
@@ -82,7 +79,7 @@ const ExportButton: React.FC<{
|
|||||||
const ImageExportModal = ({
|
const ImageExportModal = ({
|
||||||
elements,
|
elements,
|
||||||
appState,
|
appState,
|
||||||
exportPadding = 10,
|
exportPadding = DEFAULT_EXPORT_PADDING,
|
||||||
actionManager,
|
actionManager,
|
||||||
onExportToPng,
|
onExportToPng,
|
||||||
onExportToSvg,
|
onExportToSvg,
|
||||||
@@ -98,7 +95,6 @@ const ImageExportModal = ({
|
|||||||
onCloseRequest: () => void;
|
onCloseRequest: () => void;
|
||||||
}) => {
|
}) => {
|
||||||
const someElementIsSelected = isSomeElementSelected(elements, appState);
|
const someElementIsSelected = isSomeElementSelected(elements, appState);
|
||||||
const [scale, setScale] = useState(defaultScale);
|
|
||||||
const [exportSelected, setExportSelected] = useState(someElementIsSelected);
|
const [exportSelected, setExportSelected] = useState(someElementIsSelected);
|
||||||
const previewRef = useRef<HTMLDivElement>(null);
|
const previewRef = useRef<HTMLDivElement>(null);
|
||||||
const { exportBackground, viewBackgroundColor } = appState;
|
const { exportBackground, viewBackgroundColor } = appState;
|
||||||
@@ -121,7 +117,6 @@ const ImageExportModal = ({
|
|||||||
exportBackground,
|
exportBackground,
|
||||||
viewBackgroundColor,
|
viewBackgroundColor,
|
||||||
exportPadding,
|
exportPadding,
|
||||||
scale,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// if converting to blob fails, there's some problem that will
|
// if converting to blob fails, there's some problem that will
|
||||||
@@ -144,7 +139,6 @@ const ImageExportModal = ({
|
|||||||
exportBackground,
|
exportBackground,
|
||||||
exportPadding,
|
exportPadding,
|
||||||
viewBackgroundColor,
|
viewBackgroundColor,
|
||||||
scale,
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -175,33 +169,8 @@ const ImageExportModal = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: "flex", alignItems: "center", marginTop: ".6em" }}>
|
<div style={{ display: "flex", alignItems: "center", marginTop: ".6em" }}>
|
||||||
<Stack.Row gap={2} justifyContent={"center"}>
|
<Stack.Row gap={2}>
|
||||||
{scales.map((_scale) => {
|
{actionManager.renderAction("changeExportScale")}
|
||||||
const [width, height] = getExportSize(
|
|
||||||
exportedElements,
|
|
||||||
exportPadding,
|
|
||||||
_scale,
|
|
||||||
);
|
|
||||||
|
|
||||||
const scaleButtonTitle = `${t(
|
|
||||||
"buttons.scale",
|
|
||||||
)} ${_scale}x (${width}x${height})`;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ToolButton
|
|
||||||
key={_scale}
|
|
||||||
size="s"
|
|
||||||
type="radio"
|
|
||||||
icon={`${_scale}x`}
|
|
||||||
name="export-canvas-scale"
|
|
||||||
title={scaleButtonTitle}
|
|
||||||
aria-label={scaleButtonTitle}
|
|
||||||
id="export-canvas-scale"
|
|
||||||
checked={_scale === scale}
|
|
||||||
onChange={() => setScale(_scale)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Stack.Row>
|
</Stack.Row>
|
||||||
<p style={{ marginLeft: "1em", userSelect: "none" }}>Scale</p>
|
<p style={{ marginLeft: "1em", userSelect: "none" }}>Scale</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -213,14 +182,15 @@ const ImageExportModal = ({
|
|||||||
margin: ".6em 0",
|
margin: ".6em 0",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{!fsSupported && actionManager.renderAction("changeProjectName")}
|
{!nativeFileSystemSupported &&
|
||||||
|
actionManager.renderAction("changeProjectName")}
|
||||||
</div>
|
</div>
|
||||||
<Stack.Row gap={2} justifyContent="center" style={{ margin: "2em 0" }}>
|
<Stack.Row gap={2} justifyContent="center" style={{ margin: "2em 0" }}>
|
||||||
<ExportButton
|
<ExportButton
|
||||||
color="indigo"
|
color="indigo"
|
||||||
title={t("buttons.exportToPng")}
|
title={t("buttons.exportToPng")}
|
||||||
aria-label={t("buttons.exportToPng")}
|
aria-label={t("buttons.exportToPng")}
|
||||||
onClick={() => onExportToPng(exportedElements, scale)}
|
onClick={() => onExportToPng(exportedElements)}
|
||||||
>
|
>
|
||||||
PNG
|
PNG
|
||||||
</ExportButton>
|
</ExportButton>
|
||||||
@@ -228,14 +198,14 @@ const ImageExportModal = ({
|
|||||||
color="red"
|
color="red"
|
||||||
title={t("buttons.exportToSvg")}
|
title={t("buttons.exportToSvg")}
|
||||||
aria-label={t("buttons.exportToSvg")}
|
aria-label={t("buttons.exportToSvg")}
|
||||||
onClick={() => onExportToSvg(exportedElements, scale)}
|
onClick={() => onExportToSvg(exportedElements)}
|
||||||
>
|
>
|
||||||
SVG
|
SVG
|
||||||
</ExportButton>
|
</ExportButton>
|
||||||
{probablySupportsClipboardBlob && (
|
{probablySupportsClipboardBlob && (
|
||||||
<ExportButton
|
<ExportButton
|
||||||
title={t("buttons.copyPngToClipboard")}
|
title={t("buttons.copyPngToClipboard")}
|
||||||
onClick={() => onExportToClipboard(exportedElements, scale)}
|
onClick={() => onExportToClipboard(exportedElements)}
|
||||||
color="gray"
|
color="gray"
|
||||||
shade={7}
|
shade={7}
|
||||||
>
|
>
|
||||||
@@ -250,7 +220,7 @@ const ImageExportModal = ({
|
|||||||
export const ImageExportDialog = ({
|
export const ImageExportDialog = ({
|
||||||
elements,
|
elements,
|
||||||
appState,
|
appState,
|
||||||
exportPadding = 10,
|
exportPadding = DEFAULT_EXPORT_PADDING,
|
||||||
actionManager,
|
actionManager,
|
||||||
onExportToPng,
|
onExportToPng,
|
||||||
onExportToSvg,
|
onExportToSvg,
|
||||||
|
|||||||
@@ -1,30 +1,25 @@
|
|||||||
import React from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
|
|
||||||
import { LoadingMessage } from "./LoadingMessage";
|
import { LoadingMessage } from "./LoadingMessage";
|
||||||
import { defaultLang, Language, languages, setLanguage } from "../i18n";
|
import { defaultLang, Language, languages, setLanguage } from "../i18n";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
langCode: Language["code"];
|
langCode: Language["code"];
|
||||||
|
children: React.ReactElement;
|
||||||
}
|
}
|
||||||
interface State {
|
|
||||||
isLoading: boolean;
|
|
||||||
}
|
|
||||||
export class InitializeApp extends React.Component<Props, State> {
|
|
||||||
public state: { isLoading: boolean } = {
|
|
||||||
isLoading: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
async componentDidMount() {
|
export const InitializeApp = (props: Props) => {
|
||||||
const currentLang =
|
const [loading, setLoading] = useState(true);
|
||||||
languages.find((lang) => lang.code === this.props.langCode) ||
|
|
||||||
defaultLang;
|
useEffect(() => {
|
||||||
|
const updateLang = async () => {
|
||||||
await setLanguage(currentLang);
|
await setLanguage(currentLang);
|
||||||
this.setState({
|
};
|
||||||
isLoading: false,
|
const currentLang =
|
||||||
});
|
languages.find((lang) => lang.code === props.langCode) || defaultLang;
|
||||||
}
|
updateLang();
|
||||||
|
setLoading(false);
|
||||||
|
}, [props.langCode]);
|
||||||
|
|
||||||
public render() {
|
return loading ? <LoadingMessage /> : props.children;
|
||||||
return this.state.isLoading ? <LoadingMessage /> : this.props.children;
|
};
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { actionSaveFileToDisk } from "../actions/actionExport";
|
|||||||
import { Card } from "./Card";
|
import { Card } from "./Card";
|
||||||
|
|
||||||
import "./ExportDialog.scss";
|
import "./ExportDialog.scss";
|
||||||
import { supported as fsSupported } from "browser-fs-access";
|
import { nativeFileSystemSupported } from "../data/filesystem";
|
||||||
|
|
||||||
export type ExportCB = (
|
export type ExportCB = (
|
||||||
elements: readonly NonDeletedExcalidrawElement[],
|
elements: readonly NonDeletedExcalidrawElement[],
|
||||||
@@ -42,7 +42,8 @@ const JSONExportModal = ({
|
|||||||
<h2>{t("exportDialog.disk_title")}</h2>
|
<h2>{t("exportDialog.disk_title")}</h2>
|
||||||
<div className="Card-details">
|
<div className="Card-details">
|
||||||
{t("exportDialog.disk_details")}
|
{t("exportDialog.disk_details")}
|
||||||
{!fsSupported && actionManager.renderAction("changeProjectName")}
|
{!nativeFileSystemSupported &&
|
||||||
|
actionManager.renderAction("changeProjectName")}
|
||||||
</div>
|
</div>
|
||||||
<ToolButton
|
<ToolButton
|
||||||
className="Card-button"
|
className="Card-button"
|
||||||
|
|||||||
@@ -73,10 +73,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
:root[dir="ltr"] &.layer-ui__wrapper__footer-left--transition-left {
|
:root[dir="ltr"] &.layer-ui__wrapper__footer-left--transition-left {
|
||||||
transform: translate(-92px, 0);
|
transform: translate(-76px, 0);
|
||||||
}
|
}
|
||||||
:root[dir="rtl"] &.layer-ui__wrapper__footer-left--transition-left {
|
:root[dir="rtl"] &.layer-ui__wrapper__footer-left--transition-left {
|
||||||
transform: translate(92px, 0);
|
transform: translate(76px, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.layer-ui__wrapper__footer-left--transition-bottom {
|
&.layer-ui__wrapper__footer-left--transition-bottom {
|
||||||
@@ -116,8 +116,19 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.layer-ui__wrapper__footer-left,
|
.layer-ui__wrapper__footer-left,
|
||||||
.layer-ui__wrapper__footer-right {
|
.layer-ui__wrapper__footer-right,
|
||||||
|
.disable-zen-mode--visible {
|
||||||
pointer-events: all;
|
pointer-events: all;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.layer-ui__wrapper__footer-left {
|
||||||
|
margin-bottom: 0.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layer-ui__wrapper__footer-right {
|
||||||
|
margin-top: auto;
|
||||||
|
margin-bottom: auto;
|
||||||
|
margin-inline-end: 1em;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+46
-13
@@ -36,7 +36,7 @@ import { Island } from "./Island";
|
|||||||
import "./LayerUI.scss";
|
import "./LayerUI.scss";
|
||||||
import { LibraryUnit } from "./LibraryUnit";
|
import { LibraryUnit } from "./LibraryUnit";
|
||||||
import { LoadingMessage } from "./LoadingMessage";
|
import { LoadingMessage } from "./LoadingMessage";
|
||||||
import { LockIcon } from "./LockIcon";
|
import { LockButton } from "./LockButton";
|
||||||
import { MobileMenu } from "./MobileMenu";
|
import { MobileMenu } from "./MobileMenu";
|
||||||
import { PasteChartDialog } from "./PasteChartDialog";
|
import { PasteChartDialog } from "./PasteChartDialog";
|
||||||
import { Section } from "./Section";
|
import { Section } from "./Section";
|
||||||
@@ -47,6 +47,8 @@ import { Tooltip } from "./Tooltip";
|
|||||||
import { UserList } from "./UserList";
|
import { UserList } from "./UserList";
|
||||||
import Library from "../data/library";
|
import Library from "../data/library";
|
||||||
import { JSONExportDialog } from "./JSONExportDialog";
|
import { JSONExportDialog } from "./JSONExportDialog";
|
||||||
|
import { LibraryButton } from "./LibraryButton";
|
||||||
|
import { isImageFileHandle } from "../data/blob";
|
||||||
|
|
||||||
interface LayerUIProps {
|
interface LayerUIProps {
|
||||||
actionManager: ActionManager;
|
actionManager: ActionManager;
|
||||||
@@ -107,6 +109,7 @@ const LibraryMenuItems = ({
|
|||||||
onAddToLibrary,
|
onAddToLibrary,
|
||||||
onInsertShape,
|
onInsertShape,
|
||||||
pendingElements,
|
pendingElements,
|
||||||
|
theme,
|
||||||
setAppState,
|
setAppState,
|
||||||
setLibraryItems,
|
setLibraryItems,
|
||||||
libraryReturnUrl,
|
libraryReturnUrl,
|
||||||
@@ -119,6 +122,7 @@ const LibraryMenuItems = ({
|
|||||||
onRemoveFromLibrary: (index: number) => void;
|
onRemoveFromLibrary: (index: number) => void;
|
||||||
onInsertShape: (elements: LibraryItem) => void;
|
onInsertShape: (elements: LibraryItem) => void;
|
||||||
onAddToLibrary: (elements: LibraryItem) => void;
|
onAddToLibrary: (elements: LibraryItem) => void;
|
||||||
|
theme: AppState["theme"];
|
||||||
setAppState: React.Component<any, AppState>["setState"];
|
setAppState: React.Component<any, AppState>["setState"];
|
||||||
setLibraryItems: (library: LibraryItems) => void;
|
setLibraryItems: (library: LibraryItems) => void;
|
||||||
libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"];
|
libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"];
|
||||||
@@ -192,7 +196,7 @@ const LibraryMenuItems = ({
|
|||||||
<a
|
<a
|
||||||
href={`https://libraries.excalidraw.com?target=${
|
href={`https://libraries.excalidraw.com?target=${
|
||||||
window.name || "_blank"
|
window.name || "_blank"
|
||||||
}&referrer=${referrer}&useHash=true&token=${id}`}
|
}&referrer=${referrer}&useHash=true&token=${id}&theme=${theme}`}
|
||||||
target="_excalidraw_libraries"
|
target="_excalidraw_libraries"
|
||||||
>
|
>
|
||||||
{t("labels.libraries")}
|
{t("labels.libraries")}
|
||||||
@@ -246,6 +250,7 @@ const LibraryMenu = ({
|
|||||||
onInsertShape,
|
onInsertShape,
|
||||||
pendingElements,
|
pendingElements,
|
||||||
onAddToLibrary,
|
onAddToLibrary,
|
||||||
|
theme,
|
||||||
setAppState,
|
setAppState,
|
||||||
libraryReturnUrl,
|
libraryReturnUrl,
|
||||||
focusContainer,
|
focusContainer,
|
||||||
@@ -256,6 +261,7 @@ const LibraryMenu = ({
|
|||||||
onClickOutside: (event: MouseEvent) => void;
|
onClickOutside: (event: MouseEvent) => void;
|
||||||
onInsertShape: (elements: LibraryItem) => void;
|
onInsertShape: (elements: LibraryItem) => void;
|
||||||
onAddToLibrary: () => void;
|
onAddToLibrary: () => void;
|
||||||
|
theme: AppState["theme"];
|
||||||
setAppState: React.Component<any, AppState>["setState"];
|
setAppState: React.Component<any, AppState>["setState"];
|
||||||
libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"];
|
libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"];
|
||||||
focusContainer: () => void;
|
focusContainer: () => void;
|
||||||
@@ -345,6 +351,7 @@ const LibraryMenu = ({
|
|||||||
libraryReturnUrl={libraryReturnUrl}
|
libraryReturnUrl={libraryReturnUrl}
|
||||||
focusContainer={focusContainer}
|
focusContainer={focusContainer}
|
||||||
library={library}
|
library={library}
|
||||||
|
theme={theme}
|
||||||
id={id}
|
id={id}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -400,19 +407,25 @@ const LayerUI = ({
|
|||||||
|
|
||||||
const createExporter = (type: ExportType): ExportCB => async (
|
const createExporter = (type: ExportType): ExportCB => async (
|
||||||
exportedElements,
|
exportedElements,
|
||||||
scale,
|
|
||||||
) => {
|
) => {
|
||||||
await exportCanvas(type, exportedElements, appState, {
|
const fileHandle = await exportCanvas(type, exportedElements, appState, {
|
||||||
exportBackground: appState.exportBackground,
|
exportBackground: appState.exportBackground,
|
||||||
name: appState.name,
|
name: appState.name,
|
||||||
viewBackgroundColor: appState.viewBackgroundColor,
|
viewBackgroundColor: appState.viewBackgroundColor,
|
||||||
scale,
|
|
||||||
})
|
})
|
||||||
.catch(muteFSAbortError)
|
.catch(muteFSAbortError)
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
setAppState({ errorMessage: error.message });
|
setAppState({ errorMessage: error.message });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (
|
||||||
|
appState.exportEmbedScene &&
|
||||||
|
fileHandle &&
|
||||||
|
isImageFileHandle(fileHandle)
|
||||||
|
) {
|
||||||
|
setAppState({ fileHandle });
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -484,6 +497,9 @@ const LayerUI = ({
|
|||||||
setAppState={setAppState}
|
setAppState={setAppState}
|
||||||
showThemeBtn={showThemeBtn}
|
showThemeBtn={showThemeBtn}
|
||||||
/>
|
/>
|
||||||
|
{appState.fileHandle && (
|
||||||
|
<>{actionManager.renderAction("saveToActiveFile")}</>
|
||||||
|
)}
|
||||||
</Stack.Col>
|
</Stack.Col>
|
||||||
</Island>
|
</Island>
|
||||||
</Section>
|
</Section>
|
||||||
@@ -502,7 +518,8 @@ const LayerUI = ({
|
|||||||
style={{
|
style={{
|
||||||
// we want to make sure this doesn't overflow so substracting 200
|
// we want to make sure this doesn't overflow so substracting 200
|
||||||
// which is approximately height of zoom footer and top left menu items with some buffer
|
// which is approximately height of zoom footer and top left menu items with some buffer
|
||||||
maxHeight: `${appState.height - 200}px`,
|
// if active file name is displayed, subtracting 248 to account for its height
|
||||||
|
maxHeight: `${appState.height - (appState.fileHandle ? 248 : 200)}px`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SelectedShapeActions
|
<SelectedShapeActions
|
||||||
@@ -539,6 +556,7 @@ const LayerUI = ({
|
|||||||
libraryReturnUrl={libraryReturnUrl}
|
libraryReturnUrl={libraryReturnUrl}
|
||||||
focusContainer={focusContainer}
|
focusContainer={focusContainer}
|
||||||
library={library}
|
library={library}
|
||||||
|
theme={appState.theme}
|
||||||
id={id}
|
id={id}
|
||||||
/>
|
/>
|
||||||
) : null;
|
) : null;
|
||||||
@@ -566,6 +584,12 @@ const LayerUI = ({
|
|||||||
{(heading) => (
|
{(heading) => (
|
||||||
<Stack.Col gap={4} align="start">
|
<Stack.Col gap={4} align="start">
|
||||||
<Stack.Row gap={1}>
|
<Stack.Row gap={1}>
|
||||||
|
<LockButton
|
||||||
|
zenModeEnabled={zenModeEnabled}
|
||||||
|
checked={appState.elementLocked}
|
||||||
|
onChange={onLockToggle}
|
||||||
|
title={t("toolBar.lock")}
|
||||||
|
/>
|
||||||
<Island
|
<Island
|
||||||
padding={1}
|
padding={1}
|
||||||
className={clsx({ "zen-mode": zenModeEnabled })}
|
className={clsx({ "zen-mode": zenModeEnabled })}
|
||||||
@@ -577,15 +601,12 @@ const LayerUI = ({
|
|||||||
canvas={canvas}
|
canvas={canvas}
|
||||||
elementType={appState.elementType}
|
elementType={appState.elementType}
|
||||||
setAppState={setAppState}
|
setAppState={setAppState}
|
||||||
isLibraryOpen={appState.isLibraryOpen}
|
|
||||||
/>
|
/>
|
||||||
</Stack.Row>
|
</Stack.Row>
|
||||||
</Island>
|
</Island>
|
||||||
<LockIcon
|
<LibraryButton
|
||||||
zenModeEnabled={zenModeEnabled}
|
appState={appState}
|
||||||
checked={appState.elementLocked}
|
setAppState={setAppState}
|
||||||
onChange={onLockToggle}
|
|
||||||
title={t("toolBar.lock")}
|
|
||||||
/>
|
/>
|
||||||
</Stack.Row>
|
</Stack.Row>
|
||||||
{libraryMenu}
|
{libraryMenu}
|
||||||
@@ -611,7 +632,9 @@ const LayerUI = ({
|
|||||||
label={client.username || "Unknown user"}
|
label={client.username || "Unknown user"}
|
||||||
key={clientId}
|
key={clientId}
|
||||||
>
|
>
|
||||||
{actionManager.renderAction("goToCollaborator", clientId)}
|
{actionManager.renderAction("goToCollaborator", {
|
||||||
|
id: clientId,
|
||||||
|
})}
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
))}
|
))}
|
||||||
</UserList>
|
</UserList>
|
||||||
@@ -644,6 +667,16 @@ const LayerUI = ({
|
|||||||
zoom={appState.zoom}
|
zoom={appState.zoom}
|
||||||
/>
|
/>
|
||||||
</Island>
|
</Island>
|
||||||
|
{!viewModeEnabled && (
|
||||||
|
<div
|
||||||
|
className={clsx("undo-redo-buttons zen-mode-transition", {
|
||||||
|
"layer-ui__wrapper__footer-left--transition-bottom": zenModeEnabled,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{actionManager.renderAction("undo", { size: "small" })}
|
||||||
|
{actionManager.renderAction("redo", { size: "small" })}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</Section>
|
</Section>
|
||||||
</Stack.Col>
|
</Stack.Col>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import React from "react";
|
||||||
|
import clsx from "clsx";
|
||||||
|
import { t } from "../i18n";
|
||||||
|
import { AppState } from "../types";
|
||||||
|
import { capitalizeString } from "../utils";
|
||||||
|
|
||||||
|
const LIBRARY_ICON = (
|
||||||
|
<svg viewBox="0 0 576 512">
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M542.22 32.05c-54.8 3.11-163.72 14.43-230.96 55.59-4.64 2.84-7.27 7.89-7.27 13.17v363.87c0 11.55 12.63 18.85 23.28 13.49 69.18-34.82 169.23-44.32 218.7-46.92 16.89-.89 30.02-14.43 30.02-30.66V62.75c.01-17.71-15.35-31.74-33.77-30.7zM264.73 87.64C197.5 46.48 88.58 35.17 33.78 32.05 15.36 31.01 0 45.04 0 62.75V400.6c0 16.24 13.13 29.78 30.02 30.66 49.49 2.6 149.59 12.11 218.77 46.95 10.62 5.35 23.21-1.94 23.21-13.46V100.63c0-5.29-2.62-10.14-7.27-12.99z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const LibraryButton: React.FC<{
|
||||||
|
appState: AppState;
|
||||||
|
setAppState: React.Component<any, AppState>["setState"];
|
||||||
|
}> = ({ appState, setAppState }) => {
|
||||||
|
return (
|
||||||
|
<label
|
||||||
|
className={clsx(
|
||||||
|
"ToolIcon ToolIcon_type_floating ToolIcon__library zen-mode-visibility",
|
||||||
|
`ToolIcon_size_medium`,
|
||||||
|
{
|
||||||
|
"zen-mode-visibility--hidden": appState.zenModeEnabled,
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
title={`${capitalizeString(t("toolBar.library"))} — 9`}
|
||||||
|
style={{ marginInlineStart: "var(--space-factor)" }}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
className="ToolIcon_type_checkbox"
|
||||||
|
type="checkbox"
|
||||||
|
name="editor-library"
|
||||||
|
onChange={(event) => {
|
||||||
|
setAppState({ isLibraryOpen: event.target.checked });
|
||||||
|
}}
|
||||||
|
checked={appState.isLibraryOpen}
|
||||||
|
aria-label={capitalizeString(t("toolBar.library"))}
|
||||||
|
aria-keyshortcuts="9"
|
||||||
|
/>
|
||||||
|
<div className="ToolIcon__icon">{LIBRARY_ICON}</div>
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import oc from "open-color";
|
import oc from "open-color";
|
||||||
import React, { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { close } from "../components/icons";
|
import { close } from "../components/icons";
|
||||||
import { MIME_TYPES } from "../constants";
|
import { MIME_TYPES } from "../constants";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
@@ -36,7 +36,11 @@ export const LibraryUnit = ({
|
|||||||
if (!elementsToRender) {
|
if (!elementsToRender) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const svg = exportToSvg(elementsToRender, {
|
let svg: SVGSVGElement;
|
||||||
|
const current = ref.current!;
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
svg = await exportToSvg(elementsToRender, {
|
||||||
exportBackground: false,
|
exportBackground: false,
|
||||||
viewBackgroundColor: oc.white,
|
viewBackgroundColor: oc.white,
|
||||||
});
|
});
|
||||||
@@ -44,13 +48,15 @@ export const LibraryUnit = ({
|
|||||||
if (child.tagName !== "svg") {
|
if (child.tagName !== "svg") {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
ref.current!.removeChild(child);
|
current!.removeChild(child);
|
||||||
}
|
}
|
||||||
ref.current!.appendChild(svg);
|
current!.appendChild(svg);
|
||||||
|
})();
|
||||||
|
|
||||||
const current = ref.current!;
|
|
||||||
return () => {
|
return () => {
|
||||||
|
if (svg) {
|
||||||
current.removeChild(svg);
|
current.removeChild(svg);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}, [elements, pendingElements]);
|
}, [elements, pendingElements]);
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import React from "react";
|
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
|
|
||||||
export const LoadingMessage = () => {
|
export const LoadingMessage = () => {
|
||||||
|
|||||||
@@ -2,20 +2,17 @@ import "./ToolIcon.scss";
|
|||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
|
import { ToolButtonSize } from "./ToolButton";
|
||||||
type LockIconSize = "s" | "m";
|
|
||||||
|
|
||||||
type LockIconProps = {
|
type LockIconProps = {
|
||||||
title?: string;
|
title?: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
id?: string;
|
|
||||||
checked: boolean;
|
checked: boolean;
|
||||||
onChange?(): void;
|
onChange?(): void;
|
||||||
size?: LockIconSize;
|
|
||||||
zenModeEnabled?: boolean;
|
zenModeEnabled?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const DEFAULT_SIZE: LockIconSize = "m";
|
const DEFAULT_SIZE: ToolButtonSize = "medium";
|
||||||
|
|
||||||
const ICONS = {
|
const ICONS = {
|
||||||
CHECKED: (
|
CHECKED: (
|
||||||
@@ -41,12 +38,12 @@ const ICONS = {
|
|||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const LockIcon = (props: LockIconProps) => {
|
export const LockButton = (props: LockIconProps) => {
|
||||||
return (
|
return (
|
||||||
<label
|
<label
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"ToolIcon ToolIcon__lock ToolIcon_type_floating zen-mode-visibility",
|
"ToolIcon ToolIcon__lock ToolIcon_type_floating zen-mode-visibility",
|
||||||
`ToolIcon_size_${props.size || DEFAULT_SIZE}`,
|
`ToolIcon_size_${DEFAULT_SIZE}`,
|
||||||
{
|
{
|
||||||
"zen-mode-visibility--hidden": props.zenModeEnabled,
|
"zen-mode-visibility--hidden": props.zenModeEnabled,
|
||||||
},
|
},
|
||||||
@@ -57,7 +54,6 @@ export const LockIcon = (props: LockIconProps) => {
|
|||||||
className="ToolIcon_type_checkbox"
|
className="ToolIcon_type_checkbox"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
name={props.name}
|
name={props.name}
|
||||||
id={props.id}
|
|
||||||
onChange={props.onChange}
|
onChange={props.onChange}
|
||||||
checked={props.checked}
|
checked={props.checked}
|
||||||
aria-label={props.title}
|
aria-label={props.title}
|
||||||
@@ -13,9 +13,10 @@ import { SelectedShapeActions, ShapesSwitcher } from "./Actions";
|
|||||||
import { Section } from "./Section";
|
import { Section } from "./Section";
|
||||||
import CollabButton from "./CollabButton";
|
import CollabButton from "./CollabButton";
|
||||||
import { SCROLLBAR_WIDTH, SCROLLBAR_MARGIN } from "../scene/scrollbars";
|
import { SCROLLBAR_WIDTH, SCROLLBAR_MARGIN } from "../scene/scrollbars";
|
||||||
import { LockIcon } from "./LockIcon";
|
import { LockButton } from "./LockButton";
|
||||||
import { UserList } from "./UserList";
|
import { UserList } from "./UserList";
|
||||||
import { BackgroundPickerAndDarkModeToggle } from "./BackgroundPickerAndDarkModeToggle";
|
import { BackgroundPickerAndDarkModeToggle } from "./BackgroundPickerAndDarkModeToggle";
|
||||||
|
import { LibraryButton } from "./LibraryButton";
|
||||||
|
|
||||||
type MobileMenuProps = {
|
type MobileMenuProps = {
|
||||||
appState: AppState;
|
appState: AppState;
|
||||||
@@ -64,15 +65,15 @@ export const MobileMenu = ({
|
|||||||
canvas={canvas}
|
canvas={canvas}
|
||||||
elementType={appState.elementType}
|
elementType={appState.elementType}
|
||||||
setAppState={setAppState}
|
setAppState={setAppState}
|
||||||
isLibraryOpen={appState.isLibraryOpen}
|
|
||||||
/>
|
/>
|
||||||
</Stack.Row>
|
</Stack.Row>
|
||||||
</Island>
|
</Island>
|
||||||
<LockIcon
|
<LockButton
|
||||||
checked={appState.elementLocked}
|
checked={appState.elementLocked}
|
||||||
onChange={onLockToggle}
|
onChange={onLockToggle}
|
||||||
title={t("toolBar.lock")}
|
title={t("toolBar.lock")}
|
||||||
/>
|
/>
|
||||||
|
<LibraryButton appState={appState} setAppState={setAppState} />
|
||||||
</Stack.Row>
|
</Stack.Row>
|
||||||
{libraryMenu}
|
{libraryMenu}
|
||||||
</Stack.Col>
|
</Stack.Col>
|
||||||
@@ -167,10 +168,9 @@ export const MobileMenu = ({
|
|||||||
)
|
)
|
||||||
.map(([clientId, client]) => (
|
.map(([clientId, client]) => (
|
||||||
<React.Fragment key={clientId}>
|
<React.Fragment key={clientId}>
|
||||||
{actionManager.renderAction(
|
{actionManager.renderAction("goToCollaborator", {
|
||||||
"goToCollaborator",
|
id: clientId,
|
||||||
clientId,
|
})}
|
||||||
)}
|
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
))}
|
))}
|
||||||
</UserList>
|
</UserList>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import clsx from "clsx";
|
|||||||
import { KEYS } from "../keys";
|
import { KEYS } from "../keys";
|
||||||
import { useExcalidrawContainer, useIsMobile } from "./App";
|
import { useExcalidrawContainer, useIsMobile } from "./App";
|
||||||
import { AppState } from "../types";
|
import { AppState } from "../types";
|
||||||
|
import { THEME } from "../constants";
|
||||||
|
|
||||||
export const Modal = (props: {
|
export const Modal = (props: {
|
||||||
className?: string;
|
className?: string;
|
||||||
@@ -15,7 +16,7 @@ export const Modal = (props: {
|
|||||||
labelledBy: string;
|
labelledBy: string;
|
||||||
theme?: AppState["theme"];
|
theme?: AppState["theme"];
|
||||||
}) => {
|
}) => {
|
||||||
const { theme = "light" } = props;
|
const { theme = THEME.LIGHT } = props;
|
||||||
const modalRoot = useBodyRoot(theme);
|
const modalRoot = useBodyRoot(theme);
|
||||||
|
|
||||||
if (!modalRoot) {
|
if (!modalRoot) {
|
||||||
@@ -58,7 +59,7 @@ const useBodyRoot = (theme: AppState["theme"]) => {
|
|||||||
const isMobileRef = useRef(isMobile);
|
const isMobileRef = useRef(isMobile);
|
||||||
isMobileRef.current = isMobile;
|
isMobileRef.current = isMobile;
|
||||||
|
|
||||||
const excalidrawContainer = useExcalidrawContainer();
|
const { container: excalidrawContainer } = useExcalidrawContainer();
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
if (div) {
|
if (div) {
|
||||||
|
|||||||
@@ -34,19 +34,21 @@ const ChartPreviewBtn = (props: {
|
|||||||
0,
|
0,
|
||||||
);
|
);
|
||||||
setChartElements(elements);
|
setChartElements(elements);
|
||||||
|
let svg: SVGSVGElement;
|
||||||
|
const previewNode = previewRef.current!;
|
||||||
|
|
||||||
const svg = exportToSvg(elements, {
|
(async () => {
|
||||||
|
svg = await exportToSvg(elements, {
|
||||||
exportBackground: false,
|
exportBackground: false,
|
||||||
viewBackgroundColor: oc.white,
|
viewBackgroundColor: oc.white,
|
||||||
});
|
});
|
||||||
|
|
||||||
const previewNode = previewRef.current!;
|
|
||||||
|
|
||||||
previewNode.appendChild(svg);
|
previewNode.appendChild(svg);
|
||||||
|
|
||||||
if (props.selected) {
|
if (props.selected) {
|
||||||
(previewNode.parentNode as HTMLDivElement).focus();
|
(previewNode.parentNode as HTMLDivElement).focus();
|
||||||
}
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
previewNode.removeChild(svg);
|
previewNode.removeChild(svg);
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import "./TextInput.scss";
|
import "./TextInput.scss";
|
||||||
|
|
||||||
import React, { Component } from "react";
|
import React, { useState } from "react";
|
||||||
import { focusNearestParent } from "../utils";
|
import { focusNearestParent } from "../utils";
|
||||||
|
|
||||||
import "./ProjectName.scss";
|
import "./ProjectName.scss";
|
||||||
|
import { useExcalidrawContainer } from "./App";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
value: string;
|
value: string;
|
||||||
@@ -12,22 +13,19 @@ type Props = {
|
|||||||
isNameEditable: boolean;
|
isNameEditable: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
type State = {
|
export const ProjectName = (props: Props) => {
|
||||||
fileName: string;
|
const { id } = useExcalidrawContainer();
|
||||||
};
|
const [fileName, setFileName] = useState<string>(props.value);
|
||||||
export class ProjectName extends Component<Props, State> {
|
|
||||||
state = {
|
const handleBlur = (event: any) => {
|
||||||
fileName: this.props.value,
|
|
||||||
};
|
|
||||||
private handleBlur = (event: any) => {
|
|
||||||
focusNearestParent(event.target);
|
focusNearestParent(event.target);
|
||||||
const value = event.target.value;
|
const value = event.target.value;
|
||||||
if (value !== this.props.value) {
|
if (value !== props.value) {
|
||||||
this.props.onChange(value);
|
props.onChange(value);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private handleKeyDown = (event: React.KeyboardEvent<HTMLElement>) => {
|
const handleKeyDown = (event: React.KeyboardEvent<HTMLElement>) => {
|
||||||
if (event.key === "Enter") {
|
if (event.key === "Enter") {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (event.nativeEvent.isComposing || event.keyCode === 229) {
|
if (event.nativeEvent.isComposing || event.keyCode === 229) {
|
||||||
@@ -37,29 +35,25 @@ export class ProjectName extends Component<Props, State> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
public render() {
|
|
||||||
return (
|
return (
|
||||||
<div className="ProjectName">
|
<div className="ProjectName">
|
||||||
<label className="ProjectName-label" htmlFor="filename">
|
<label className="ProjectName-label" htmlFor="filename">
|
||||||
{`${this.props.label}${this.props.isNameEditable ? "" : ":"}`}
|
{`${props.label}${props.isNameEditable ? "" : ":"}`}
|
||||||
</label>
|
</label>
|
||||||
{this.props.isNameEditable ? (
|
{props.isNameEditable ? (
|
||||||
<input
|
<input
|
||||||
className="TextInput"
|
className="TextInput"
|
||||||
onBlur={this.handleBlur}
|
onBlur={handleBlur}
|
||||||
onKeyDown={this.handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
id="filename"
|
id={`${id}-filename`}
|
||||||
value={this.state.fileName}
|
value={fileName}
|
||||||
onChange={(event) =>
|
onChange={(event) => setFileName(event.target.value)}
|
||||||
this.setState({ fileName: event.target.value })
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<span className="TextInput TextInput--readonly" id="filename">
|
<span className="TextInput TextInput--readonly" id={`${id}-filename`}>
|
||||||
{this.props.value}
|
{props.value}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
|
import { useExcalidrawContainer } from "./App";
|
||||||
|
|
||||||
interface SectionProps extends React.HTMLProps<HTMLElement> {
|
interface SectionProps extends React.HTMLProps<HTMLElement> {
|
||||||
heading: string;
|
heading: string;
|
||||||
@@ -7,13 +8,14 @@ interface SectionProps extends React.HTMLProps<HTMLElement> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const Section = ({ heading, children, ...props }: SectionProps) => {
|
export const Section = ({ heading, children, ...props }: SectionProps) => {
|
||||||
|
const { id } = useExcalidrawContainer();
|
||||||
const header = (
|
const header = (
|
||||||
<h2 className="visually-hidden" id={`${heading}-title`}>
|
<h2 className="visually-hidden" id={`${id}-${heading}-title`}>
|
||||||
{t(`headings.${heading}`)}
|
{t(`headings.${heading}`)}
|
||||||
</h2>
|
</h2>
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<section {...props} aria-labelledby={`${heading}-title`}>
|
<section {...props} aria-labelledby={`${id}-${heading}-title`}>
|
||||||
{typeof children === "function" ? (
|
{typeof children === "function" ? (
|
||||||
children(header)
|
children(header)
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useCallback, useEffect, useRef } from "react";
|
import { useCallback, useEffect, useRef } from "react";
|
||||||
import { TOAST_TIMEOUT } from "../constants";
|
import { TOAST_TIMEOUT } from "../constants";
|
||||||
import "./Toast.scss";
|
import "./Toast.scss";
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,9 @@ import "./ToolIcon.scss";
|
|||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
|
import { useExcalidrawContainer } from "./App";
|
||||||
|
|
||||||
type ToolIconSize = "s" | "m";
|
export type ToolButtonSize = "small" | "medium";
|
||||||
|
|
||||||
type ToolButtonBaseProps = {
|
type ToolButtonBaseProps = {
|
||||||
icon?: React.ReactNode;
|
icon?: React.ReactNode;
|
||||||
@@ -14,7 +15,7 @@ type ToolButtonBaseProps = {
|
|||||||
title?: string;
|
title?: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
id?: string;
|
id?: string;
|
||||||
size?: ToolIconSize;
|
size?: ToolButtonSize;
|
||||||
keyBindingLabel?: string;
|
keyBindingLabel?: string;
|
||||||
showAriaLabel?: boolean;
|
showAriaLabel?: boolean;
|
||||||
hidden?: boolean;
|
hidden?: boolean;
|
||||||
@@ -40,12 +41,11 @@ type ToolButtonProps =
|
|||||||
onChange?(): void;
|
onChange?(): void;
|
||||||
});
|
});
|
||||||
|
|
||||||
const DEFAULT_SIZE: ToolIconSize = "m";
|
|
||||||
|
|
||||||
export const ToolButton = React.forwardRef((props: ToolButtonProps, ref) => {
|
export const ToolButton = React.forwardRef((props: ToolButtonProps, ref) => {
|
||||||
|
const { id: excalId } = useExcalidrawContainer();
|
||||||
const innerRef = React.useRef(null);
|
const innerRef = React.useRef(null);
|
||||||
React.useImperativeHandle(ref, () => innerRef.current);
|
React.useImperativeHandle(ref, () => innerRef.current);
|
||||||
const sizeCn = `ToolIcon_size_${props.size || DEFAULT_SIZE}`;
|
const sizeCn = `ToolIcon_size_${props.size}`;
|
||||||
|
|
||||||
if (props.type === "button" || props.type === "icon") {
|
if (props.type === "button" || props.type === "icon") {
|
||||||
return (
|
return (
|
||||||
@@ -98,7 +98,7 @@ export const ToolButton = React.forwardRef((props: ToolButtonProps, ref) => {
|
|||||||
aria-label={props["aria-label"]}
|
aria-label={props["aria-label"]}
|
||||||
aria-keyshortcuts={props["aria-keyshortcuts"]}
|
aria-keyshortcuts={props["aria-keyshortcuts"]}
|
||||||
data-testid={props["data-testid"]}
|
data-testid={props["data-testid"]}
|
||||||
id={props.id}
|
id={`${excalId}-${props.id}`}
|
||||||
onChange={props.onChange}
|
onChange={props.onChange}
|
||||||
checked={props.checked}
|
checked={props.checked}
|
||||||
ref={innerRef}
|
ref={innerRef}
|
||||||
@@ -116,4 +116,5 @@ export const ToolButton = React.forwardRef((props: ToolButtonProps, ref) => {
|
|||||||
ToolButton.defaultProps = {
|
ToolButton.defaultProps = {
|
||||||
visible: true,
|
visible: true,
|
||||||
className: "",
|
className: "",
|
||||||
|
size: "medium",
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,10 +8,18 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
font-family: Cascadia;
|
font-family: Cascadia;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
background-color: var(--button-gray-1);
|
|
||||||
-webkit-tap-highlight-color: transparent;
|
-webkit-tap-highlight-color: transparent;
|
||||||
border-radius: var(--space-factor);
|
border-radius: var(--space-factor);
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
|
||||||
|
background-color: var(--button-gray-1);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--button-gray-2);
|
||||||
|
}
|
||||||
|
&:active {
|
||||||
|
background-color: var(--button-gray-3);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.ToolIcon--plain {
|
.ToolIcon--plain {
|
||||||
@@ -52,9 +60,9 @@
|
|||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ToolIcon_size_s .ToolIcon__icon {
|
.ToolIcon_size_small .ToolIcon__icon {
|
||||||
width: 1.4rem;
|
width: 2rem;
|
||||||
height: 1.4rem;
|
height: 2rem;
|
||||||
font-size: 0.8em;
|
font-size: 0.8em;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,14 +74,6 @@
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: inherit;
|
font-size: inherit;
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: var(--button-gray-1);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:active {
|
|
||||||
background-color: var(--button-gray-2);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:focus {
|
&:focus {
|
||||||
box-shadow: 0 0 0 2px var(--focus-highlight-color);
|
box-shadow: 0 0 0 2px var(--focus-highlight-color);
|
||||||
}
|
}
|
||||||
@@ -86,6 +86,14 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--button-gray-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
background-color: var(--button-gray-3);
|
||||||
|
}
|
||||||
|
|
||||||
&--show {
|
&--show {
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
}
|
}
|
||||||
@@ -103,6 +111,9 @@
|
|||||||
|
|
||||||
&:not(.ToolIcon_toggle_opaque):checked + .ToolIcon__icon {
|
&:not(.ToolIcon_toggle_opaque):checked + .ToolIcon__icon {
|
||||||
background-color: var(--button-gray-2);
|
background-color: var(--button-gray-2);
|
||||||
|
&:active {
|
||||||
|
background-color: var(--button-gray-3);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&:focus + .ToolIcon__icon {
|
&:focus + .ToolIcon__icon {
|
||||||
@@ -130,12 +141,21 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.ToolIcon__icon {
|
.ToolIcon__icon {
|
||||||
|
background-color: var(--button-gray-1);
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--button-gray-2);
|
||||||
|
}
|
||||||
|
&:active {
|
||||||
|
background-color: var(--button-gray-3);
|
||||||
|
}
|
||||||
|
|
||||||
width: 2rem;
|
width: 2rem;
|
||||||
height: 2em;
|
height: 2em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.ToolIcon.ToolIcon__lock {
|
.ToolIcon.ToolIcon__lock {
|
||||||
|
margin-inline-end: var(--space-factor);
|
||||||
&.ToolIcon_type_floating {
|
&.ToolIcon_type_floating {
|
||||||
margin-left: 0.1rem;
|
margin-left: 0.1rem;
|
||||||
}
|
}
|
||||||
@@ -166,10 +186,9 @@
|
|||||||
// move the lock button out of the way on small viewports
|
// move the lock button out of the way on small viewports
|
||||||
// it begins to collide with the GitHub icon before we switch to mobile mode
|
// it begins to collide with the GitHub icon before we switch to mobile mode
|
||||||
@media (max-width: 760px) {
|
@media (max-width: 760px) {
|
||||||
.ToolIcon.ToolIcon__lock {
|
.ToolIcon.ToolIcon_type_floating {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 60px;
|
|
||||||
right: -8px;
|
right: -8px;
|
||||||
|
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
@@ -194,6 +213,14 @@
|
|||||||
position: static;
|
position: static;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.ToolIcon.ToolIcon__library {
|
||||||
|
top: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ToolIcon.ToolIcon__lock {
|
||||||
|
margin-inline-end: 0;
|
||||||
|
top: 60px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.unlocked-icon {
|
.unlocked-icon {
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
@import "../css/variables.module";
|
@import "../css/variables.module";
|
||||||
|
|
||||||
|
// container in body where the actual tooltip is appended to
|
||||||
.excalidraw-tooltip {
|
.excalidraw-tooltip {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
@@ -24,8 +26,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.excalidraw {
|
// wraps the element we want to apply the tooltip to
|
||||||
.Tooltip-icon {
|
.excalidraw-tooltip-wrapper {
|
||||||
|
display: flex;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.excalidraw-tooltip-icon {
|
||||||
width: 0.9em;
|
width: 0.9em;
|
||||||
height: 0.9em;
|
height: 0.9em;
|
||||||
margin-left: 5px;
|
margin-left: 5px;
|
||||||
@@ -36,4 +43,3 @@
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ export const Tooltip = ({ children, label, long = false }: TooltipProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
className="excalidraw-tooltip-wrapper"
|
||||||
onPointerEnter={(event) =>
|
onPointerEnter={(event) =>
|
||||||
updateTooltip(
|
updateTooltip(
|
||||||
event.currentTarget as HTMLDivElement,
|
event.currentTarget as HTMLDivElement,
|
||||||
|
|||||||
+57
-75
@@ -10,13 +10,15 @@ import React from "react";
|
|||||||
|
|
||||||
import oc from "open-color";
|
import oc from "open-color";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
|
import { Theme } from "../element/types";
|
||||||
|
import { THEME } from "../constants";
|
||||||
|
|
||||||
const activeElementColor = (theme: "light" | "dark") =>
|
const activeElementColor = (theme: Theme) =>
|
||||||
theme === "light" ? oc.orange[4] : oc.orange[9];
|
theme === THEME.LIGHT ? oc.orange[4] : oc.orange[9];
|
||||||
const iconFillColor = (theme: "light" | "dark") =>
|
const iconFillColor = (theme: Theme) =>
|
||||||
theme === "light" ? oc.black : oc.gray[4];
|
theme === THEME.LIGHT ? oc.black : oc.gray[4];
|
||||||
const handlerColor = (theme: "light" | "dark") =>
|
const handlerColor = (theme: Theme) =>
|
||||||
theme === "light" ? oc.white : "#1e1e1e";
|
theme === THEME.LIGHT ? oc.white : "#1e1e1e";
|
||||||
|
|
||||||
type Opts = {
|
type Opts = {
|
||||||
width?: number;
|
width?: number;
|
||||||
@@ -24,7 +26,10 @@ type Opts = {
|
|||||||
mirror?: true;
|
mirror?: true;
|
||||||
} & React.SVGProps<SVGSVGElement>;
|
} & React.SVGProps<SVGSVGElement>;
|
||||||
|
|
||||||
const createIcon = (d: string | React.ReactNode, opts: number | Opts = 512) => {
|
export const createIcon = (
|
||||||
|
d: string | React.ReactNode,
|
||||||
|
opts: number | Opts = 512,
|
||||||
|
) => {
|
||||||
const { width = 512, height = width, mirror, style } =
|
const { width = 512, height = width, mirror, style } =
|
||||||
typeof opts === "number" ? ({ width: opts } as Opts) : opts;
|
typeof opts === "number" ? ({ width: opts } as Opts) : opts;
|
||||||
return (
|
return (
|
||||||
@@ -172,8 +177,7 @@ export const resetZoom = createIcon(
|
|||||||
{ width: 1024 },
|
{ width: 1024 },
|
||||||
);
|
);
|
||||||
|
|
||||||
export const BringForwardIcon = React.memo(
|
export const BringForwardIcon = React.memo(({ theme }: { theme: Theme }) =>
|
||||||
({ theme }: { theme: "light" | "dark" }) =>
|
|
||||||
createIcon(
|
createIcon(
|
||||||
<>
|
<>
|
||||||
<path
|
<path
|
||||||
@@ -193,8 +197,7 @@ export const BringForwardIcon = React.memo(
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
export const SendBackwardIcon = React.memo(
|
export const SendBackwardIcon = React.memo(({ theme }: { theme: Theme }) =>
|
||||||
({ theme }: { theme: "light" | "dark" }) =>
|
|
||||||
createIcon(
|
createIcon(
|
||||||
<>
|
<>
|
||||||
<path
|
<path
|
||||||
@@ -214,8 +217,7 @@ export const SendBackwardIcon = React.memo(
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
export const BringToFrontIcon = React.memo(
|
export const BringToFrontIcon = React.memo(({ theme }: { theme: Theme }) =>
|
||||||
({ theme }: { theme: "light" | "dark" }) =>
|
|
||||||
createIcon(
|
createIcon(
|
||||||
<>
|
<>
|
||||||
<path
|
<path
|
||||||
@@ -235,8 +237,7 @@ export const BringToFrontIcon = React.memo(
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
export const SendToBackIcon = React.memo(
|
export const SendToBackIcon = React.memo(({ theme }: { theme: Theme }) =>
|
||||||
({ theme }: { theme: "light" | "dark" }) =>
|
|
||||||
createIcon(
|
createIcon(
|
||||||
<>
|
<>
|
||||||
<path
|
<path
|
||||||
@@ -262,8 +263,7 @@ export const SendToBackIcon = React.memo(
|
|||||||
// first one the user sees. Horizontal align icons should not be flipped since
|
// first one the user sees. Horizontal align icons should not be flipped since
|
||||||
// that would make them lie about their function.
|
// that would make them lie about their function.
|
||||||
//
|
//
|
||||||
export const AlignTopIcon = React.memo(
|
export const AlignTopIcon = React.memo(({ theme }: { theme: Theme }) =>
|
||||||
({ theme }: { theme: "light" | "dark" }) =>
|
|
||||||
createIcon(
|
createIcon(
|
||||||
<>
|
<>
|
||||||
<path
|
<path
|
||||||
@@ -284,8 +284,7 @@ export const AlignTopIcon = React.memo(
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
export const AlignBottomIcon = React.memo(
|
export const AlignBottomIcon = React.memo(({ theme }: { theme: Theme }) =>
|
||||||
({ theme }: { theme: "light" | "dark" }) =>
|
|
||||||
createIcon(
|
createIcon(
|
||||||
<>
|
<>
|
||||||
<path
|
<path
|
||||||
@@ -306,8 +305,7 @@ export const AlignBottomIcon = React.memo(
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
export const AlignLeftIcon = React.memo(
|
export const AlignLeftIcon = React.memo(({ theme }: { theme: Theme }) =>
|
||||||
({ theme }: { theme: "light" | "dark" }) =>
|
|
||||||
createIcon(
|
createIcon(
|
||||||
<>
|
<>
|
||||||
<path
|
<path
|
||||||
@@ -328,8 +326,7 @@ export const AlignLeftIcon = React.memo(
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
export const AlignRightIcon = React.memo(
|
export const AlignRightIcon = React.memo(({ theme }: { theme: Theme }) =>
|
||||||
({ theme }: { theme: "light" | "dark" }) =>
|
|
||||||
createIcon(
|
createIcon(
|
||||||
<>
|
<>
|
||||||
<path
|
<path
|
||||||
@@ -351,7 +348,7 @@ export const AlignRightIcon = React.memo(
|
|||||||
);
|
);
|
||||||
|
|
||||||
export const DistributeHorizontallyIcon = React.memo(
|
export const DistributeHorizontallyIcon = React.memo(
|
||||||
({ theme }: { theme: "light" | "dark" }) =>
|
({ theme }: { theme: Theme }) =>
|
||||||
createIcon(
|
createIcon(
|
||||||
<>
|
<>
|
||||||
<path
|
<path
|
||||||
@@ -372,7 +369,7 @@ export const DistributeHorizontallyIcon = React.memo(
|
|||||||
);
|
);
|
||||||
|
|
||||||
export const DistributeVerticallyIcon = React.memo(
|
export const DistributeVerticallyIcon = React.memo(
|
||||||
({ theme }: { theme: "light" | "dark" }) =>
|
({ theme }: { theme: Theme }) =>
|
||||||
createIcon(
|
createIcon(
|
||||||
<>
|
<>
|
||||||
<path
|
<path
|
||||||
@@ -393,8 +390,7 @@ export const DistributeVerticallyIcon = React.memo(
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
export const CenterVerticallyIcon = React.memo(
|
export const CenterVerticallyIcon = React.memo(({ theme }: { theme: Theme }) =>
|
||||||
({ theme }: { theme: "light" | "dark" }) =>
|
|
||||||
createIcon(
|
createIcon(
|
||||||
<>
|
<>
|
||||||
<path
|
<path
|
||||||
@@ -417,7 +413,7 @@ export const CenterVerticallyIcon = React.memo(
|
|||||||
);
|
);
|
||||||
|
|
||||||
export const CenterHorizontallyIcon = React.memo(
|
export const CenterHorizontallyIcon = React.memo(
|
||||||
({ theme }: { theme: "light" | "dark" }) =>
|
({ theme }: { theme: Theme }) =>
|
||||||
createIcon(
|
createIcon(
|
||||||
<>
|
<>
|
||||||
<path
|
<path
|
||||||
@@ -474,7 +470,12 @@ export const shield = createIcon(
|
|||||||
{ width: 24 },
|
{ width: 24 },
|
||||||
);
|
);
|
||||||
|
|
||||||
export const GroupIcon = React.memo(({ theme }: { theme: "light" | "dark" }) =>
|
export const file = createIcon(
|
||||||
|
"M369.9 97.9L286 14C277 5 264.8-.1 252.1-.1H48C21.5 0 0 21.5 0 48v416c0 26.5 21.5 48 48 48h288c26.5 0 48-21.5 48-48V131.9c0-12.7-5.1-25-14.1-34zM332.1 128H256V51.9l76.1 76.1zM48 464V48h160v104c0 13.3 10.7 24 24 24h104v288H48zm32-48h224V288l-23.5-23.5c-4.7-4.7-12.3-4.7-17 0L176 352l-39.5-39.5c-4.7-4.7-12.3-4.7-17 0L80 352v64zm48-240c-26.5 0-48 21.5-48 48s21.5 48 48 48 48-21.5 48-48-21.5-48-48-48z",
|
||||||
|
{ width: 384, height: 512 },
|
||||||
|
);
|
||||||
|
|
||||||
|
export const GroupIcon = React.memo(({ theme }: { theme: Theme }) =>
|
||||||
createIcon(
|
createIcon(
|
||||||
<>
|
<>
|
||||||
<path d="M25 26H111V111H25" fill={iconFillColor(theme)} />
|
<path d="M25 26H111V111H25" fill={iconFillColor(theme)} />
|
||||||
@@ -504,8 +505,7 @@ export const GroupIcon = React.memo(({ theme }: { theme: "light" | "dark" }) =>
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
export const UngroupIcon = React.memo(
|
export const UngroupIcon = React.memo(({ theme }: { theme: Theme }) =>
|
||||||
({ theme }: { theme: "light" | "dark" }) =>
|
|
||||||
createIcon(
|
createIcon(
|
||||||
<>
|
<>
|
||||||
<path d="M25 26H111V111H25" fill={iconFillColor(theme)} />
|
<path d="M25 26H111V111H25" fill={iconFillColor(theme)} />
|
||||||
@@ -537,8 +537,7 @@ export const UngroupIcon = React.memo(
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
export const FillHachureIcon = React.memo(
|
export const FillHachureIcon = React.memo(({ theme }: { theme: Theme }) =>
|
||||||
({ theme }: { theme: "light" | "dark" }) =>
|
|
||||||
createIcon(
|
createIcon(
|
||||||
<path
|
<path
|
||||||
fillRule="evenodd"
|
fillRule="evenodd"
|
||||||
@@ -550,8 +549,7 @@ export const FillHachureIcon = React.memo(
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
export const FillCrossHatchIcon = React.memo(
|
export const FillCrossHatchIcon = React.memo(({ theme }: { theme: Theme }) =>
|
||||||
({ theme }: { theme: "light" | "dark" }) =>
|
|
||||||
createIcon(
|
createIcon(
|
||||||
<g fill={iconFillColor(theme)} fillRule="evenodd" clipRule="evenodd">
|
<g fill={iconFillColor(theme)} fillRule="evenodd" clipRule="evenodd">
|
||||||
<path d="M20.101 16H28.0934L36 8.95989V4H33.5779L20.101 16ZM30.5704 4L17.0935 16H9.10101L22.5779 4H30.5704ZM19.5704 4L6.09349 16H4V10.7475L11.5779 4H19.5704ZM8.57036 4H4V8.06952L8.57036 4ZM36 11.6378L31.101 16H36V11.6378ZM2 2V18H38V2H2Z" />
|
<path d="M20.101 16H28.0934L36 8.95989V4H33.5779L20.101 16ZM30.5704 4L17.0935 16H9.10101L22.5779 4H30.5704ZM19.5704 4L6.09349 16H4V10.7475L11.5779 4H19.5704ZM8.57036 4H4V8.06952L8.57036 4ZM36 11.6378L31.101 16H36V11.6378ZM2 2V18H38V2H2Z" />
|
||||||
@@ -561,8 +559,7 @@ export const FillCrossHatchIcon = React.memo(
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
export const FillSolidIcon = React.memo(
|
export const FillSolidIcon = React.memo(({ theme }: { theme: Theme }) =>
|
||||||
({ theme }: { theme: "light" | "dark" }) =>
|
|
||||||
createIcon(<path d="M2 2H38V18H2V2Z" fill={iconFillColor(theme)} />, {
|
createIcon(<path d="M2 2H38V18H2V2Z" fill={iconFillColor(theme)} />, {
|
||||||
width: 40,
|
width: 40,
|
||||||
height: 20,
|
height: 20,
|
||||||
@@ -570,7 +567,7 @@ export const FillSolidIcon = React.memo(
|
|||||||
);
|
);
|
||||||
|
|
||||||
export const StrokeWidthIcon = React.memo(
|
export const StrokeWidthIcon = React.memo(
|
||||||
({ theme, strokeWidth }: { theme: "light" | "dark"; strokeWidth: number }) =>
|
({ theme, strokeWidth }: { theme: Theme; strokeWidth: number }) =>
|
||||||
createIcon(
|
createIcon(
|
||||||
<path
|
<path
|
||||||
d="M6 10H32"
|
d="M6 10H32"
|
||||||
@@ -583,8 +580,7 @@ export const StrokeWidthIcon = React.memo(
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
export const StrokeStyleSolidIcon = React.memo(
|
export const StrokeStyleSolidIcon = React.memo(({ theme }: { theme: Theme }) =>
|
||||||
({ theme }: { theme: "light" | "dark" }) =>
|
|
||||||
createIcon(
|
createIcon(
|
||||||
<path
|
<path
|
||||||
d="M6 10H34"
|
d="M6 10H34"
|
||||||
@@ -600,8 +596,7 @@ export const StrokeStyleSolidIcon = React.memo(
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
export const StrokeStyleDashedIcon = React.memo(
|
export const StrokeStyleDashedIcon = React.memo(({ theme }: { theme: Theme }) =>
|
||||||
({ theme }: { theme: "light" | "dark" }) =>
|
|
||||||
createIcon(
|
createIcon(
|
||||||
<path
|
<path
|
||||||
d="M6 10H34"
|
d="M6 10H34"
|
||||||
@@ -615,8 +610,7 @@ export const StrokeStyleDashedIcon = React.memo(
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
export const StrokeStyleDottedIcon = React.memo(
|
export const StrokeStyleDottedIcon = React.memo(({ theme }: { theme: Theme }) =>
|
||||||
({ theme }: { theme: "light" | "dark" }) =>
|
|
||||||
createIcon(
|
createIcon(
|
||||||
<path
|
<path
|
||||||
d="M6 10H36"
|
d="M6 10H36"
|
||||||
@@ -631,7 +625,7 @@ export const StrokeStyleDottedIcon = React.memo(
|
|||||||
);
|
);
|
||||||
|
|
||||||
export const SloppinessArchitectIcon = React.memo(
|
export const SloppinessArchitectIcon = React.memo(
|
||||||
({ theme }: { theme: "light" | "dark" }) =>
|
({ theme }: { theme: Theme }) =>
|
||||||
createIcon(
|
createIcon(
|
||||||
<path
|
<path
|
||||||
d="M3.00098 16.1691C6.28774 13.9744 19.6399 2.8905 22.7215 3.00082C25.8041 3.11113 19.1158 15.5488 21.4962 16.8309C23.8757 18.1131 34.4155 11.7148 37.0001 10.6919"
|
d="M3.00098 16.1691C6.28774 13.9744 19.6399 2.8905 22.7215 3.00082C25.8041 3.11113 19.1158 15.5488 21.4962 16.8309C23.8757 18.1131 34.4155 11.7148 37.0001 10.6919"
|
||||||
@@ -644,8 +638,7 @@ export const SloppinessArchitectIcon = React.memo(
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
export const SloppinessArtistIcon = React.memo(
|
export const SloppinessArtistIcon = React.memo(({ theme }: { theme: Theme }) =>
|
||||||
({ theme }: { theme: "light" | "dark" }) =>
|
|
||||||
createIcon(
|
createIcon(
|
||||||
<path
|
<path
|
||||||
d="M3 17C6.68158 14.8752 16.1296 9.09849 22.0648 6.54922C28 3.99995 22.2896 13.3209 25 14C27.7104 14.6791 36.3757 9.6471 36.3757 9.6471M6.40706 15C13 11.1918 20.0468 1.51045 23.0234 3.0052C26 4.49995 20.457 12.8659 22.7285 16.4329C25 20 36.3757 13 36.3757 13"
|
d="M3 17C6.68158 14.8752 16.1296 9.09849 22.0648 6.54922C28 3.99995 22.2896 13.3209 25 14C27.7104 14.6791 36.3757 9.6471 36.3757 9.6471M6.40706 15C13 11.1918 20.0468 1.51045 23.0234 3.0052C26 4.49995 20.457 12.8659 22.7285 16.4329C25 20 36.3757 13 36.3757 13"
|
||||||
@@ -659,7 +652,7 @@ export const SloppinessArtistIcon = React.memo(
|
|||||||
);
|
);
|
||||||
|
|
||||||
export const SloppinessCartoonistIcon = React.memo(
|
export const SloppinessCartoonistIcon = React.memo(
|
||||||
({ theme }: { theme: "light" | "dark" }) =>
|
({ theme }: { theme: Theme }) =>
|
||||||
createIcon(
|
createIcon(
|
||||||
<path
|
<path
|
||||||
d="M3 15.6468C6.93692 13.5378 22.5544 2.81528 26.6206 3.00242C30.6877 3.18956 25.6708 15.3346 27.4009 16.7705C29.1309 18.2055 35.4001 12.4762 37 11.6177M3.97143 10.4917C6.61158 9.24563 16.3706 2.61886 19.8104 3.01724C23.2522 3.41472 22.0773 12.2013 24.6181 12.8783C27.1598 13.5536 33.3179 8.04068 35.0571 7.07244"
|
d="M3 15.6468C6.93692 13.5378 22.5544 2.81528 26.6206 3.00242C30.6877 3.18956 25.6708 15.3346 27.4009 16.7705C29.1309 18.2055 35.4001 12.4762 37 11.6177M3.97143 10.4917C6.61158 9.24563 16.3706 2.61886 19.8104 3.01724C23.2522 3.41472 22.0773 12.2013 24.6181 12.8783C27.1598 13.5536 33.3179 8.04068 35.0571 7.07244"
|
||||||
@@ -672,8 +665,7 @@ export const SloppinessCartoonistIcon = React.memo(
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
export const EdgeSharpIcon = React.memo(
|
export const EdgeSharpIcon = React.memo(({ theme }: { theme: Theme }) =>
|
||||||
({ theme }: { theme: "light" | "dark" }) =>
|
|
||||||
createIcon(
|
createIcon(
|
||||||
<path
|
<path
|
||||||
d="M10 17L10 5L35 5"
|
d="M10 17L10 5L35 5"
|
||||||
@@ -686,8 +678,7 @@ export const EdgeSharpIcon = React.memo(
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
export const EdgeRoundIcon = React.memo(
|
export const EdgeRoundIcon = React.memo(({ theme }: { theme: Theme }) =>
|
||||||
({ theme }: { theme: "light" | "dark" }) =>
|
|
||||||
createIcon(
|
createIcon(
|
||||||
<path
|
<path
|
||||||
d="M10 17V15C10 8 13 5 21 5L33.5 5"
|
d="M10 17V15C10 8 13 5 21 5L33.5 5"
|
||||||
@@ -700,8 +691,7 @@ export const EdgeRoundIcon = React.memo(
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
export const ArrowheadNoneIcon = React.memo(
|
export const ArrowheadNoneIcon = React.memo(({ theme }: { theme: Theme }) =>
|
||||||
({ theme }: { theme: "light" | "dark" }) =>
|
|
||||||
createIcon(
|
createIcon(
|
||||||
<path
|
<path
|
||||||
d="M6 10H34"
|
d="M6 10H34"
|
||||||
@@ -717,7 +707,7 @@ export const ArrowheadNoneIcon = React.memo(
|
|||||||
);
|
);
|
||||||
|
|
||||||
export const ArrowheadArrowIcon = React.memo(
|
export const ArrowheadArrowIcon = React.memo(
|
||||||
({ theme, flip = false }: { theme: "light" | "dark"; flip?: boolean }) =>
|
({ theme, flip = false }: { theme: Theme; flip?: boolean }) =>
|
||||||
createIcon(
|
createIcon(
|
||||||
<g
|
<g
|
||||||
transform={flip ? "translate(40, 0) scale(-1, 1)" : ""}
|
transform={flip ? "translate(40, 0) scale(-1, 1)" : ""}
|
||||||
@@ -733,7 +723,7 @@ export const ArrowheadArrowIcon = React.memo(
|
|||||||
);
|
);
|
||||||
|
|
||||||
export const ArrowheadDotIcon = React.memo(
|
export const ArrowheadDotIcon = React.memo(
|
||||||
({ theme, flip = false }: { theme: "light" | "dark"; flip?: boolean }) =>
|
({ theme, flip = false }: { theme: Theme; flip?: boolean }) =>
|
||||||
createIcon(
|
createIcon(
|
||||||
<g
|
<g
|
||||||
stroke={iconFillColor(theme)}
|
stroke={iconFillColor(theme)}
|
||||||
@@ -748,7 +738,7 @@ export const ArrowheadDotIcon = React.memo(
|
|||||||
);
|
);
|
||||||
|
|
||||||
export const ArrowheadBarIcon = React.memo(
|
export const ArrowheadBarIcon = React.memo(
|
||||||
({ theme, flip = false }: { theme: "light" | "dark"; flip?: boolean }) =>
|
({ theme, flip = false }: { theme: Theme; flip?: boolean }) =>
|
||||||
createIcon(
|
createIcon(
|
||||||
<g transform={flip ? "translate(40, 0) scale(-1, 1)" : ""}>
|
<g transform={flip ? "translate(40, 0) scale(-1, 1)" : ""}>
|
||||||
<path
|
<path
|
||||||
@@ -762,8 +752,7 @@ export const ArrowheadBarIcon = React.memo(
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
export const FontSizeSmallIcon = React.memo(
|
export const FontSizeSmallIcon = React.memo(({ theme }: { theme: Theme }) =>
|
||||||
({ theme }: { theme: "light" | "dark" }) =>
|
|
||||||
createIcon(
|
createIcon(
|
||||||
<path
|
<path
|
||||||
fill={iconFillColor(theme)}
|
fill={iconFillColor(theme)}
|
||||||
@@ -773,8 +762,7 @@ export const FontSizeSmallIcon = React.memo(
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
export const FontSizeMediumIcon = React.memo(
|
export const FontSizeMediumIcon = React.memo(({ theme }: { theme: Theme }) =>
|
||||||
({ theme }: { theme: "light" | "dark" }) =>
|
|
||||||
createIcon(
|
createIcon(
|
||||||
<path
|
<path
|
||||||
fill={iconFillColor(theme)}
|
fill={iconFillColor(theme)}
|
||||||
@@ -784,8 +772,7 @@ export const FontSizeMediumIcon = React.memo(
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
export const FontSizeLargeIcon = React.memo(
|
export const FontSizeLargeIcon = React.memo(({ theme }: { theme: Theme }) =>
|
||||||
({ theme }: { theme: "light" | "dark" }) =>
|
|
||||||
createIcon(
|
createIcon(
|
||||||
<path
|
<path
|
||||||
fill={iconFillColor(theme)}
|
fill={iconFillColor(theme)}
|
||||||
@@ -796,7 +783,7 @@ export const FontSizeLargeIcon = React.memo(
|
|||||||
);
|
);
|
||||||
|
|
||||||
export const FontSizeExtraLargeIcon = React.memo(
|
export const FontSizeExtraLargeIcon = React.memo(
|
||||||
({ theme }: { theme: "light" | "dark" }) =>
|
({ theme }: { theme: Theme }) =>
|
||||||
createIcon(
|
createIcon(
|
||||||
<path
|
<path
|
||||||
fill={iconFillColor(theme)}
|
fill={iconFillColor(theme)}
|
||||||
@@ -807,7 +794,7 @@ export const FontSizeExtraLargeIcon = React.memo(
|
|||||||
);
|
);
|
||||||
|
|
||||||
export const FontFamilyHandDrawnIcon = React.memo(
|
export const FontFamilyHandDrawnIcon = React.memo(
|
||||||
({ theme }: { theme: "light" | "dark" }) =>
|
({ theme }: { theme: Theme }) =>
|
||||||
createIcon(
|
createIcon(
|
||||||
<path
|
<path
|
||||||
fill={iconFillColor(theme)}
|
fill={iconFillColor(theme)}
|
||||||
@@ -817,8 +804,7 @@ export const FontFamilyHandDrawnIcon = React.memo(
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
export const FontFamilyNormalIcon = React.memo(
|
export const FontFamilyNormalIcon = React.memo(({ theme }: { theme: Theme }) =>
|
||||||
({ theme }: { theme: "light" | "dark" }) =>
|
|
||||||
createIcon(
|
createIcon(
|
||||||
<>
|
<>
|
||||||
<path
|
<path
|
||||||
@@ -834,8 +820,7 @@ export const FontFamilyNormalIcon = React.memo(
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
export const FontFamilyCodeIcon = React.memo(
|
export const FontFamilyCodeIcon = React.memo(({ theme }: { theme: Theme }) =>
|
||||||
({ theme }: { theme: "light" | "dark" }) =>
|
|
||||||
createIcon(
|
createIcon(
|
||||||
<>
|
<>
|
||||||
<path
|
<path
|
||||||
@@ -847,8 +832,7 @@ export const FontFamilyCodeIcon = React.memo(
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
export const TextAlignLeftIcon = React.memo(
|
export const TextAlignLeftIcon = React.memo(({ theme }: { theme: Theme }) =>
|
||||||
({ theme }: { theme: "light" | "dark" }) =>
|
|
||||||
createIcon(
|
createIcon(
|
||||||
<path
|
<path
|
||||||
d="M12.83 352h262.34A12.82 12.82 0 00288 339.17v-38.34A12.82 12.82 0 00275.17 288H12.83A12.82 12.82 0 000 300.83v38.34A12.82 12.82 0 0012.83 352zm0-256h262.34A12.82 12.82 0 00288 83.17V44.83A12.82 12.82 0 00275.17 32H12.83A12.82 12.82 0 000 44.83v38.34A12.82 12.82 0 0012.83 96zM432 160H16a16 16 0 00-16 16v32a16 16 0 0016 16h416a16 16 0 0016-16v-32a16 16 0 00-16-16zm0 256H16a16 16 0 00-16 16v32a16 16 0 0016 16h416a16 16 0 0016-16v-32a16 16 0 00-16-16z"
|
d="M12.83 352h262.34A12.82 12.82 0 00288 339.17v-38.34A12.82 12.82 0 00275.17 288H12.83A12.82 12.82 0 000 300.83v38.34A12.82 12.82 0 0012.83 352zm0-256h262.34A12.82 12.82 0 00288 83.17V44.83A12.82 12.82 0 00275.17 32H12.83A12.82 12.82 0 000 44.83v38.34A12.82 12.82 0 0012.83 96zM432 160H16a16 16 0 00-16 16v32a16 16 0 0016 16h416a16 16 0 0016-16v-32a16 16 0 00-16-16zm0 256H16a16 16 0 00-16 16v32a16 16 0 0016 16h416a16 16 0 0016-16v-32a16 16 0 00-16-16z"
|
||||||
@@ -859,8 +843,7 @@ export const TextAlignLeftIcon = React.memo(
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
export const TextAlignCenterIcon = React.memo(
|
export const TextAlignCenterIcon = React.memo(({ theme }: { theme: Theme }) =>
|
||||||
({ theme }: { theme: "light" | "dark" }) =>
|
|
||||||
createIcon(
|
createIcon(
|
||||||
<path
|
<path
|
||||||
d="M432 160H16a16 16 0 00-16 16v32a16 16 0 0016 16h416a16 16 0 0016-16v-32a16 16 0 00-16-16zm0 256H16a16 16 0 00-16 16v32a16 16 0 0016 16h416a16 16 0 0016-16v-32a16 16 0 00-16-16zM108.1 96h231.81A12.09 12.09 0 00352 83.9V44.09A12.09 12.09 0 00339.91 32H108.1A12.09 12.09 0 0096 44.09V83.9A12.1 12.1 0 00108.1 96zm231.81 256A12.09 12.09 0 00352 339.9v-39.81A12.09 12.09 0 00339.91 288H108.1A12.09 12.09 0 0096 300.09v39.81a12.1 12.1 0 0012.1 12.1z"
|
d="M432 160H16a16 16 0 00-16 16v32a16 16 0 0016 16h416a16 16 0 0016-16v-32a16 16 0 00-16-16zm0 256H16a16 16 0 00-16 16v32a16 16 0 0016 16h416a16 16 0 0016-16v-32a16 16 0 00-16-16zM108.1 96h231.81A12.09 12.09 0 00352 83.9V44.09A12.09 12.09 0 00339.91 32H108.1A12.09 12.09 0 0096 44.09V83.9A12.1 12.1 0 00108.1 96zm231.81 256A12.09 12.09 0 00352 339.9v-39.81A12.09 12.09 0 00339.91 288H108.1A12.09 12.09 0 0096 300.09v39.81a12.1 12.1 0 0012.1 12.1z"
|
||||||
@@ -870,8 +853,7 @@ export const TextAlignCenterIcon = React.memo(
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
export const TextAlignRightIcon = React.memo(
|
export const TextAlignRightIcon = React.memo(({ theme }: { theme: Theme }) =>
|
||||||
({ theme }: { theme: "light" | "dark" }) =>
|
|
||||||
createIcon(
|
createIcon(
|
||||||
<path
|
<path
|
||||||
d="M16 224h416a16 16 0 0016-16v-32a16 16 0 00-16-16H16a16 16 0 00-16 16v32a16 16 0 0016 16zm416 192H16a16 16 0 00-16 16v32a16 16 0 0016 16h416a16 16 0 0016-16v-32a16 16 0 00-16-16zm3.17-384H172.83A12.82 12.82 0 00160 44.83v38.34A12.82 12.82 0 00172.83 96h262.34A12.82 12.82 0 00448 83.17V44.83A12.82 12.82 0 00435.17 32zm0 256H172.83A12.82 12.82 0 00160 300.83v38.34A12.82 12.82 0 00172.83 352h262.34A12.82 12.82 0 00448 339.17v-38.34A12.82 12.82 0 00435.17 288z"
|
d="M16 224h416a16 16 0 0016-16v-32a16 16 0 00-16-16H16a16 16 0 00-16 16v32a16 16 0 0016 16zm416 192H16a16 16 0 00-16 16v32a16 16 0 0016 16h416a16 16 0 0016-16v-32a16 16 0 00-16-16zm3.17-384H172.83A12.82 12.82 0 00160 44.83v38.34A12.82 12.82 0 00172.83 96h262.34A12.82 12.82 0 00448 83.17V44.83A12.82 12.82 0 00435.17 32zm0 256H172.83A12.82 12.82 0 00160 300.83v38.34A12.82 12.82 0 00172.83 352h262.34A12.82 12.82 0 00448 339.17v-38.34A12.82 12.82 0 00435.17 288z"
|
||||||
|
|||||||
+17
-7
@@ -1,6 +1,6 @@
|
|||||||
import { FontFamily } from "./element/types";
|
|
||||||
import cssVariables from "./css/variables.module.scss";
|
import cssVariables from "./css/variables.module.scss";
|
||||||
import { AppProps } from "./types";
|
import { AppProps } from "./types";
|
||||||
|
import { FontFamilyValues } from "./element/types";
|
||||||
|
|
||||||
export const APP_NAME = "Excalidraw";
|
export const APP_NAME = "Excalidraw";
|
||||||
|
|
||||||
@@ -14,6 +14,7 @@ export const CURSOR_TYPE = {
|
|||||||
TEXT: "text",
|
TEXT: "text",
|
||||||
CROSSHAIR: "crosshair",
|
CROSSHAIR: "crosshair",
|
||||||
GRABBING: "grabbing",
|
GRABBING: "grabbing",
|
||||||
|
GRAB: "grab",
|
||||||
POINTER: "pointer",
|
POINTER: "pointer",
|
||||||
MOVE: "move",
|
MOVE: "move",
|
||||||
AUTO: "",
|
AUTO: "",
|
||||||
@@ -34,6 +35,7 @@ export enum EVENT {
|
|||||||
MOUSE_MOVE = "mousemove",
|
MOUSE_MOVE = "mousemove",
|
||||||
RESIZE = "resize",
|
RESIZE = "resize",
|
||||||
UNLOAD = "unload",
|
UNLOAD = "unload",
|
||||||
|
FOCUS = "focus",
|
||||||
BLUR = "blur",
|
BLUR = "blur",
|
||||||
DRAG_OVER = "dragover",
|
DRAG_OVER = "dragover",
|
||||||
DROP = "drop",
|
DROP = "drop",
|
||||||
@@ -63,15 +65,20 @@ export const CLASSES = {
|
|||||||
|
|
||||||
// 1-based in case we ever do `if(element.fontFamily)`
|
// 1-based in case we ever do `if(element.fontFamily)`
|
||||||
export const FONT_FAMILY = {
|
export const FONT_FAMILY = {
|
||||||
1: "Virgil",
|
Virgil: 1,
|
||||||
2: "Helvetica",
|
Helvetica: 2,
|
||||||
3: "Cascadia",
|
Cascadia: 3,
|
||||||
} as const;
|
};
|
||||||
|
|
||||||
|
export const THEME = {
|
||||||
|
LIGHT: "light",
|
||||||
|
DARK: "dark",
|
||||||
|
};
|
||||||
|
|
||||||
export const WINDOWS_EMOJI_FALLBACK_FONT = "Segoe UI Emoji";
|
export const WINDOWS_EMOJI_FALLBACK_FONT = "Segoe UI Emoji";
|
||||||
|
|
||||||
export const DEFAULT_FONT_SIZE = 20;
|
export const DEFAULT_FONT_SIZE = 20;
|
||||||
export const DEFAULT_FONT_FAMILY: FontFamily = 1;
|
export const DEFAULT_FONT_FAMILY: FontFamilyValues = FONT_FAMILY.Virgil;
|
||||||
export const DEFAULT_TEXT_ALIGN = "left";
|
export const DEFAULT_TEXT_ALIGN = "left";
|
||||||
export const DEFAULT_VERTICAL_ALIGN = "top";
|
export const DEFAULT_VERTICAL_ALIGN = "top";
|
||||||
export const DEFAULT_VERSION = "{version}";
|
export const DEFAULT_VERSION = "{version}";
|
||||||
@@ -83,7 +90,7 @@ export const GRID_SIZE = 20; // TODO make it configurable?
|
|||||||
export const MIME_TYPES = {
|
export const MIME_TYPES = {
|
||||||
excalidraw: "application/vnd.excalidraw+json",
|
excalidraw: "application/vnd.excalidraw+json",
|
||||||
excalidrawlib: "application/vnd.excalidrawlib+json",
|
excalidrawlib: "application/vnd.excalidrawlib+json",
|
||||||
};
|
} as const;
|
||||||
|
|
||||||
export const EXPORT_DATA_TYPES = {
|
export const EXPORT_DATA_TYPES = {
|
||||||
excalidraw: "excalidraw",
|
excalidraw: "excalidraw",
|
||||||
@@ -144,3 +151,6 @@ export const MQ_MAX_WIDTH_LANDSCAPE = 1000;
|
|||||||
export const MQ_MAX_HEIGHT_LANDSCAPE = 500;
|
export const MQ_MAX_HEIGHT_LANDSCAPE = 500;
|
||||||
|
|
||||||
export const MAX_DECIMALS_FOR_SVG_EXPORT = 2;
|
export const MAX_DECIMALS_FOR_SVG_EXPORT = 2;
|
||||||
|
|
||||||
|
export const EXPORT_SCALES = [1, 2, 3];
|
||||||
|
export const DEFAULT_EXPORT_PADDING = 10; // px
|
||||||
|
|||||||
+22
-25
@@ -51,11 +51,12 @@
|
|||||||
image-rendering: -moz-crisp-edges; // FF
|
image-rendering: -moz-crisp-edges; // FF
|
||||||
|
|
||||||
z-index: var(--zIndex-canvas);
|
z-index: var(--zIndex-canvas);
|
||||||
}
|
|
||||||
|
|
||||||
#canvas {
|
|
||||||
// Remove the main canvas from document flow to avoid resizeObserver
|
// Remove the main canvas from document flow to avoid resizeObserver
|
||||||
// feedback loop (see https://github.com/excalidraw/excalidraw/pull/3379)
|
// feedback loop (see https://github.com/excalidraw/excalidraw/pull/3379)
|
||||||
|
}
|
||||||
|
|
||||||
|
&__canvas {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -413,22 +414,6 @@
|
|||||||
&:active {
|
&:active {
|
||||||
background-color: var(--button-gray-2);
|
background-color: var(--button-gray-2);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.dropdown-select--floating {
|
|
||||||
margin: 0.5em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown-select__language.dropdown-select--floating {
|
|
||||||
bottom: 10px;
|
|
||||||
|
|
||||||
:root[dir="ltr"] & {
|
|
||||||
right: 44px;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[dir="rtl"] & {
|
|
||||||
left: 44px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.zIndexButton {
|
.zIndexButton {
|
||||||
@@ -455,26 +440,38 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.help-icon {
|
.help-icon {
|
||||||
|
display: flex;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
fill: $oc-gray-6;
|
fill: $oc-gray-6;
|
||||||
width: 1.5rem;
|
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
margin-top: 5px;
|
|
||||||
background: none;
|
background: none;
|
||||||
color: var(--icon-fill-color);
|
color: var(--icon-fill-color);
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 1.5rem;
|
||||||
|
height: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: none;
|
background: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
:root[dir="ltr"] & {
|
|
||||||
margin-right: 14px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
:root[dir="rtl"] & {
|
.reset-zoom-button {
|
||||||
margin-left: 14px;
|
padding: 0.2em;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--text-primary-color);
|
||||||
|
font-family: var(--ui-font);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.undo-redo-buttons {
|
||||||
|
display: grid;
|
||||||
|
grid-auto-flow: column;
|
||||||
|
gap: 0.4em;
|
||||||
|
margin-top: auto;
|
||||||
|
margin-bottom: auto;
|
||||||
|
margin-inline-start: 0.6em;
|
||||||
}
|
}
|
||||||
|
|
||||||
@include isMobile {
|
@include isMobile {
|
||||||
|
|||||||
+24
-1
@@ -1,10 +1,12 @@
|
|||||||
import { cleanAppStateForExport } from "../appState";
|
import { cleanAppStateForExport } from "../appState";
|
||||||
import { EXPORT_DATA_TYPES } from "../constants";
|
import { EXPORT_DATA_TYPES } from "../constants";
|
||||||
import { clearElementsForExport } from "../element";
|
import { clearElementsForExport } from "../element";
|
||||||
|
import { ExcalidrawElement } from "../element/types";
|
||||||
import { CanvasError } from "../errors";
|
import { CanvasError } from "../errors";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { calculateScrollCenter } from "../scene";
|
import { calculateScrollCenter } from "../scene";
|
||||||
import { AppState } from "../types";
|
import { AppState } from "../types";
|
||||||
|
import { FileSystemHandle } from "./filesystem";
|
||||||
import { isValidExcalidrawData } from "./json";
|
import { isValidExcalidrawData } from "./json";
|
||||||
import { restore } from "./restore";
|
import { restore } from "./restore";
|
||||||
import { ImportedLibraryData } from "./types";
|
import { ImportedLibraryData } from "./types";
|
||||||
@@ -79,10 +81,30 @@ export const getMimeType = (blob: Blob | string): string => {
|
|||||||
return "";
|
return "";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getFileHandleType = (handle: FileSystemHandle | null) => {
|
||||||
|
if (!handle) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return handle.name.match(/\.(json|excalidraw|png|svg)$/)?.[1] || null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isImageFileHandleType = (
|
||||||
|
type: string | null,
|
||||||
|
): type is "png" | "svg" => {
|
||||||
|
return type === "png" || type === "svg";
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isImageFileHandle = (handle: FileSystemHandle | null) => {
|
||||||
|
const type = getFileHandleType(handle);
|
||||||
|
return type === "png" || type === "svg";
|
||||||
|
};
|
||||||
|
|
||||||
export const loadFromBlob = async (
|
export const loadFromBlob = async (
|
||||||
blob: Blob,
|
blob: Blob,
|
||||||
/** @see restore.localAppState */
|
/** @see restore.localAppState */
|
||||||
localAppState: AppState | null,
|
localAppState: AppState | null,
|
||||||
|
localElements: readonly ExcalidrawElement[] | null,
|
||||||
) => {
|
) => {
|
||||||
const contents = await parseFileContents(blob);
|
const contents = await parseFileContents(blob);
|
||||||
try {
|
try {
|
||||||
@@ -95,7 +117,7 @@ export const loadFromBlob = async (
|
|||||||
elements: clearElementsForExport(data.elements || []),
|
elements: clearElementsForExport(data.elements || []),
|
||||||
appState: {
|
appState: {
|
||||||
theme: localAppState?.theme,
|
theme: localAppState?.theme,
|
||||||
fileHandle: (!blob.type.startsWith("image/") && blob.handle) || null,
|
fileHandle: blob.handle || null,
|
||||||
...cleanAppStateForExport(data.appState || {}),
|
...cleanAppStateForExport(data.appState || {}),
|
||||||
...(localAppState
|
...(localAppState
|
||||||
? calculateScrollCenter(data.elements || [], localAppState, null)
|
? calculateScrollCenter(data.elements || [], localAppState, null)
|
||||||
@@ -103,6 +125,7 @@ export const loadFromBlob = async (
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
localAppState,
|
localAppState,
|
||||||
|
localElements,
|
||||||
);
|
);
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
@@ -0,0 +1,122 @@
|
|||||||
|
import {
|
||||||
|
FileWithHandle,
|
||||||
|
fileOpen as _fileOpen,
|
||||||
|
fileSave as _fileSave,
|
||||||
|
FileSystemHandle,
|
||||||
|
supported as nativeFileSystemSupported,
|
||||||
|
} from "@dwelle/browser-fs-access";
|
||||||
|
import { EVENT, MIME_TYPES } from "../constants";
|
||||||
|
import { AbortError } from "../errors";
|
||||||
|
import { debounce } from "../utils";
|
||||||
|
|
||||||
|
type FILE_EXTENSION =
|
||||||
|
| "jpg"
|
||||||
|
| "png"
|
||||||
|
| "svg"
|
||||||
|
| "json"
|
||||||
|
| "excalidraw"
|
||||||
|
| "excalidrawlib";
|
||||||
|
|
||||||
|
const FILE_TYPE_TO_MIME_TYPE: Record<FILE_EXTENSION, string> = {
|
||||||
|
jpg: "image/jpeg",
|
||||||
|
png: "image/png",
|
||||||
|
svg: "image/svg+xml",
|
||||||
|
json: "application/json",
|
||||||
|
excalidraw: MIME_TYPES.excalidraw,
|
||||||
|
excalidrawlib: MIME_TYPES.excalidrawlib,
|
||||||
|
};
|
||||||
|
|
||||||
|
const INPUT_CHANGE_INTERVAL_MS = 500;
|
||||||
|
|
||||||
|
export const fileOpen = <M extends boolean | undefined = false>(opts: {
|
||||||
|
extensions?: FILE_EXTENSION[];
|
||||||
|
description?: string;
|
||||||
|
multiple?: M;
|
||||||
|
}): Promise<
|
||||||
|
M extends false | undefined ? FileWithHandle : FileWithHandle[]
|
||||||
|
> => {
|
||||||
|
// an unsafe TS hack, alas not much we can do AFAIK
|
||||||
|
type RetType = M extends false | undefined
|
||||||
|
? FileWithHandle
|
||||||
|
: FileWithHandle[];
|
||||||
|
|
||||||
|
const mimeTypes = opts.extensions?.reduce((mimeTypes, type) => {
|
||||||
|
mimeTypes.push(FILE_TYPE_TO_MIME_TYPE[type]);
|
||||||
|
|
||||||
|
return mimeTypes;
|
||||||
|
}, [] as string[]);
|
||||||
|
|
||||||
|
const extensions = opts.extensions?.reduce((acc, ext) => {
|
||||||
|
if (ext === "jpg") {
|
||||||
|
return acc.concat(".jpg", ".jpeg");
|
||||||
|
}
|
||||||
|
return acc.concat(`.${ext}`);
|
||||||
|
}, [] as string[]);
|
||||||
|
|
||||||
|
return _fileOpen({
|
||||||
|
description: opts.description,
|
||||||
|
extensions,
|
||||||
|
mimeTypes,
|
||||||
|
multiple: opts.multiple ?? false,
|
||||||
|
legacySetup: (resolve, reject, input) => {
|
||||||
|
const scheduleRejection = debounce(reject, INPUT_CHANGE_INTERVAL_MS);
|
||||||
|
const focusHandler = () => {
|
||||||
|
checkForFile();
|
||||||
|
document.addEventListener(EVENT.KEYUP, scheduleRejection);
|
||||||
|
document.addEventListener(EVENT.POINTER_UP, scheduleRejection);
|
||||||
|
scheduleRejection();
|
||||||
|
};
|
||||||
|
const checkForFile = () => {
|
||||||
|
// this hack might not work when expecting multiple files
|
||||||
|
if (input.files?.length) {
|
||||||
|
const ret = opts.multiple ? [...input.files] : input.files[0];
|
||||||
|
resolve(ret as RetType);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
window.addEventListener(EVENT.FOCUS, focusHandler);
|
||||||
|
});
|
||||||
|
const interval = window.setInterval(() => {
|
||||||
|
checkForFile();
|
||||||
|
}, INPUT_CHANGE_INTERVAL_MS);
|
||||||
|
return (rejectPromise) => {
|
||||||
|
clearInterval(interval);
|
||||||
|
scheduleRejection.cancel();
|
||||||
|
window.removeEventListener(EVENT.FOCUS, focusHandler);
|
||||||
|
document.removeEventListener(EVENT.KEYUP, scheduleRejection);
|
||||||
|
document.removeEventListener(EVENT.POINTER_UP, scheduleRejection);
|
||||||
|
if (rejectPromise) {
|
||||||
|
// so that something is shown in console if we need to debug this
|
||||||
|
console.warn("Opening the file was canceled (legacy-fs).");
|
||||||
|
rejectPromise(new AbortError());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
}) as Promise<RetType>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fileSave = (
|
||||||
|
blob: Blob,
|
||||||
|
opts: {
|
||||||
|
/** supply without the extension */
|
||||||
|
name: string;
|
||||||
|
/** file extension */
|
||||||
|
extension: FILE_EXTENSION;
|
||||||
|
description?: string;
|
||||||
|
/** existing FileSystemHandle */
|
||||||
|
fileHandle?: FileSystemHandle | null;
|
||||||
|
},
|
||||||
|
) => {
|
||||||
|
return _fileSave(
|
||||||
|
blob,
|
||||||
|
{
|
||||||
|
fileName: `${opts.name}.${opts.extension}`,
|
||||||
|
description: opts.description,
|
||||||
|
extensions: [`.${opts.extension}`],
|
||||||
|
},
|
||||||
|
opts.fileHandle,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export type { FileSystemHandle };
|
||||||
|
export { nativeFileSystemSupported };
|
||||||
+21
-25
@@ -1,14 +1,15 @@
|
|||||||
import { fileSave } from "browser-fs-access";
|
|
||||||
import {
|
import {
|
||||||
copyBlobToClipboardAsPng,
|
copyBlobToClipboardAsPng,
|
||||||
copyTextToSystemClipboard,
|
copyTextToSystemClipboard,
|
||||||
} from "../clipboard";
|
} from "../clipboard";
|
||||||
|
import { DEFAULT_EXPORT_PADDING } from "../constants";
|
||||||
import { NonDeletedExcalidrawElement } from "../element/types";
|
import { NonDeletedExcalidrawElement } from "../element/types";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { exportToCanvas, exportToSvg } from "../scene/export";
|
import { exportToCanvas, exportToSvg } from "../scene/export";
|
||||||
import { ExportType } from "../scene/types";
|
import { ExportType } from "../scene/types";
|
||||||
import { AppState } from "../types";
|
import { AppState } from "../types";
|
||||||
import { canvasToBlob } from "./blob";
|
import { canvasToBlob } from "./blob";
|
||||||
|
import { fileSave, FileSystemHandle } from "./filesystem";
|
||||||
import { serializeAsJSON } from "./json";
|
import { serializeAsJSON } from "./json";
|
||||||
|
|
||||||
export { loadFromBlob } from "./blob";
|
export { loadFromBlob } from "./blob";
|
||||||
@@ -20,45 +21,41 @@ export const exportCanvas = async (
|
|||||||
appState: AppState,
|
appState: AppState,
|
||||||
{
|
{
|
||||||
exportBackground,
|
exportBackground,
|
||||||
exportPadding = 10,
|
exportPadding = DEFAULT_EXPORT_PADDING,
|
||||||
viewBackgroundColor,
|
viewBackgroundColor,
|
||||||
name,
|
name,
|
||||||
scale = 1,
|
fileHandle = null,
|
||||||
}: {
|
}: {
|
||||||
exportBackground: boolean;
|
exportBackground: boolean;
|
||||||
exportPadding?: number;
|
exportPadding?: number;
|
||||||
viewBackgroundColor: string;
|
viewBackgroundColor: string;
|
||||||
name: string;
|
name: string;
|
||||||
scale?: number;
|
fileHandle?: FileSystemHandle | null;
|
||||||
},
|
},
|
||||||
) => {
|
) => {
|
||||||
if (elements.length === 0) {
|
if (elements.length === 0) {
|
||||||
throw new Error(t("alerts.cannotExportEmptyCanvas"));
|
throw new Error(t("alerts.cannotExportEmptyCanvas"));
|
||||||
}
|
}
|
||||||
if (type === "svg" || type === "clipboard-svg") {
|
if (type === "svg" || type === "clipboard-svg") {
|
||||||
const tempSvg = exportToSvg(elements, {
|
const tempSvg = await exportToSvg(elements, {
|
||||||
exportBackground,
|
exportBackground,
|
||||||
exportWithDarkMode: appState.exportWithDarkMode,
|
exportWithDarkMode: appState.exportWithDarkMode,
|
||||||
viewBackgroundColor,
|
viewBackgroundColor,
|
||||||
exportPadding,
|
exportPadding,
|
||||||
scale,
|
exportScale: appState.exportScale,
|
||||||
metadata:
|
exportEmbedScene: appState.exportEmbedScene && type === "svg",
|
||||||
appState.exportEmbedScene && type === "svg"
|
|
||||||
? await (
|
|
||||||
await import(/* webpackChunkName: "image" */ "./image")
|
|
||||||
).encodeSvgMetadata({
|
|
||||||
text: serializeAsJSON(elements, appState),
|
|
||||||
})
|
|
||||||
: undefined,
|
|
||||||
});
|
});
|
||||||
if (type === "svg") {
|
if (type === "svg") {
|
||||||
await fileSave(new Blob([tempSvg.outerHTML], { type: "image/svg+xml" }), {
|
return await fileSave(
|
||||||
fileName: `${name}.svg`,
|
new Blob([tempSvg.outerHTML], { type: "image/svg+xml" }),
|
||||||
extensions: [".svg"],
|
{
|
||||||
});
|
name,
|
||||||
return;
|
extension: "svg",
|
||||||
|
fileHandle,
|
||||||
|
},
|
||||||
|
);
|
||||||
} else if (type === "clipboard-svg") {
|
} else if (type === "clipboard-svg") {
|
||||||
copyTextToSystemClipboard(tempSvg.outerHTML);
|
await copyTextToSystemClipboard(tempSvg.outerHTML);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -67,7 +64,6 @@ export const exportCanvas = async (
|
|||||||
exportBackground,
|
exportBackground,
|
||||||
viewBackgroundColor,
|
viewBackgroundColor,
|
||||||
exportPadding,
|
exportPadding,
|
||||||
scale,
|
|
||||||
});
|
});
|
||||||
tempCanvas.style.display = "none";
|
tempCanvas.style.display = "none";
|
||||||
document.body.appendChild(tempCanvas);
|
document.body.appendChild(tempCanvas);
|
||||||
@@ -75,7 +71,6 @@ export const exportCanvas = async (
|
|||||||
tempCanvas.remove();
|
tempCanvas.remove();
|
||||||
|
|
||||||
if (type === "png") {
|
if (type === "png") {
|
||||||
const fileName = `${name}.png`;
|
|
||||||
if (appState.exportEmbedScene) {
|
if (appState.exportEmbedScene) {
|
||||||
blob = await (
|
blob = await (
|
||||||
await import(/* webpackChunkName: "image" */ "./image")
|
await import(/* webpackChunkName: "image" */ "./image")
|
||||||
@@ -85,9 +80,10 @@ export const exportCanvas = async (
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
await fileSave(blob, {
|
return await fileSave(blob, {
|
||||||
fileName,
|
name,
|
||||||
extensions: [".png"],
|
extension: "png",
|
||||||
|
fileHandle,
|
||||||
});
|
});
|
||||||
} else if (type === "clipboard") {
|
} else if (type === "clipboard") {
|
||||||
try {
|
try {
|
||||||
|
|||||||
+23
-20
@@ -1,10 +1,10 @@
|
|||||||
import { fileOpen, fileSave } from "browser-fs-access";
|
import { fileOpen, fileSave } from "./filesystem";
|
||||||
import { cleanAppStateForExport } from "../appState";
|
import { cleanAppStateForExport } from "../appState";
|
||||||
import { EXPORT_DATA_TYPES, EXPORT_SOURCE, MIME_TYPES } from "../constants";
|
import { EXPORT_DATA_TYPES, EXPORT_SOURCE, MIME_TYPES } from "../constants";
|
||||||
import { clearElementsForExport } from "../element";
|
import { clearElementsForExport } from "../element";
|
||||||
import { ExcalidrawElement } from "../element/types";
|
import { ExcalidrawElement } from "../element/types";
|
||||||
import { AppState } from "../types";
|
import { AppState } from "../types";
|
||||||
import { loadFromBlob } from "./blob";
|
import { isImageFileHandle, loadFromBlob } from "./blob";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ExportedDataState,
|
ExportedDataState,
|
||||||
@@ -15,7 +15,7 @@ import Library from "./library";
|
|||||||
|
|
||||||
export const serializeAsJSON = (
|
export const serializeAsJSON = (
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly ExcalidrawElement[],
|
||||||
appState: AppState,
|
appState: Partial<AppState>,
|
||||||
): string => {
|
): string => {
|
||||||
const data: ExportedDataState = {
|
const data: ExportedDataState = {
|
||||||
type: EXPORT_DATA_TYPES.excalidraw,
|
type: EXPORT_DATA_TYPES.excalidraw,
|
||||||
@@ -37,19 +37,21 @@ export const saveAsJSON = async (
|
|||||||
type: MIME_TYPES.excalidraw,
|
type: MIME_TYPES.excalidraw,
|
||||||
});
|
});
|
||||||
|
|
||||||
const fileHandle = await fileSave(
|
const fileHandle = await fileSave(blob, {
|
||||||
blob,
|
name: appState.name,
|
||||||
{
|
extension: "excalidraw",
|
||||||
fileName: `${appState.name}.excalidraw`,
|
|
||||||
description: "Excalidraw file",
|
description: "Excalidraw file",
|
||||||
extensions: [".excalidraw"],
|
fileHandle: isImageFileHandle(appState.fileHandle)
|
||||||
},
|
? null
|
||||||
appState.fileHandle,
|
: appState.fileHandle,
|
||||||
);
|
});
|
||||||
return { fileHandle };
|
return { fileHandle };
|
||||||
};
|
};
|
||||||
|
|
||||||
export const loadFromJSON = async (localAppState: AppState) => {
|
export const loadFromJSON = async (
|
||||||
|
localAppState: AppState,
|
||||||
|
localElements: readonly ExcalidrawElement[] | null,
|
||||||
|
) => {
|
||||||
const blob = await fileOpen({
|
const blob = await fileOpen({
|
||||||
description: "Excalidraw files",
|
description: "Excalidraw files",
|
||||||
// ToDo: Be over-permissive until https://bugs.webkit.org/show_bug.cgi?id=34442
|
// ToDo: Be over-permissive until https://bugs.webkit.org/show_bug.cgi?id=34442
|
||||||
@@ -64,7 +66,7 @@ export const loadFromJSON = async (localAppState: AppState) => {
|
|||||||
],
|
],
|
||||||
*/
|
*/
|
||||||
});
|
});
|
||||||
return loadFromBlob(blob, localAppState);
|
return loadFromBlob(blob, localAppState, localElements);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isValidExcalidrawData = (data?: {
|
export const isValidExcalidrawData = (data?: {
|
||||||
@@ -98,15 +100,16 @@ export const saveLibraryAsJSON = async (library: Library) => {
|
|||||||
library: libraryItems,
|
library: libraryItems,
|
||||||
};
|
};
|
||||||
const serialized = JSON.stringify(data, null, 2);
|
const serialized = JSON.stringify(data, null, 2);
|
||||||
const fileName = "library.excalidrawlib";
|
await fileSave(
|
||||||
const blob = new Blob([serialized], {
|
new Blob([serialized], {
|
||||||
type: MIME_TYPES.excalidrawlib,
|
type: MIME_TYPES.excalidrawlib,
|
||||||
});
|
}),
|
||||||
await fileSave(blob, {
|
{
|
||||||
fileName,
|
name: "library",
|
||||||
|
extension: "excalidrawlib",
|
||||||
description: "Excalidraw library file",
|
description: "Excalidraw library file",
|
||||||
extensions: [".excalidrawlib"],
|
},
|
||||||
});
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const importLibraryFromJSON = async (library: Library) => {
|
export const importLibraryFromJSON = async (library: Library) => {
|
||||||
|
|||||||
+2
-2
@@ -2,7 +2,7 @@ import { loadLibraryFromBlob } from "./blob";
|
|||||||
import { LibraryItems, LibraryItem } from "../types";
|
import { LibraryItems, LibraryItem } from "../types";
|
||||||
import { restoreElements } from "./restore";
|
import { restoreElements } from "./restore";
|
||||||
import { getNonDeletedElements } from "../element";
|
import { getNonDeletedElements } from "../element";
|
||||||
import App from "../components/App";
|
import type App from "../components/App";
|
||||||
|
|
||||||
class Library {
|
class Library {
|
||||||
private libraryCache: LibraryItems | null = null;
|
private libraryCache: LibraryItems | null = null;
|
||||||
@@ -18,7 +18,7 @@ class Library {
|
|||||||
};
|
};
|
||||||
|
|
||||||
restoreLibraryItem = (libraryItem: LibraryItem): LibraryItem | null => {
|
restoreLibraryItem = (libraryItem: LibraryItem): LibraryItem | null => {
|
||||||
const elements = getNonDeletedElements(restoreElements(libraryItem));
|
const elements = getNonDeletedElements(restoreElements(libraryItem, null));
|
||||||
return elements.length ? elements : null;
|
return elements.length ? elements : null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import { ExcalidrawElement } from "../element/types";
|
||||||
|
import { AppState } from "../types";
|
||||||
|
import { exportCanvas } from ".";
|
||||||
|
import { getNonDeletedElements } from "../element";
|
||||||
|
import { getFileHandleType, isImageFileHandleType } from "./blob";
|
||||||
|
|
||||||
|
export const resaveAsImageWithScene = async (
|
||||||
|
elements: readonly ExcalidrawElement[],
|
||||||
|
appState: AppState,
|
||||||
|
) => {
|
||||||
|
const { exportBackground, viewBackgroundColor, name, fileHandle } = appState;
|
||||||
|
|
||||||
|
const fileHandleType = getFileHandleType(fileHandle);
|
||||||
|
|
||||||
|
if (!fileHandle || !isImageFileHandleType(fileHandleType)) {
|
||||||
|
throw new Error(
|
||||||
|
"fileHandle should exist and should be of type svg or png when resaving",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
appState = {
|
||||||
|
...appState,
|
||||||
|
exportEmbedScene: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
await exportCanvas(
|
||||||
|
fileHandleType,
|
||||||
|
getNonDeletedElements(elements),
|
||||||
|
appState,
|
||||||
|
{
|
||||||
|
exportBackground,
|
||||||
|
viewBackgroundColor,
|
||||||
|
name,
|
||||||
|
fileHandle,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return { fileHandle };
|
||||||
|
};
|
||||||
+29
-16
@@ -1,21 +1,26 @@
|
|||||||
import {
|
import {
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
FontFamily,
|
|
||||||
ExcalidrawSelectionElement,
|
ExcalidrawSelectionElement,
|
||||||
|
FontFamilyValues,
|
||||||
} from "../element/types";
|
} from "../element/types";
|
||||||
import { AppState, NormalizedZoomValue } from "../types";
|
import { AppState, NormalizedZoomValue } from "../types";
|
||||||
import { ImportedDataState } from "./types";
|
import { ImportedDataState } from "./types";
|
||||||
import { isInvisiblySmallElement, getNormalizedDimensions } from "../element";
|
import {
|
||||||
|
getElementMap,
|
||||||
|
getNormalizedDimensions,
|
||||||
|
isInvisiblySmallElement,
|
||||||
|
} from "../element";
|
||||||
import { isLinearElementType } from "../element/typeChecks";
|
import { isLinearElementType } from "../element/typeChecks";
|
||||||
import { randomId } from "../random";
|
import { randomId } from "../random";
|
||||||
import {
|
import {
|
||||||
FONT_FAMILY,
|
|
||||||
DEFAULT_FONT_FAMILY,
|
DEFAULT_FONT_FAMILY,
|
||||||
DEFAULT_TEXT_ALIGN,
|
DEFAULT_TEXT_ALIGN,
|
||||||
DEFAULT_VERTICAL_ALIGN,
|
DEFAULT_VERTICAL_ALIGN,
|
||||||
|
FONT_FAMILY,
|
||||||
} from "../constants";
|
} from "../constants";
|
||||||
import { getDefaultAppState } from "../appState";
|
import { getDefaultAppState } from "../appState";
|
||||||
import { LinearElementEditor } from "../element/linearElementEditor";
|
import { LinearElementEditor } from "../element/linearElementEditor";
|
||||||
|
import { bumpVersion } from "../element/mutateElement";
|
||||||
|
|
||||||
type RestoredAppState = Omit<
|
type RestoredAppState = Omit<
|
||||||
AppState,
|
AppState,
|
||||||
@@ -41,11 +46,11 @@ export type RestoredDataState = {
|
|||||||
appState: RestoredAppState;
|
appState: RestoredAppState;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getFontFamilyByName = (fontFamilyName: string): FontFamily => {
|
const getFontFamilyByName = (fontFamilyName: string): FontFamilyValues => {
|
||||||
for (const [id, fontFamilyString] of Object.entries(FONT_FAMILY)) {
|
if (Object.keys(FONT_FAMILY).includes(fontFamilyName)) {
|
||||||
if (fontFamilyString.includes(fontFamilyName)) {
|
return FONT_FAMILY[
|
||||||
return parseInt(id) as FontFamily;
|
fontFamilyName as keyof typeof FONT_FAMILY
|
||||||
}
|
] as FontFamilyValues;
|
||||||
}
|
}
|
||||||
return DEFAULT_FONT_FAMILY;
|
return DEFAULT_FONT_FAMILY;
|
||||||
};
|
};
|
||||||
@@ -181,13 +186,20 @@ const restoreElement = (
|
|||||||
|
|
||||||
export const restoreElements = (
|
export const restoreElements = (
|
||||||
elements: ImportedDataState["elements"],
|
elements: ImportedDataState["elements"],
|
||||||
|
/** NOTE doesn't serve for reconciliation */
|
||||||
|
localElements: readonly ExcalidrawElement[] | null | undefined,
|
||||||
): ExcalidrawElement[] => {
|
): ExcalidrawElement[] => {
|
||||||
|
const localElementsMap = localElements ? getElementMap(localElements) : null;
|
||||||
return (elements || []).reduce((elements, element) => {
|
return (elements || []).reduce((elements, element) => {
|
||||||
// filtering out selection, which is legacy, no longer kept in elements,
|
// filtering out selection, which is legacy, no longer kept in elements,
|
||||||
// and causing issues if retained
|
// and causing issues if retained
|
||||||
if (element.type !== "selection" && !isInvisiblySmallElement(element)) {
|
if (element.type !== "selection" && !isInvisiblySmallElement(element)) {
|
||||||
const migratedElement = restoreElement(element);
|
let migratedElement: ExcalidrawElement = restoreElement(element);
|
||||||
if (migratedElement) {
|
if (migratedElement) {
|
||||||
|
const localElement = localElementsMap?.[element.id];
|
||||||
|
if (localElement && localElement.version > migratedElement.version) {
|
||||||
|
migratedElement = bumpVersion(migratedElement, localElement.version);
|
||||||
|
}
|
||||||
elements.push(migratedElement);
|
elements.push(migratedElement);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -197,25 +209,25 @@ export const restoreElements = (
|
|||||||
|
|
||||||
export const restoreAppState = (
|
export const restoreAppState = (
|
||||||
appState: ImportedDataState["appState"],
|
appState: ImportedDataState["appState"],
|
||||||
localAppState: Partial<AppState> | null,
|
localAppState: Partial<AppState> | null | undefined,
|
||||||
): RestoredAppState => {
|
): RestoredAppState => {
|
||||||
appState = appState || {};
|
appState = appState || {};
|
||||||
|
|
||||||
const defaultAppState = getDefaultAppState();
|
const defaultAppState = getDefaultAppState();
|
||||||
const nextAppState = {} as typeof defaultAppState;
|
const nextAppState = {} as typeof defaultAppState;
|
||||||
|
|
||||||
for (const [key, val] of Object.entries(defaultAppState) as [
|
for (const [key, defaultValue] of Object.entries(defaultAppState) as [
|
||||||
keyof typeof defaultAppState,
|
keyof typeof defaultAppState,
|
||||||
any,
|
any,
|
||||||
][]) {
|
][]) {
|
||||||
const restoredValue = appState[key];
|
const suppliedValue = appState[key];
|
||||||
const localValue = localAppState ? localAppState[key] : undefined;
|
const localValue = localAppState ? localAppState[key] : undefined;
|
||||||
(nextAppState as any)[key] =
|
(nextAppState as any)[key] =
|
||||||
restoredValue !== undefined
|
suppliedValue !== undefined
|
||||||
? restoredValue
|
? suppliedValue
|
||||||
: localValue !== undefined
|
: localValue !== undefined
|
||||||
? localValue
|
? localValue
|
||||||
: val;
|
: defaultValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -243,9 +255,10 @@ export const restore = (
|
|||||||
* Supply `null` if you can't get access to it.
|
* Supply `null` if you can't get access to it.
|
||||||
*/
|
*/
|
||||||
localAppState: Partial<AppState> | null | undefined,
|
localAppState: Partial<AppState> | null | undefined,
|
||||||
|
localElements: readonly ExcalidrawElement[] | null | undefined,
|
||||||
): RestoredDataState => {
|
): RestoredDataState => {
|
||||||
return {
|
return {
|
||||||
elements: restoreElements(data?.elements),
|
elements: restoreElements(data?.elements, localElements),
|
||||||
appState: restoreAppState(data?.appState, localAppState || null),
|
appState: restoreAppState(data?.appState, localAppState || null),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -328,15 +328,15 @@ const hitTestFreeDrawElement = (
|
|||||||
let P: readonly [number, number];
|
let P: readonly [number, number];
|
||||||
|
|
||||||
// For freedraw dots
|
// For freedraw dots
|
||||||
if (element.points.length === 2) {
|
if (
|
||||||
return (
|
|
||||||
distance2d(A[0], A[1], x, y) < threshold ||
|
distance2d(A[0], A[1], x, y) < threshold ||
|
||||||
distance2d(B[0], B[1], x, y) < threshold
|
distance2d(B[0], B[1], x, y) < threshold
|
||||||
);
|
) {
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// For freedraw lines
|
// For freedraw lines
|
||||||
for (let i = 1; i < element.points.length - 1; i++) {
|
for (let i = 0; i < element.points.length; i++) {
|
||||||
const delta = [B[0] - A[0], B[1] - A[1]];
|
const delta = [B[0] - A[0], B[1] - A[1]];
|
||||||
const length = Math.hypot(delta[1], delta[0]);
|
const length = Math.hypot(delta[1], delta[0]);
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { mutateElement } from "./mutateElement";
|
|||||||
import { getPerfectElementSize } from "./sizeHelpers";
|
import { getPerfectElementSize } from "./sizeHelpers";
|
||||||
import Scene from "../scene/Scene";
|
import Scene from "../scene/Scene";
|
||||||
import { NonDeletedExcalidrawElement } from "./types";
|
import { NonDeletedExcalidrawElement } from "./types";
|
||||||
import { PointerDownState } from "../components/App";
|
import { PointerDownState } from "../types";
|
||||||
|
|
||||||
export const dragSelectedElements = (
|
export const dragSelectedElements = (
|
||||||
pointerDownState: PointerDownState,
|
pointerDownState: PointerDownState,
|
||||||
|
|||||||
@@ -120,8 +120,11 @@ export const newElementWith = <TElement extends ExcalidrawElement>(
|
|||||||
*
|
*
|
||||||
* NOTE: does not trigger re-render.
|
* NOTE: does not trigger re-render.
|
||||||
*/
|
*/
|
||||||
export const bumpVersion = (element: Mutable<ExcalidrawElement>) => {
|
export const bumpVersion = (
|
||||||
element.version = element.version + 1;
|
element: Mutable<ExcalidrawElement>,
|
||||||
|
version?: ExcalidrawElement["version"],
|
||||||
|
) => {
|
||||||
|
element.version = (version ?? element.version) + 1;
|
||||||
element.versionNonce = randomInteger();
|
element.versionNonce = randomInteger();
|
||||||
return element;
|
return element;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { duplicateElement } from "./newElement";
|
import { duplicateElement } from "./newElement";
|
||||||
import { mutateElement } from "./mutateElement";
|
import { mutateElement } from "./mutateElement";
|
||||||
import { API } from "../tests/helpers/api";
|
import { API } from "../tests/helpers/api";
|
||||||
|
import { FONT_FAMILY } from "../constants";
|
||||||
|
|
||||||
const isPrimitive = (val: any) => {
|
const isPrimitive = (val: any) => {
|
||||||
const type = typeof val;
|
const type = typeof val;
|
||||||
@@ -79,7 +80,7 @@ it("clones text element", () => {
|
|||||||
opacity: 100,
|
opacity: 100,
|
||||||
text: "hello",
|
text: "hello",
|
||||||
fontSize: 20,
|
fontSize: 20,
|
||||||
fontFamily: 1,
|
fontFamily: FONT_FAMILY.Virgil,
|
||||||
textAlign: "left",
|
textAlign: "left",
|
||||||
verticalAlign: "top",
|
verticalAlign: "top",
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,11 +5,11 @@ import {
|
|||||||
ExcalidrawGenericElement,
|
ExcalidrawGenericElement,
|
||||||
NonDeleted,
|
NonDeleted,
|
||||||
TextAlign,
|
TextAlign,
|
||||||
FontFamily,
|
|
||||||
GroupId,
|
GroupId,
|
||||||
VerticalAlign,
|
VerticalAlign,
|
||||||
Arrowhead,
|
Arrowhead,
|
||||||
ExcalidrawFreeDrawElement,
|
ExcalidrawFreeDrawElement,
|
||||||
|
FontFamilyValues,
|
||||||
} from "../element/types";
|
} from "../element/types";
|
||||||
import { measureText, getFontString } from "../utils";
|
import { measureText, getFontString } from "../utils";
|
||||||
import { randomInteger, randomId } from "../random";
|
import { randomInteger, randomId } from "../random";
|
||||||
@@ -109,7 +109,7 @@ export const newTextElement = (
|
|||||||
opts: {
|
opts: {
|
||||||
text: string;
|
text: string;
|
||||||
fontSize: number;
|
fontSize: number;
|
||||||
fontFamily: FontFamily;
|
fontFamily: FontFamilyValues;
|
||||||
textAlign: TextAlign;
|
textAlign: TextAlign;
|
||||||
verticalAlign: VerticalAlign;
|
verticalAlign: VerticalAlign;
|
||||||
} & ElementConstructorOpts,
|
} & ElementConstructorOpts,
|
||||||
|
|||||||
@@ -32,8 +32,7 @@ import {
|
|||||||
MaybeTransformHandleType,
|
MaybeTransformHandleType,
|
||||||
TransformHandleDirection,
|
TransformHandleDirection,
|
||||||
} from "./transformHandles";
|
} from "./transformHandles";
|
||||||
import { PointerDownState } from "../components/App";
|
import { Point, PointerDownState } from "../types";
|
||||||
import { Point } from "../types";
|
|
||||||
|
|
||||||
export const normalizeAngle = (angle: number): number => {
|
export const normalizeAngle = (angle: number): number => {
|
||||||
if (angle >= 2 * Math.PI) {
|
if (angle >= 2 * Math.PI) {
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import { Point } from "../types";
|
import { Point } from "../types";
|
||||||
import { FONT_FAMILY } from "../constants";
|
import { FONT_FAMILY, THEME } from "../constants";
|
||||||
|
|
||||||
export type ChartType = "bar" | "line";
|
export type ChartType = "bar" | "line";
|
||||||
export type FillStyle = "hachure" | "cross-hatch" | "solid";
|
export type FillStyle = "hachure" | "cross-hatch" | "solid";
|
||||||
export type FontFamily = keyof typeof FONT_FAMILY;
|
export type FontFamilyKeys = keyof typeof FONT_FAMILY;
|
||||||
|
export type FontFamilyValues = typeof FONT_FAMILY[FontFamilyKeys];
|
||||||
|
export type Theme = typeof THEME[keyof typeof THEME];
|
||||||
export type FontString = string & { _brand: "fontString" };
|
export type FontString = string & { _brand: "fontString" };
|
||||||
export type GroupId = string;
|
export type GroupId = string;
|
||||||
export type PointerType = "mouse" | "pen" | "touch";
|
export type PointerType = "mouse" | "pen" | "touch";
|
||||||
@@ -91,7 +93,7 @@ export type ExcalidrawTextElement = _ExcalidrawElementBase &
|
|||||||
Readonly<{
|
Readonly<{
|
||||||
type: "text";
|
type: "text";
|
||||||
fontSize: number;
|
fontSize: number;
|
||||||
fontFamily: FontFamily;
|
fontFamily: FontFamilyValues;
|
||||||
text: string;
|
text: string;
|
||||||
baseline: number;
|
baseline: number;
|
||||||
textAlign: TextAlign;
|
textAlign: TextAlign;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
type CANVAS_ERROR_NAMES = "CANVAS_ERROR" | "CANVAS_POSSIBLY_TOO_BIG";
|
type CANVAS_ERROR_NAMES = "CANVAS_ERROR" | "CANVAS_POSSIBLY_TOO_BIG";
|
||||||
|
|
||||||
export class CanvasError extends Error {
|
export class CanvasError extends Error {
|
||||||
constructor(
|
constructor(
|
||||||
message: string = "Couldn't export canvas.",
|
message: string = "Couldn't export canvas.",
|
||||||
@@ -9,3 +10,9 @@ export class CanvasError extends Error {
|
|||||||
this.message = message;
|
this.message = message;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class AbortError extends DOMException {
|
||||||
|
constructor(message: string = "Request Aborted") {
|
||||||
|
super(message, "AbortError");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { debounce, getVersion, nFormatter } from "../utils";
|
import { debounce, getVersion, nFormatter } from "../utils";
|
||||||
import {
|
import {
|
||||||
getElementsStorageSize,
|
getElementsStorageSize,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import throttle from "lodash.throttle";
|
import throttle from "lodash.throttle";
|
||||||
import React, { PureComponent } from "react";
|
import { PureComponent } from "react";
|
||||||
import { ExcalidrawImperativeAPI } from "../../components/App";
|
import { ExcalidrawImperativeAPI } from "../../types";
|
||||||
import { ErrorDialog } from "../../components/ErrorDialog";
|
import { ErrorDialog } from "../../components/ErrorDialog";
|
||||||
import { APP_NAME, ENV, EVENT } from "../../constants";
|
import { APP_NAME, ENV, EVENT } from "../../constants";
|
||||||
import { ImportedDataState } from "../../data/types";
|
import { ImportedDataState } from "../../data/types";
|
||||||
@@ -41,6 +41,7 @@ import { UserIdleState } from "../../types";
|
|||||||
import { IDLE_THRESHOLD, ACTIVE_THRESHOLD } from "../../constants";
|
import { IDLE_THRESHOLD, ACTIVE_THRESHOLD } from "../../constants";
|
||||||
import { trackEvent } from "../../analytics";
|
import { trackEvent } from "../../analytics";
|
||||||
import { isInvisiblySmallElement } from "../../element";
|
import { isInvisiblySmallElement } from "../../element";
|
||||||
|
import { getRandomUsername } from "@excalidraw/random-username";
|
||||||
|
|
||||||
interface CollabState {
|
interface CollabState {
|
||||||
modalIsShown: boolean;
|
modalIsShown: boolean;
|
||||||
@@ -223,6 +224,10 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!this.state.username) {
|
||||||
|
this.updateUsername(getRandomUsername());
|
||||||
|
}
|
||||||
|
|
||||||
let roomId;
|
let roomId;
|
||||||
let roomKey;
|
let roomKey;
|
||||||
|
|
||||||
@@ -593,7 +598,7 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
|
|||||||
this.setState({ modalIsShown: false });
|
this.setState({ modalIsShown: false });
|
||||||
};
|
};
|
||||||
|
|
||||||
onUsernameChange = (username: string) => {
|
updateUsername = (username: string) => {
|
||||||
this.setState({ username });
|
this.setState({ username });
|
||||||
saveUsernameToLocalStorage(username);
|
saveUsernameToLocalStorage(username);
|
||||||
};
|
};
|
||||||
@@ -635,7 +640,7 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
|
|||||||
handleClose={this.handleClose}
|
handleClose={this.handleClose}
|
||||||
activeRoomLink={activeRoomLink}
|
activeRoomLink={activeRoomLink}
|
||||||
username={username}
|
username={username}
|
||||||
onUsernameChange={this.onUsernameChange}
|
onUsernameChange={this.updateUsername}
|
||||||
onRoomCreate={this.openPortal}
|
onRoomCreate={this.openPortal}
|
||||||
onRoomDestroy={this.closePortal}
|
onRoomDestroy={this.closePortal}
|
||||||
setErrorMessage={(errorMessage) => {
|
setErrorMessage={(errorMessage) => {
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { t } from "../../i18n";
|
|||||||
import "./RoomDialog.scss";
|
import "./RoomDialog.scss";
|
||||||
import Stack from "../../components/Stack";
|
import Stack from "../../components/Stack";
|
||||||
import { AppState } from "../../types";
|
import { AppState } from "../../types";
|
||||||
|
import { getRandomUsername } from "@excalidraw/random-username";
|
||||||
|
|
||||||
const getShareIcon = () => {
|
const getShareIcon = () => {
|
||||||
const navigator = window.navigator as any;
|
const navigator = window.navigator as any;
|
||||||
@@ -137,9 +138,14 @@ const RoomDialog = ({
|
|||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
id="username"
|
id="username"
|
||||||
value={username || ""}
|
value={username}
|
||||||
className="RoomDialog-username TextInput"
|
className="RoomDialog-username TextInput"
|
||||||
onChange={(event) => onUsernameChange(event.target.value)}
|
onChange={(event) => onUsernameChange(event.target.value)}
|
||||||
|
onBlur={(event) => {
|
||||||
|
if (!event.target.value.trim()) {
|
||||||
|
onUsernameChange(getRandomUsername());
|
||||||
|
}
|
||||||
|
}}
|
||||||
onKeyPress={(event) => event.key === "Enter" && handleClose()}
|
onKeyPress={(event) => event.key === "Enter" && handleClose()}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Card } from "../../components/Card";
|
||||||
|
import { ToolButton } from "../../components/ToolButton";
|
||||||
|
import { serializeAsJSON } from "../../data/json";
|
||||||
|
import { getImportedKey, createIV, generateEncryptionKey } from "../data";
|
||||||
|
import { loadFirebaseStorage } from "../data/firebase";
|
||||||
|
import { NonDeletedExcalidrawElement } from "../../element/types";
|
||||||
|
import { AppState } from "../../types";
|
||||||
|
import { nanoid } from "nanoid";
|
||||||
|
import { t } from "../../i18n";
|
||||||
|
import { excalidrawPlusIcon } from "./icons";
|
||||||
|
|
||||||
|
const encryptData = async (
|
||||||
|
key: string,
|
||||||
|
json: string,
|
||||||
|
): Promise<{ blob: Blob; iv: Uint8Array }> => {
|
||||||
|
const importedKey = await getImportedKey(key, "encrypt");
|
||||||
|
const iv = createIV();
|
||||||
|
const encoded = new TextEncoder().encode(json);
|
||||||
|
const ciphertext = await window.crypto.subtle.encrypt(
|
||||||
|
{
|
||||||
|
name: "AES-GCM",
|
||||||
|
iv,
|
||||||
|
},
|
||||||
|
importedKey,
|
||||||
|
encoded,
|
||||||
|
);
|
||||||
|
|
||||||
|
return { blob: new Blob([new Uint8Array(ciphertext)]), iv };
|
||||||
|
};
|
||||||
|
|
||||||
|
const exportToExcalidrawPlus = async (
|
||||||
|
elements: readonly NonDeletedExcalidrawElement[],
|
||||||
|
appState: AppState,
|
||||||
|
) => {
|
||||||
|
const firebase = await loadFirebaseStorage();
|
||||||
|
|
||||||
|
const id = `${nanoid(12)}`;
|
||||||
|
|
||||||
|
const key = (await generateEncryptionKey())!;
|
||||||
|
const encryptedData = await encryptData(
|
||||||
|
key,
|
||||||
|
serializeAsJSON(elements, appState),
|
||||||
|
);
|
||||||
|
|
||||||
|
const blob = new Blob([encryptedData.iv, encryptedData.blob], {
|
||||||
|
type: "application/octet-stream",
|
||||||
|
});
|
||||||
|
|
||||||
|
await firebase
|
||||||
|
.storage()
|
||||||
|
.ref(`/migrations/scenes/${id}`)
|
||||||
|
.put(blob, {
|
||||||
|
customMetadata: {
|
||||||
|
data: JSON.stringify({ version: 1, name: appState.name }),
|
||||||
|
created: Date.now().toString(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
window.open(`https://plus.excalidraw.com/import?excalidraw=${id},${key}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ExportToExcalidrawPlus: React.FC<{
|
||||||
|
elements: readonly NonDeletedExcalidrawElement[];
|
||||||
|
appState: AppState;
|
||||||
|
onError: (error: Error) => void;
|
||||||
|
}> = ({ elements, appState, onError }) => {
|
||||||
|
return (
|
||||||
|
<Card color="indigo">
|
||||||
|
<div className="Card-icon">{excalidrawPlusIcon}</div>
|
||||||
|
<h2>Excalidraw+</h2>
|
||||||
|
<div className="Card-details">
|
||||||
|
{t("exportDialog.excalidrawplus_description")}
|
||||||
|
</div>
|
||||||
|
<ToolButton
|
||||||
|
className="Card-button"
|
||||||
|
type="button"
|
||||||
|
title={t("exportDialog.excalidrawplus_button")}
|
||||||
|
aria-label={t("exportDialog.excalidrawplus_button")}
|
||||||
|
showAriaLabel={true}
|
||||||
|
onClick={async () => {
|
||||||
|
try {
|
||||||
|
await exportToExcalidrawPlus(elements, appState);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
onError(new Error(t("exportDialog.excalidrawplus_exportError")));
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
import oc from "open-color";
|
import oc from "open-color";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import { THEME } from "../../constants";
|
||||||
|
import { Theme } from "../../element/types";
|
||||||
|
|
||||||
// https://github.com/tholman/github-corners
|
// https://github.com/tholman/github-corners
|
||||||
export const GitHubCorner = React.memo(
|
export const GitHubCorner = React.memo(
|
||||||
({ theme, dir }: { theme: "light" | "dark"; dir: string }) => (
|
({ theme, dir }: { theme: Theme; dir: string }) => (
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
width="40"
|
width="40"
|
||||||
@@ -25,18 +27,18 @@ export const GitHubCorner = React.memo(
|
|||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
d="M0 0l115 115h15l12 27 108 108V0z"
|
d="M0 0l115 115h15l12 27 108 108V0z"
|
||||||
fill={theme === "light" ? oc.gray[6] : oc.gray[7]}
|
fill={theme === THEME.LIGHT ? oc.gray[6] : oc.gray[7]}
|
||||||
/>
|
/>
|
||||||
<path
|
<path
|
||||||
className="octo-arm"
|
className="octo-arm"
|
||||||
d="M128 109c-15-9-9-19-9-19 3-7 2-11 2-11-1-7 3-2 3-2 4 5 2 11 2 11-3 10 5 15 9 16"
|
d="M128 109c-15-9-9-19-9-19 3-7 2-11 2-11-1-7 3-2 3-2 4 5 2 11 2 11-3 10 5 15 9 16"
|
||||||
style={{ transformOrigin: "130px 106px" }}
|
style={{ transformOrigin: "130px 106px" }}
|
||||||
fill={theme === "light" ? oc.white : "var(--default-bg-color)"}
|
fill={theme === THEME.LIGHT ? oc.white : "var(--default-bg-color)"}
|
||||||
/>
|
/>
|
||||||
<path
|
<path
|
||||||
className="octo-body"
|
className="octo-body"
|
||||||
d="M115 115s4 2 5 0l14-14c3-2 6-3 8-3-8-11-15-24 2-41 5-5 10-7 16-7 1-2 3-7 12-11 0 0 5 3 7 16 4 2 8 5 12 9s7 8 9 12c14 3 17 7 17 7-4 8-9 11-11 11 0 6-2 11-7 16-16 16-30 10-41 2 0 3-1 7-5 11l-12 11c-1 1 1 5 1 5z"
|
d="M115 115s4 2 5 0l14-14c3-2 6-3 8-3-8-11-15-24 2-41 5-5 10-7 16-7 1-2 3-7 12-11 0 0 5 3 7 16 4 2 8 5 12 9s7 8 9 12c14 3 17 7 17 7-4 8-9 11-11 11 0 6-2 11-7 16-16 16-30 10-41 2 0 3-1 7-5 11l-12 11c-1 1 1 5 1 5z"
|
||||||
fill={theme === "light" ? oc.white : "var(--default-bg-color)"}
|
fill={theme === THEME.LIGHT ? oc.white : "var(--default-bg-color)"}
|
||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
@@ -1,23 +1,18 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import clsx from "clsx";
|
|
||||||
import * as i18n from "../../i18n";
|
import * as i18n from "../../i18n";
|
||||||
|
|
||||||
export const LanguageList = ({
|
export const LanguageList = ({
|
||||||
onChange,
|
onChange,
|
||||||
languages = i18n.languages,
|
languages = i18n.languages,
|
||||||
currentLangCode = i18n.getLanguage().code,
|
currentLangCode = i18n.getLanguage().code,
|
||||||
floating,
|
|
||||||
}: {
|
}: {
|
||||||
languages?: { code: string; label: string }[];
|
languages?: { code: string; label: string }[];
|
||||||
onChange: (langCode: i18n.Language["code"]) => void;
|
onChange: (langCode: i18n.Language["code"]) => void;
|
||||||
currentLangCode?: i18n.Language["code"];
|
currentLangCode?: i18n.Language["code"];
|
||||||
floating?: boolean;
|
|
||||||
}) => (
|
}) => (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<select
|
<select
|
||||||
className={clsx("dropdown-select dropdown-select__language", {
|
className="dropdown-select dropdown-select__language"
|
||||||
"dropdown-select--floating": floating,
|
|
||||||
})}
|
|
||||||
onChange={({ target }) => onChange(target.value)}
|
onChange={({ target }) => onChange(target.value)}
|
||||||
value={currentLangCode}
|
value={currentLangCode}
|
||||||
aria-label={i18n.t("buttons.selectLanguage")}
|
aria-label={i18n.t("buttons.selectLanguage")}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -5,15 +5,19 @@ import { getSceneVersion } from "../../element";
|
|||||||
import Portal from "../collab/Portal";
|
import Portal from "../collab/Portal";
|
||||||
import { restoreElements } from "../../data/restore";
|
import { restoreElements } from "../../data/restore";
|
||||||
|
|
||||||
|
// private
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
let firebasePromise: Promise<
|
let firebasePromise: Promise<
|
||||||
typeof import("firebase/app").default
|
typeof import("firebase/app").default
|
||||||
> | null = null;
|
> | null = null;
|
||||||
|
let firestorePromise: Promise<any> | null = null;
|
||||||
|
let firebseStoragePromise: Promise<any> | null = null;
|
||||||
|
|
||||||
const loadFirebase = async () => {
|
const _loadFirebase = async () => {
|
||||||
const firebase = (
|
const firebase = (
|
||||||
await import(/* webpackChunkName: "firebase" */ "firebase/app")
|
await import(/* webpackChunkName: "firebase" */ "firebase/app")
|
||||||
).default;
|
).default;
|
||||||
await import(/* webpackChunkName: "firestore" */ "firebase/firestore");
|
|
||||||
|
|
||||||
const firebaseConfig = JSON.parse(process.env.REACT_APP_FIREBASE_CONFIG);
|
const firebaseConfig = JSON.parse(process.env.REACT_APP_FIREBASE_CONFIG);
|
||||||
firebase.initializeApp(firebaseConfig);
|
firebase.initializeApp(firebaseConfig);
|
||||||
@@ -21,13 +25,37 @@ const loadFirebase = async () => {
|
|||||||
return firebase;
|
return firebase;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getFirebase = async (): Promise<
|
const _getFirebase = async (): Promise<
|
||||||
typeof import("firebase/app").default
|
typeof import("firebase/app").default
|
||||||
> => {
|
> => {
|
||||||
if (!firebasePromise) {
|
if (!firebasePromise) {
|
||||||
firebasePromise = loadFirebase();
|
firebasePromise = _loadFirebase();
|
||||||
}
|
}
|
||||||
return await firebasePromise!;
|
return firebasePromise;
|
||||||
|
};
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const loadFirestore = async () => {
|
||||||
|
const firebase = await _getFirebase();
|
||||||
|
if (!firestorePromise) {
|
||||||
|
firestorePromise = import(
|
||||||
|
/* webpackChunkName: "firestore" */ "firebase/firestore"
|
||||||
|
);
|
||||||
|
await firestorePromise;
|
||||||
|
}
|
||||||
|
return firebase;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const loadFirebaseStorage = async () => {
|
||||||
|
const firebase = await _getFirebase();
|
||||||
|
if (!firebseStoragePromise) {
|
||||||
|
firebseStoragePromise = import(
|
||||||
|
/* webpackChunkName: "storage" */ "firebase/storage"
|
||||||
|
);
|
||||||
|
await firebseStoragePromise;
|
||||||
|
}
|
||||||
|
return firebase;
|
||||||
};
|
};
|
||||||
|
|
||||||
interface FirebaseStoredScene {
|
interface FirebaseStoredScene {
|
||||||
@@ -108,7 +136,7 @@ export const saveToFirebase = async (
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const firebase = await getFirebase();
|
const firebase = await loadFirestore();
|
||||||
const sceneVersion = getSceneVersion(elements);
|
const sceneVersion = getSceneVersion(elements);
|
||||||
const { ciphertext, iv } = await encryptElements(roomKey, elements);
|
const { ciphertext, iv } = await encryptElements(roomKey, elements);
|
||||||
|
|
||||||
@@ -150,7 +178,7 @@ export const loadFromFirebase = async (
|
|||||||
roomKey: string,
|
roomKey: string,
|
||||||
socket: SocketIOClient.Socket | null,
|
socket: SocketIOClient.Socket | null,
|
||||||
): Promise<readonly ExcalidrawElement[] | null> => {
|
): Promise<readonly ExcalidrawElement[] | null> => {
|
||||||
const firebase = await getFirebase();
|
const firebase = await loadFirestore();
|
||||||
const db = firebase.firestore();
|
const db = firebase.firestore();
|
||||||
|
|
||||||
const docRef = db.collection("scenes").doc(roomId);
|
const docRef = db.collection("scenes").doc(roomId);
|
||||||
@@ -168,5 +196,5 @@ export const loadFromFirebase = async (
|
|||||||
firebaseSceneVersionCache.set(socket, getSceneVersion(elements));
|
firebaseSceneVersionCache.set(socket, getSceneVersion(elements));
|
||||||
}
|
}
|
||||||
|
|
||||||
return restoreElements(elements);
|
return restoreElements(elements, null);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ const generateRandomID = async () => {
|
|||||||
return Array.from(arr, byteToHex).join("");
|
return Array.from(arr, byteToHex).join("");
|
||||||
};
|
};
|
||||||
|
|
||||||
const generateEncryptionKey = async () => {
|
export const generateEncryptionKey = async () => {
|
||||||
const key = await window.crypto.subtle.generateKey(
|
const key = await window.crypto.subtle.generateKey(
|
||||||
{
|
{
|
||||||
name: "AES-GCM",
|
name: "AES-GCM",
|
||||||
@@ -137,6 +137,10 @@ export const decryptAESGEM = async (
|
|||||||
export const getCollaborationLinkData = (link: string) => {
|
export const getCollaborationLinkData = (link: string) => {
|
||||||
const hash = new URL(link).hash;
|
const hash = new URL(link).hash;
|
||||||
const match = hash.match(/^#room=([a-zA-Z0-9_-]+),([a-zA-Z0-9_-]+)$/);
|
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;
|
||||||
|
}
|
||||||
return match ? { roomId: match[1], roomKey: match[2] } : null;
|
return match ? { roomId: match[1], roomKey: match[2] } : null;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -176,7 +180,7 @@ export const getImportedKey = (key: string, usage: KeyUsage) =>
|
|||||||
[usage],
|
[usage],
|
||||||
);
|
);
|
||||||
|
|
||||||
const decryptImported = async (
|
export const decryptImported = async (
|
||||||
iv: ArrayBuffer,
|
iv: ArrayBuffer,
|
||||||
encrypted: ArrayBuffer,
|
encrypted: ArrayBuffer,
|
||||||
privateKey: string,
|
privateKey: string,
|
||||||
@@ -257,9 +261,10 @@ export const loadScene = async (
|
|||||||
data = restore(
|
data = restore(
|
||||||
await importFromBackend(id, privateKey),
|
await importFromBackend(id, privateKey),
|
||||||
localDataState?.appState,
|
localDataState?.appState,
|
||||||
|
localDataState?.elements,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
data = restore(localDataState || null, null);
|
data = restore(localDataState || null, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -2,12 +2,18 @@
|
|||||||
.layer-ui__wrapper__footer-center {
|
.layer-ui__wrapper__footer-center {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
margin-top: auto;
|
||||||
|
margin-bottom: auto;
|
||||||
|
margin-inline-start: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.encrypted-icon {
|
.encrypted-icon {
|
||||||
border-radius: var(--space-factor);
|
border-radius: var(--space-factor);
|
||||||
color: var(--icon-green-fill-color);
|
color: var(--icon-green-fill-color);
|
||||||
margin-top: 13px;
|
margin-top: auto;
|
||||||
|
margin-bottom: auto;
|
||||||
|
margin-inline-start: auto;
|
||||||
|
margin-inline-end: 0.6em;
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
width: 1.2rem;
|
width: 1.2rem;
|
||||||
|
|||||||
@@ -1,14 +1,7 @@
|
|||||||
import LanguageDetector from "i18next-browser-languagedetector";
|
import LanguageDetector from "i18next-browser-languagedetector";
|
||||||
import React, {
|
import { useCallback, useContext, useEffect, useRef, useState } from "react";
|
||||||
useCallback,
|
|
||||||
useContext,
|
|
||||||
useEffect,
|
|
||||||
useRef,
|
|
||||||
useState,
|
|
||||||
} from "react";
|
|
||||||
import { trackEvent } from "../analytics";
|
import { trackEvent } from "../analytics";
|
||||||
import { getDefaultAppState } from "../appState";
|
import { getDefaultAppState } from "../appState";
|
||||||
import { ExcalidrawImperativeAPI } from "../components/App";
|
|
||||||
import { ErrorDialog } from "../components/ErrorDialog";
|
import { ErrorDialog } from "../components/ErrorDialog";
|
||||||
import { TopErrorBoundary } from "../components/TopErrorBoundary";
|
import { TopErrorBoundary } from "../components/TopErrorBoundary";
|
||||||
import {
|
import {
|
||||||
@@ -31,7 +24,7 @@ import Excalidraw, {
|
|||||||
defaultLang,
|
defaultLang,
|
||||||
languages,
|
languages,
|
||||||
} from "../packages/excalidraw/index";
|
} from "../packages/excalidraw/index";
|
||||||
import { AppState, LibraryItems } from "../types";
|
import { AppState, LibraryItems, ExcalidrawImperativeAPI } from "../types";
|
||||||
import {
|
import {
|
||||||
debounce,
|
debounce,
|
||||||
getVersion,
|
getVersion,
|
||||||
@@ -56,6 +49,7 @@ import { Tooltip } from "../components/Tooltip";
|
|||||||
import { shield } from "../components/icons";
|
import { shield } from "../components/icons";
|
||||||
|
|
||||||
import "./index.scss";
|
import "./index.scss";
|
||||||
|
import { ExportToExcalidrawPlus } from "./components/ExportToExcalidrawPlus";
|
||||||
|
|
||||||
const languageDetector = new LanguageDetector();
|
const languageDetector = new LanguageDetector();
|
||||||
languageDetector.init({
|
languageDetector.init({
|
||||||
@@ -141,7 +135,7 @@ const initializeScene = async (opts: {
|
|||||||
const url = externalUrlMatch[1];
|
const url = externalUrlMatch[1];
|
||||||
try {
|
try {
|
||||||
const request = await fetch(window.decodeURIComponent(url));
|
const request = await fetch(window.decodeURIComponent(url));
|
||||||
const data = await loadFromBlob(await request.blob(), null);
|
const data = await loadFromBlob(await request.blob(), null, null);
|
||||||
if (
|
if (
|
||||||
!scene.elements.length ||
|
!scene.elements.length ||
|
||||||
window.confirm(t("alerts.loadSceneOverridePrompt"))
|
window.confirm(t("alerts.loadSceneOverridePrompt"))
|
||||||
@@ -348,11 +342,8 @@ const ExcalidrawWrapper = () => {
|
|||||||
|
|
||||||
const renderLanguageList = () => (
|
const renderLanguageList = () => (
|
||||||
<LanguageList
|
<LanguageList
|
||||||
onChange={(langCode) => {
|
onChange={(langCode) => setLangCode(langCode)}
|
||||||
setLangCode(langCode);
|
|
||||||
}}
|
|
||||||
languages={languages}
|
languages={languages}
|
||||||
floating={!isMobile}
|
|
||||||
currentLangCode={langCode}
|
currentLangCode={langCode}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -428,6 +419,21 @@ const ExcalidrawWrapper = () => {
|
|||||||
canvasActions: {
|
canvasActions: {
|
||||||
export: {
|
export: {
|
||||||
onExportToBackend,
|
onExportToBackend,
|
||||||
|
renderCustomUI: (elements, appState) => {
|
||||||
|
return (
|
||||||
|
<ExportToExcalidrawPlus
|
||||||
|
elements={elements}
|
||||||
|
appState={appState}
|
||||||
|
onError={(error) => {
|
||||||
|
excalidrawAPI?.updateScene({
|
||||||
|
appState: {
|
||||||
|
errorMessage: error.message,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
@@ -438,6 +444,7 @@ const ExcalidrawWrapper = () => {
|
|||||||
detectScroll={false}
|
detectScroll={false}
|
||||||
handleKeyboardGlobally={true}
|
handleKeyboardGlobally={true}
|
||||||
onLibraryChange={onLibraryChange}
|
onLibraryChange={onLibraryChange}
|
||||||
|
autoFocus={true}
|
||||||
/>
|
/>
|
||||||
{excalidrawAPI && <CollabWrapper excalidrawAPI={excalidrawAPI} />}
|
{excalidrawAPI && <CollabWrapper excalidrawAPI={excalidrawAPI} />}
|
||||||
{errorMessage && (
|
{errorMessage && (
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ const allLanguages: Language[] = [
|
|||||||
{ code: "zh-TW", label: "繁體中文" },
|
{ code: "zh-TW", label: "繁體中文" },
|
||||||
{ code: "lv-LV", label: "Latviešu" },
|
{ code: "lv-LV", label: "Latviešu" },
|
||||||
{ code: "cs-CZ", label: "Česky" },
|
{ code: "cs-CZ", label: "Česky" },
|
||||||
|
{ code: "kk-KZ", label: "Қазақ тілі" },
|
||||||
].concat([defaultLang]);
|
].concat([defaultLang]);
|
||||||
|
|
||||||
export const languages: Language[] = allLanguages
|
export const languages: Language[] = allLanguages
|
||||||
|
|||||||
@@ -69,7 +69,6 @@ const canvas = exportToCanvas(
|
|||||||
{
|
{
|
||||||
exportBackground: true,
|
exportBackground: true,
|
||||||
viewBackgroundColor: "#ffffff",
|
viewBackgroundColor: "#ffffff",
|
||||||
scale: 1,
|
|
||||||
},
|
},
|
||||||
createCanvas,
|
createCanvas,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import React from "react";
|
|
||||||
import ReactDOM from "react-dom";
|
import ReactDOM from "react-dom";
|
||||||
import ExcalidrawApp from "./excalidraw-app";
|
import ExcalidrawApp from "./excalidraw-app";
|
||||||
|
|
||||||
|
|||||||
+83
-26
@@ -4,7 +4,7 @@
|
|||||||
"pasteCharts": "لصق الرسوم البيانية",
|
"pasteCharts": "لصق الرسوم البيانية",
|
||||||
"selectAll": "تحديد الكل",
|
"selectAll": "تحديد الكل",
|
||||||
"multiSelect": "إضافة عنصر للتحديد",
|
"multiSelect": "إضافة عنصر للتحديد",
|
||||||
"moveCanvas": "نقل لوح رسم",
|
"moveCanvas": "نقل لوح الرسم",
|
||||||
"cut": "قص",
|
"cut": "قص",
|
||||||
"copy": "نسخ",
|
"copy": "نسخ",
|
||||||
"copyAsPng": "نسخ إلى الحافظة بصيغة PNG",
|
"copyAsPng": "نسخ إلى الحافظة بصيغة PNG",
|
||||||
@@ -14,18 +14,18 @@
|
|||||||
"bringToFront": "أحضر للأمام",
|
"bringToFront": "أحضر للأمام",
|
||||||
"sendBackward": "أرسل للخلف",
|
"sendBackward": "أرسل للخلف",
|
||||||
"delete": "حذف",
|
"delete": "حذف",
|
||||||
"copyStyles": "نسخ النمط",
|
"copyStyles": "نسخ الأنماط",
|
||||||
"pasteStyles": "لصق النمط",
|
"pasteStyles": "لصق الأنماط",
|
||||||
"stroke": "الحدود",
|
"stroke": "الخط",
|
||||||
"background": "الخلفية",
|
"background": "الخلفية",
|
||||||
"fill": "التعبئة",
|
"fill": "التعبئة",
|
||||||
"strokeWidth": "حجم الحدود",
|
"strokeWidth": "سُمك الخط",
|
||||||
"strokeShape": "",
|
"strokeShape": "شكل الخط",
|
||||||
"strokeShape_gel": "",
|
"strokeShape_gel": "قلم جل",
|
||||||
"strokeShape_fountain": "",
|
"strokeShape_fountain": "قلم رش",
|
||||||
"strokeShape_brush": "",
|
"strokeShape_brush": "فرشاه",
|
||||||
"strokeStyle": "نمط الحدود",
|
"strokeStyle": "نمط الخط",
|
||||||
"strokeStyle_solid": "صلبة",
|
"strokeStyle_solid": "كامل",
|
||||||
"strokeStyle_dashed": "متقطع",
|
"strokeStyle_dashed": "متقطع",
|
||||||
"strokeStyle_dotted": "منقط",
|
"strokeStyle_dotted": "منقط",
|
||||||
"sloppiness": "الإمالة",
|
"sloppiness": "الإمالة",
|
||||||
@@ -43,12 +43,12 @@
|
|||||||
"fontFamily": "نوع الخط",
|
"fontFamily": "نوع الخط",
|
||||||
"onlySelected": "المحدد فقط",
|
"onlySelected": "المحدد فقط",
|
||||||
"withBackground": "الخلفية",
|
"withBackground": "الخلفية",
|
||||||
"exportEmbedScene": "",
|
"exportEmbedScene": "تضمين المشهد",
|
||||||
"exportEmbedScene_details": "سيتم حفظ بيانات المشهد في ملف PNG/SVG المصدّر بحيث يمكن استعادة المشهد منه.\nسيزيد حجم الملف المصدر.",
|
"exportEmbedScene_details": "سيتم حفظ بيانات المشهد في ملف PNG/SVG المصدّر بحيث يمكن استعادة المشهد منه.\nسيزيد حجم الملف المصدر.",
|
||||||
"addWatermark": "إضافة \"مصنوعة بواسطة Excalidraw\"",
|
"addWatermark": "إضافة \"مصنوعة بواسطة Excalidraw\"",
|
||||||
"handDrawn": "رسم باليد",
|
"handDrawn": "رسم باليد",
|
||||||
"normal": "عادي",
|
"normal": "عادي",
|
||||||
"code": "الرمز",
|
"code": "رمز",
|
||||||
"small": "صغير",
|
"small": "صغير",
|
||||||
"medium": "متوسط",
|
"medium": "متوسط",
|
||||||
"large": "كبير",
|
"large": "كبير",
|
||||||
@@ -66,7 +66,7 @@
|
|||||||
"artist": "رسام",
|
"artist": "رسام",
|
||||||
"cartoonist": "كرتوني",
|
"cartoonist": "كرتوني",
|
||||||
"fileTitle": "إسم الملف",
|
"fileTitle": "إسم الملف",
|
||||||
"colorPicker": "اختيار الألوان",
|
"colorPicker": "منتقي اللون",
|
||||||
"canvasBackground": "خلفية اللوحة",
|
"canvasBackground": "خلفية اللوحة",
|
||||||
"drawingCanvas": "لوحة الرسم",
|
"drawingCanvas": "لوحة الرسم",
|
||||||
"layers": "الطبقات",
|
"layers": "الطبقات",
|
||||||
@@ -99,8 +99,10 @@
|
|||||||
"flipHorizontal": "قلب عامودي",
|
"flipHorizontal": "قلب عامودي",
|
||||||
"flipVertical": "قلب أفقي",
|
"flipVertical": "قلب أفقي",
|
||||||
"viewMode": "نمط العرض",
|
"viewMode": "نمط العرض",
|
||||||
"toggleExportColorScheme": "",
|
"toggleExportColorScheme": "تبديل نظام ألوان الصادرات",
|
||||||
"share": "مشاركة",
|
"share": "مشاركة",
|
||||||
|
"showStroke": "إظهار منتقي لون الخط",
|
||||||
|
"showBackground": "إظهار منتقي لون الخلفية",
|
||||||
"toggleTheme": "غير النمط"
|
"toggleTheme": "غير النمط"
|
||||||
},
|
},
|
||||||
"buttons": {
|
"buttons": {
|
||||||
@@ -149,13 +151,14 @@
|
|||||||
"loadSceneOverridePrompt": "تحميل الرسم الخارجي سيحل محل المحتوى الموجود لديك. هل ترغب في المتابعة؟",
|
"loadSceneOverridePrompt": "تحميل الرسم الخارجي سيحل محل المحتوى الموجود لديك. هل ترغب في المتابعة؟",
|
||||||
"collabStopOverridePrompt": "إيقاف الجلسة سيؤدي إلى الكتابة فوق رسومك السابقة المخزنة داخليا. هل أنت متأكد؟\n\n(إذا كنت ترغب في الاحتفاظ برسمك المخزن داخليا، ببساطة أغلق علامة تبويب المتصفح بدلاً من ذلك.)",
|
"collabStopOverridePrompt": "إيقاف الجلسة سيؤدي إلى الكتابة فوق رسومك السابقة المخزنة داخليا. هل أنت متأكد؟\n\n(إذا كنت ترغب في الاحتفاظ برسمك المخزن داخليا، ببساطة أغلق علامة تبويب المتصفح بدلاً من ذلك.)",
|
||||||
"errorLoadingLibrary": "حصل خطأ أثناء تحميل مكتبة الطرف الثالث.",
|
"errorLoadingLibrary": "حصل خطأ أثناء تحميل مكتبة الطرف الثالث.",
|
||||||
"errorAddingToLibrary": "",
|
"errorAddingToLibrary": "تعذر إضافة العنصر للمكتبة",
|
||||||
"errorRemovingFromLibrary": "",
|
"errorRemovingFromLibrary": "تعذر إزالة العنصر من المكتبة",
|
||||||
"confirmAddLibrary": "هذا سيضيف {{numShapes}} شكل إلى مكتبتك. هل أنت متأكد؟",
|
"confirmAddLibrary": "هذا سيضيف {{numShapes}} شكل إلى مكتبتك. هل أنت متأكد؟",
|
||||||
"imageDoesNotContainScene": "استيراد الصور غير مدعوم في الوقت الراهن.\n\nهل تريد استيراد مشهد؟ لا يبدو أن هذه الصورة تحتوي على أي بيانات مشهد. هل قمت بسماح هذا أثناء التصدير؟",
|
"imageDoesNotContainScene": "استيراد الصور غير مدعوم في الوقت الراهن.\n\nهل تريد استيراد مشهد؟ لا يبدو أن هذه الصورة تحتوي على أي بيانات مشهد. هل قمت بسماح هذا أثناء التصدير؟",
|
||||||
"cannotRestoreFromImage": "تعذر استعادة المشهد من ملف الصورة",
|
"cannotRestoreFromImage": "تعذر استعادة المشهد من ملف الصورة",
|
||||||
"invalidSceneUrl": "",
|
"invalidSceneUrl": "تعذر استيراد المشهد من عنوان URL المتوفر. إما أنها مشوهة، أو لا تحتوي على بيانات Excalidraw JSON صالحة.",
|
||||||
"resetLibrary": ""
|
"resetLibrary": "هذا سوف يمسح مكتبتك. هل أنت متأكد؟",
|
||||||
|
"invalidEncryptionKey": "مفتاح التشفير يجب أن يكون من 22 حرفاً. التعاون المباشر معطل."
|
||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "تحديد",
|
"selection": "تحديد",
|
||||||
@@ -164,7 +167,7 @@
|
|||||||
"ellipse": "دائرة",
|
"ellipse": "دائرة",
|
||||||
"arrow": "سهم",
|
"arrow": "سهم",
|
||||||
"line": "خط",
|
"line": "خط",
|
||||||
"freedraw": "",
|
"freedraw": "رسم",
|
||||||
"text": "نص",
|
"text": "نص",
|
||||||
"library": "مكتبة",
|
"library": "مكتبة",
|
||||||
"lock": "الحفاظ على أداة التحديد نشطة بعد الرسم"
|
"lock": "الحفاظ على أداة التحديد نشطة بعد الرسم"
|
||||||
@@ -178,6 +181,8 @@
|
|||||||
"linearElement": "انقر لبدء نقاط متعددة، اسحب لخط واحد",
|
"linearElement": "انقر لبدء نقاط متعددة، اسحب لخط واحد",
|
||||||
"freeDraw": "انقر واسحب، افرج عند الانتهاء",
|
"freeDraw": "انقر واسحب، افرج عند الانتهاء",
|
||||||
"text": "نصيحة: يمكنك أيضًا إضافة نص بالنقر المزدوج في أي مكان بأداة الاختيار",
|
"text": "نصيحة: يمكنك أيضًا إضافة نص بالنقر المزدوج في أي مكان بأداة الاختيار",
|
||||||
|
"text_selected": "انقر نقراً مزدوجاً أو اضغط ادخال لتعديل النص",
|
||||||
|
"text_editing": "اضغط على Esc أو (Ctrl أو Cmd) + Enter لإنهاء التعديل",
|
||||||
"linearElementMulti": "انقر فوق النقطة الأخيرة أو اضغط على Esc أو Enter للإنهاء",
|
"linearElementMulti": "انقر فوق النقطة الأخيرة أو اضغط على Esc أو Enter للإنهاء",
|
||||||
"lockAngle": "يمكنك تقييد الزاوية بالضغط على SHIFT",
|
"lockAngle": "يمكنك تقييد الزاوية بالضغط على SHIFT",
|
||||||
"resize": "يمكنك تقييد النسب بالضغط على SHIFT أثناء تغيير الحجم،\nاضغط على ALT لتغيير الحجم من المركز",
|
"resize": "يمكنك تقييد النسب بالضغط على SHIFT أثناء تغيير الحجم،\nاضغط على ALT لتغيير الحجم من المركز",
|
||||||
@@ -212,18 +217,21 @@
|
|||||||
"desc_inProgressIntro": "تجري الآن المشاركة الحية.",
|
"desc_inProgressIntro": "تجري الآن المشاركة الحية.",
|
||||||
"desc_shareLink": "شارك هذا الرابط مع أي شخص تريده أن يشاركك الجلسة:",
|
"desc_shareLink": "شارك هذا الرابط مع أي شخص تريده أن يشاركك الجلسة:",
|
||||||
"desc_exitSession": "إيقاف الجلسة سيؤدي إلى قطع الاتصال الخاص بك من الغرفة، ولكن ستتمكن من مواصلة العمل مع المشهد، محليا. لاحظ أن هذا لن يؤثر على الأشخاص الآخرين، و سيظلون قادرين على التعاون في إصدارهم.",
|
"desc_exitSession": "إيقاف الجلسة سيؤدي إلى قطع الاتصال الخاص بك من الغرفة، ولكن ستتمكن من مواصلة العمل مع المشهد، محليا. لاحظ أن هذا لن يؤثر على الأشخاص الآخرين، و سيظلون قادرين على التعاون في إصدارهم.",
|
||||||
"shareTitle": ""
|
"shareTitle": "الانضمام إلى جلسة تعاون حية على Excalidraw"
|
||||||
},
|
},
|
||||||
"errorDialog": {
|
"errorDialog": {
|
||||||
"title": "خطأ"
|
"title": "خطأ"
|
||||||
},
|
},
|
||||||
"exportDialog": {
|
"exportDialog": {
|
||||||
"disk_title": "حفظ الملف للجهاز",
|
"disk_title": "حفظ الملف للجهاز",
|
||||||
"disk_details": "",
|
"disk_details": "تصدير بيانات المشهد إلى ملف يمكنك الاستيراد منه لاحقاً.",
|
||||||
"disk_button": "إحفظ لملف",
|
"disk_button": "إحفظ لملف",
|
||||||
"link_title": "رابط قابل للمشاركة",
|
"link_title": "رابط قابل للمشاركة",
|
||||||
"link_details": "صدر الملف للمشاهدة فقط.",
|
"link_details": "صدر الملف للمشاهدة فقط.",
|
||||||
"link_button": "التصدير كرابط"
|
"link_button": "التصدير كرابط",
|
||||||
|
"excalidrawplus_description": "حفظ المشهد إلى مساحة العمل +Excalidraw الخاصة بك.",
|
||||||
|
"excalidrawplus_button": "تصدير",
|
||||||
|
"excalidrawplus_exportError": "تعذر التصدير إلى +Excalidraw في الوقت الحالي..."
|
||||||
},
|
},
|
||||||
"helpDialog": {
|
"helpDialog": {
|
||||||
"blog": "اقرأ مدونتنا",
|
"blog": "اقرأ مدونتنا",
|
||||||
@@ -231,16 +239,18 @@
|
|||||||
"curvedArrow": "سهم مائل",
|
"curvedArrow": "سهم مائل",
|
||||||
"curvedLine": "خط مائل",
|
"curvedLine": "خط مائل",
|
||||||
"documentation": "دليل الاستخدام",
|
"documentation": "دليل الاستخدام",
|
||||||
|
"doubleClick": "انقر مرتين",
|
||||||
"drag": "اسحب",
|
"drag": "اسحب",
|
||||||
"editor": "المحرر",
|
"editor": "المحرر",
|
||||||
|
"editSelectedShape": "تعديل الشكل المحدد (النص/السهم/الخط)",
|
||||||
"github": "عثرت على مشكلة؟ إرسال",
|
"github": "عثرت على مشكلة؟ إرسال",
|
||||||
"howto": "اتبع التعليمات",
|
"howto": "اتبع التعليمات",
|
||||||
"or": "أو",
|
"or": "أو",
|
||||||
"preventBinding": "منع ارتبط السهم",
|
"preventBinding": "منع ارتبط السهم",
|
||||||
"shapes": "أشكال",
|
"shapes": "أشكال",
|
||||||
"shortcuts": "اختصارات لوحة المفاتيح",
|
"shortcuts": "اختصارات لوحة المفاتيح",
|
||||||
"textFinish": "الانتهاء من التحرير (نص)",
|
"textFinish": "إنهاء التعديل (محرر النص)",
|
||||||
"textNewLine": "اضف سطر جديد (نص)",
|
"textNewLine": "أضف سطر جديد (محرر نص)",
|
||||||
"title": "المساعدة",
|
"title": "المساعدة",
|
||||||
"view": "عرض",
|
"view": "عرض",
|
||||||
"zoomToFit": "تكبير للملائمة",
|
"zoomToFit": "تكبير للملائمة",
|
||||||
@@ -266,12 +276,59 @@
|
|||||||
"width": "العرض"
|
"width": "العرض"
|
||||||
},
|
},
|
||||||
"toast": {
|
"toast": {
|
||||||
"copyStyles": "نسخ النمط.",
|
"copyStyles": "نسخت الانماط.",
|
||||||
"copyToClipboard": "نسخ إلى الحافظة.",
|
"copyToClipboard": "نسخ إلى الحافظة.",
|
||||||
"copyToClipboardAsPng": "تم نسخ {{exportSelection}} إلى الحافظة بصيغة PNG\n({{exportColorScheme}})",
|
"copyToClipboardAsPng": "تم نسخ {{exportSelection}} إلى الحافظة بصيغة PNG\n({{exportColorScheme}})",
|
||||||
"fileSaved": "تم حفظ الملف.",
|
"fileSaved": "تم حفظ الملف.",
|
||||||
"fileSavedToFilename": "حفظ باسم {filename}",
|
"fileSavedToFilename": "حفظ باسم {filename}",
|
||||||
"canvas": "لوحة الرسم",
|
"canvas": "لوحة الرسم",
|
||||||
"selection": "العنصر المحدد"
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user