Compare commits
126 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0e3a5b2042 | |||
| c819b653bf | |||
| 60cea7a0c2 | |||
| d63b6a3469 | |||
| 0912fe1c93 | |||
| 360310de31 | |||
| 716c78e930 | |||
| ba48974351 | |||
| 6c3e4417e1 | |||
| bc0b6e1888 | |||
| 99a22e8445 | |||
| e6d9797167 | |||
| a1e8fdfb1b | |||
| 1cce63b07b | |||
| e9c2a09c21 | |||
| 55e0812680 | |||
| 0f32278a7e | |||
| 1bdb8da1c3 | |||
| 9c9787e0a0 | |||
| c2fe24c562 | |||
| 52faa52091 | |||
| dd12abc583 | |||
| abebf9aff8 | |||
| 790c9fd02e | |||
| 357266e9ab | |||
| 0bbb4535cf | |||
| d201d0be1b | |||
| 5662c5141d | |||
| 044614dcf3 | |||
| 9ec15989ab | |||
| 08aafcd248 | |||
| ea5602457f | |||
| 3c58d19d45 | |||
| fcfcdebc99 | |||
| aa97c074a7 | |||
| d65d2c5279 | |||
| 6d40039f08 | |||
| f4e10c93e1 | |||
| 82c6df0e1f | |||
| c37bd59ddd | |||
| 198a5e3b53 | |||
| a78e1fa99b | |||
| fc5db9248c | |||
| ebf64036fd | |||
| 6271a031a3 | |||
| 78da4c075e | |||
| f1cf28a84e | |||
| 3b9290831a | |||
| bec34f2d57 | |||
| 07839f8d20 | |||
| 8068d1f853 | |||
| 92c7d3257f | |||
| a8a5e7b6ff | |||
| 45a4a00b69 | |||
| 436e539d3a | |||
| ff19167063 | |||
| 3fc531ed6e | |||
| 6f55c00814 | |||
| a7eb6e1168 | |||
| 641bbdd2da | |||
| 42b0f7a614 | |||
| c11e3818ac | |||
| 4b6aa5c53b | |||
| ebd0408d7d | |||
| f4fefbcee8 | |||
| 11b8cc2caa | |||
| 6bebfe63be | |||
| 91ab7f36e2 | |||
| 5ee8e8249c | |||
| 49c6bdd520 | |||
| 198800136e | |||
| 178ee04d82 | |||
| 18cdafbcbe | |||
| 286e9a1524 | |||
| bac76778ce | |||
| f28f7ffb6e | |||
| 12e8cc853f | |||
| 81108bf580 | |||
| 23030a15f2 | |||
| 4ef7cb7365 | |||
| 5cc3f7db80 | |||
| 5c42cb5be4 | |||
| 004d3180b5 | |||
| c12119278a | |||
| 4d628844de | |||
| 946a209927 | |||
| 811437724b | |||
| 9dcde502aa | |||
| d3106495b2 | |||
| 891ac82447 | |||
| 354976e08e | |||
| 5c73c5813c | |||
| 3a0b6fb41b | |||
| 37d513ad59 | |||
| 46624cc953 | |||
| 0d23c8dd76 | |||
| 51ef4cd97b | |||
| b558d19d37 | |||
| b8fb6580ef | |||
| 6730eb41c2 | |||
| 87c42cb327 | |||
| 8cfd05aa95 | |||
| 3ed8271344 | |||
| 73515b5a83 | |||
| 63d3da9a54 | |||
| 215fb5e357 | |||
| 886177816b | |||
| 7d29351d66 | |||
| c0047269c1 | |||
| 793b69e592 | |||
| e0a449aa40 | |||
| d5a270f643 | |||
| d126d04d17 | |||
| 153ca6a7c6 | |||
| 2618ac9f6e | |||
| f64fd80493 | |||
| a884351137 | |||
| e546a85a8d | |||
| 29e630086c | |||
| a82165cb50 | |||
| 4dc0159a05 | |||
| 458787d1d7 | |||
| 815977296e | |||
| 58f840aa93 | |||
| 422149c249 | |||
| a7cbe68ae8 |
@@ -0,0 +1,26 @@
|
|||||||
|
name: Auto release @excalidraw/excalidraw-next
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
Auto-release-excalidraw-next:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
fetch-depth: 2
|
||||||
|
- name: Setup Node.js 14.x
|
||||||
|
uses: actions/setup-node@v2
|
||||||
|
with:
|
||||||
|
node-version: 14.x
|
||||||
|
- name: Set up publish access
|
||||||
|
run: |
|
||||||
|
npm config set //registry.npmjs.org/:_authToken ${NPM_TOKEN}
|
||||||
|
env:
|
||||||
|
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||||
|
- name: Auto release
|
||||||
|
run: |
|
||||||
|
yarn autorelease
|
||||||
@@ -102,6 +102,20 @@ These instructions will get you a copy of the project up and running on your loc
|
|||||||
git clone https://github.com/excalidraw/excalidraw.git
|
git clone https://github.com/excalidraw/excalidraw.git
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### Install the dependencies
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Start the server
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn start
|
||||||
|
```
|
||||||
|
|
||||||
|
Now you can open [http://localhost:3000](http://localhost:3000) and start coding in your favorite code editor.
|
||||||
|
|
||||||
#### Commands
|
#### Commands
|
||||||
|
|
||||||
| Command | Description |
|
| Command | Description |
|
||||||
|
|||||||
+7
-5
@@ -29,12 +29,13 @@
|
|||||||
"@types/socket.io-client": "1.4.36",
|
"@types/socket.io-client": "1.4.36",
|
||||||
"browser-fs-access": "0.16.4",
|
"browser-fs-access": "0.16.4",
|
||||||
"clsx": "1.1.1",
|
"clsx": "1.1.1",
|
||||||
"firebase": "8.3.2",
|
"firebase": "8.3.3",
|
||||||
"i18next-browser-languagedetector": "6.1.0",
|
"i18next-browser-languagedetector": "6.1.0",
|
||||||
"lodash.throttle": "4.1.1",
|
"lodash.throttle": "4.1.1",
|
||||||
"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",
|
||||||
"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",
|
||||||
@@ -43,8 +44,8 @@
|
|||||||
"react": "17.0.2",
|
"react": "17.0.2",
|
||||||
"react-dom": "17.0.2",
|
"react-dom": "17.0.2",
|
||||||
"react-scripts": "4.0.3",
|
"react-scripts": "4.0.3",
|
||||||
"roughjs": "4.3.1",
|
"roughjs": "4.4.1",
|
||||||
"sass": "1.32.8",
|
"sass": "1.32.10",
|
||||||
"socket.io-client": "2.3.1",
|
"socket.io-client": "2.3.1",
|
||||||
"typescript": "4.2.4"
|
"typescript": "4.2.4"
|
||||||
},
|
},
|
||||||
@@ -54,7 +55,7 @@
|
|||||||
"@types/lodash.throttle": "4.1.6",
|
"@types/lodash.throttle": "4.1.6",
|
||||||
"@types/pako": "1.0.1",
|
"@types/pako": "1.0.1",
|
||||||
"@types/resize-observer-browser": "0.1.5",
|
"@types/resize-observer-browser": "0.1.5",
|
||||||
"eslint-config-prettier": "8.1.0",
|
"eslint-config-prettier": "8.3.0",
|
||||||
"eslint-plugin-prettier": "3.3.1",
|
"eslint-plugin-prettier": "3.3.1",
|
||||||
"firebase-tools": "9.9.0",
|
"firebase-tools": "9.9.0",
|
||||||
"husky": "4.3.8",
|
"husky": "4.3.8",
|
||||||
@@ -103,6 +104,7 @@
|
|||||||
"test:other": "yarn prettier --list-different",
|
"test:other": "yarn prettier --list-different",
|
||||||
"test:typecheck": "tsc",
|
"test:typecheck": "tsc",
|
||||||
"test:update": "yarn test:app --updateSnapshot --watchAll=false",
|
"test:update": "yarn test:app --updateSnapshot --watchAll=false",
|
||||||
"test": "yarn test:app"
|
"test": "yarn test:app",
|
||||||
|
"autorelease": "node scripts/autorelease.js"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+21
-3
@@ -107,15 +107,17 @@
|
|||||||
|
|
||||||
<!-- FIXME: remove this when we update CRA (fix SW caching) -->
|
<!-- FIXME: remove this when we update CRA (fix SW caching) -->
|
||||||
<style>
|
<style>
|
||||||
body {
|
body,
|
||||||
|
html {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
--ui-font: system-ui, BlinkMacSystemFont, -apple-system, Segoe UI,
|
--ui-font: system-ui, BlinkMacSystemFont, -apple-system, Segoe UI,
|
||||||
Roboto, Helvetica, Arial, sans-serif;
|
Roboto, Helvetica, Arial, sans-serif;
|
||||||
font-family: var(--ui-font);
|
font-family: var(--ui-font);
|
||||||
-webkit-text-size-adjust: 100%;
|
-webkit-text-size-adjust: 100%;
|
||||||
|
|
||||||
width: 100vw;
|
width: 100%;
|
||||||
height: 100vh;
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.visually-hidden {
|
.visually-hidden {
|
||||||
@@ -125,6 +127,7 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
clip: rect(1px, 1px, 1px, 1px);
|
clip: rect(1px, 1px, 1px, 1px);
|
||||||
white-space: nowrap; /* added line */
|
white-space: nowrap; /* added line */
|
||||||
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.LoadingMessage {
|
.LoadingMessage {
|
||||||
@@ -149,6 +152,21 @@
|
|||||||
}
|
}
|
||||||
#root {
|
#root {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
-webkit-touch-callout: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-khtml-user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
-ms-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
@media screen and (min-width: 1200px) {
|
||||||
|
-webkit-touch-callout: default;
|
||||||
|
-webkit-user-select: auto;
|
||||||
|
-khtml-user-select: auto;
|
||||||
|
-moz-user-select: auto;
|
||||||
|
-ms-user-select: auto;
|
||||||
|
user-select: auto;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
const fs = require("fs");
|
||||||
|
const { exec, execSync } = require("child_process");
|
||||||
|
|
||||||
|
const excalidrawDir = `${__dirname}/../src/packages/excalidraw`;
|
||||||
|
const excalidrawPackage = `${excalidrawDir}/package.json`;
|
||||||
|
const pkg = require(excalidrawPackage);
|
||||||
|
|
||||||
|
const getShortCommitHash = () => {
|
||||||
|
return execSync("git rev-parse --short HEAD").toString().trim();
|
||||||
|
};
|
||||||
|
|
||||||
|
const publish = () => {
|
||||||
|
try {
|
||||||
|
execSync(`yarn --frozen-lockfile`);
|
||||||
|
execSync(`yarn --frozen-lockfile`, { cwd: excalidrawDir });
|
||||||
|
execSync(`yarn run build:umd`, { cwd: excalidrawDir });
|
||||||
|
execSync(`yarn --cwd ${excalidrawDir} publish`);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// get files changed between prev and head commit
|
||||||
|
exec(`git diff --name-only HEAD^ HEAD`, async (error, stdout, stderr) => {
|
||||||
|
if (error || stderr) {
|
||||||
|
console.error(error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const changedFiles = stdout.trim().split("\n");
|
||||||
|
const filesToIgnoreRegex = /src\/excalidraw-app|packages\/utils/;
|
||||||
|
|
||||||
|
const excalidrawPackageFiles = changedFiles.filter((file) => {
|
||||||
|
return file.indexOf("src") >= 0 && !filesToIgnoreRegex.test(file);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!excalidrawPackageFiles.length) {
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// update package.json
|
||||||
|
pkg.version = `${pkg.version}-${getShortCommitHash()}`;
|
||||||
|
pkg.name = "@excalidraw/excalidraw-next";
|
||||||
|
fs.writeFileSync(excalidrawPackage, JSON.stringify(pkg, null, 2), "utf8");
|
||||||
|
|
||||||
|
// update readme
|
||||||
|
const data = fs.readFileSync(`${excalidrawDir}/README_NEXT.md`, "utf8");
|
||||||
|
fs.writeFileSync(`${excalidrawDir}/README.md`, data, "utf8");
|
||||||
|
|
||||||
|
publish();
|
||||||
|
});
|
||||||
@@ -37,6 +37,8 @@ const crowdinMap = {
|
|||||||
"uk-UA": "en-uk",
|
"uk-UA": "en-uk",
|
||||||
"zh-CN": "en-zhcn",
|
"zh-CN": "en-zhcn",
|
||||||
"zh-TW": "en-zhtw",
|
"zh-TW": "en-zhtw",
|
||||||
|
"lv-LV": "en-lv",
|
||||||
|
"cs-CZ": "cs-cz",
|
||||||
};
|
};
|
||||||
|
|
||||||
const flags = {
|
const flags = {
|
||||||
@@ -74,6 +76,8 @@ const flags = {
|
|||||||
"uk-UA": "🇺🇦",
|
"uk-UA": "🇺🇦",
|
||||||
"zh-CN": "🇨🇳",
|
"zh-CN": "🇨🇳",
|
||||||
"zh-TW": "🇹🇼",
|
"zh-TW": "🇹🇼",
|
||||||
|
"lv-LV": "🇱🇻",
|
||||||
|
"cs-CZ": "🇨🇿",
|
||||||
};
|
};
|
||||||
|
|
||||||
const languages = {
|
const languages = {
|
||||||
@@ -111,6 +115,8 @@ const languages = {
|
|||||||
"uk-UA": "Українська",
|
"uk-UA": "Українська",
|
||||||
"zh-CN": "简体中文",
|
"zh-CN": "简体中文",
|
||||||
"zh-TW": "繁體中文",
|
"zh-TW": "繁體中文",
|
||||||
|
"lv-LV": "Latviešu",
|
||||||
|
"cs-CZ": "Česky",
|
||||||
};
|
};
|
||||||
|
|
||||||
const percentages = fs.readFileSync(
|
const percentages = fs.readFileSync(
|
||||||
|
|||||||
@@ -2,18 +2,20 @@ import { register } from "./register";
|
|||||||
import { getSelectedElements } from "../scene";
|
import { getSelectedElements } from "../scene";
|
||||||
import { getNonDeletedElements } from "../element";
|
import { getNonDeletedElements } from "../element";
|
||||||
import { deepCopyElement } from "../element/newElement";
|
import { deepCopyElement } from "../element/newElement";
|
||||||
import { Library } from "../data/library";
|
|
||||||
|
|
||||||
export const actionAddToLibrary = register({
|
export const actionAddToLibrary = register({
|
||||||
name: "addToLibrary",
|
name: "addToLibrary",
|
||||||
perform: (elements, appState) => {
|
perform: (elements, appState, _, app) => {
|
||||||
const selectedElements = getSelectedElements(
|
const selectedElements = getSelectedElements(
|
||||||
getNonDeletedElements(elements),
|
getNonDeletedElements(elements),
|
||||||
appState,
|
appState,
|
||||||
);
|
);
|
||||||
|
|
||||||
Library.loadLibrary().then((items) => {
|
app.library.loadLibrary().then((items) => {
|
||||||
Library.saveLibrary([...items, selectedElements.map(deepCopyElement)]);
|
app.library.saveLibrary([
|
||||||
|
...items,
|
||||||
|
selectedElements.map(deepCopyElement),
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { getDefaultAppState } from "../appState";
|
|||||||
import { ColorPicker } from "../components/ColorPicker";
|
import { ColorPicker } from "../components/ColorPicker";
|
||||||
import { resetZoom, trash, zoomIn, zoomOut } from "../components/icons";
|
import { resetZoom, trash, zoomIn, zoomOut } from "../components/icons";
|
||||||
import { ToolButton } from "../components/ToolButton";
|
import { ToolButton } from "../components/ToolButton";
|
||||||
|
import { DarkModeToggle } from "../components/DarkModeToggle";
|
||||||
import { ZOOM_STEP } from "../constants";
|
import { ZOOM_STEP } from "../constants";
|
||||||
import { getCommonBounds, getNonDeletedElements } from "../element";
|
import { getCommonBounds, getNonDeletedElements } from "../element";
|
||||||
import { newElementWith } from "../element/mutateElement";
|
import { newElementWith } from "../element/mutateElement";
|
||||||
@@ -21,8 +22,8 @@ export const actionChangeViewBackgroundColor = register({
|
|||||||
name: "changeViewBackgroundColor",
|
name: "changeViewBackgroundColor",
|
||||||
perform: (_, appState, value) => {
|
perform: (_, appState, value) => {
|
||||||
return {
|
return {
|
||||||
appState: { ...appState, viewBackgroundColor: value },
|
appState: { ...appState, ...value },
|
||||||
commitToHistory: true,
|
commitToHistory: !!value.viewBackgroundColor,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
PanelComponent: ({ appState, updateData }) => {
|
PanelComponent: ({ appState, updateData }) => {
|
||||||
@@ -32,7 +33,11 @@ export const actionChangeViewBackgroundColor = register({
|
|||||||
label={t("labels.canvasBackground")}
|
label={t("labels.canvasBackground")}
|
||||||
type="canvasBackground"
|
type="canvasBackground"
|
||||||
color={appState.viewBackgroundColor}
|
color={appState.viewBackgroundColor}
|
||||||
onChange={(color) => updateData(color)}
|
onChange={(color) => updateData({ viewBackgroundColor: color })}
|
||||||
|
isActive={appState.openMenu === "canvasColorPicker"}
|
||||||
|
setActive={(active) =>
|
||||||
|
updateData({ openMenu: active ? "canvasColorPicker" : null })
|
||||||
|
}
|
||||||
data-testid="canvas-background-picker"
|
data-testid="canvas-background-picker"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -54,7 +59,6 @@ export const actionClearCanvas = register({
|
|||||||
exportBackground: appState.exportBackground,
|
exportBackground: appState.exportBackground,
|
||||||
exportEmbedScene: appState.exportEmbedScene,
|
exportEmbedScene: appState.exportEmbedScene,
|
||||||
gridSize: appState.gridSize,
|
gridSize: appState.gridSize,
|
||||||
shouldAddWatermark: appState.shouldAddWatermark,
|
|
||||||
showStats: appState.showStats,
|
showStats: appState.showStats,
|
||||||
pasteDialog: appState.pasteDialog,
|
pasteDialog: appState.pasteDialog,
|
||||||
},
|
},
|
||||||
@@ -260,3 +264,27 @@ export const actionZoomToFit = register({
|
|||||||
!event.altKey &&
|
!event.altKey &&
|
||||||
!event[KEYS.CTRL_OR_CMD],
|
!event[KEYS.CTRL_OR_CMD],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const actionToggleTheme = register({
|
||||||
|
name: "toggleTheme",
|
||||||
|
perform: (_, appState, value) => {
|
||||||
|
return {
|
||||||
|
appState: {
|
||||||
|
...appState,
|
||||||
|
theme: value || (appState.theme === "light" ? "dark" : "light"),
|
||||||
|
},
|
||||||
|
commitToHistory: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
PanelComponent: ({ appState, updateData }) => (
|
||||||
|
<div style={{ marginInlineStart: "0.25rem" }}>
|
||||||
|
<DarkModeToggle
|
||||||
|
value={appState.theme}
|
||||||
|
onChange={(theme) => {
|
||||||
|
updateData(theme);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
keyTest: (event) => event.altKey && event.shiftKey && event.code === CODES.D,
|
||||||
|
});
|
||||||
|
|||||||
@@ -50,7 +50,6 @@ export const actionCopyAsSvg = register({
|
|||||||
? selectedElements
|
? selectedElements
|
||||||
: getNonDeletedElements(elements),
|
: getNonDeletedElements(elements),
|
||||||
appState,
|
appState,
|
||||||
app.canvas,
|
|
||||||
appState,
|
appState,
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
@@ -89,7 +88,6 @@ export const actionCopyAsPng = register({
|
|||||||
? selectedElements
|
? selectedElements
|
||||||
: getNonDeletedElements(elements),
|
: getNonDeletedElements(elements),
|
||||||
appState,
|
appState,
|
||||||
app.canvas,
|
|
||||||
appState,
|
appState,
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ 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 } from "browser-fs-access";
|
import { supported as fsSupported } from "browser-fs-access";
|
||||||
|
import { CheckboxItem } from "../components/CheckboxItem";
|
||||||
|
|
||||||
export const actionChangeProjectName = register({
|
export const actionChangeProjectName = register({
|
||||||
name: "changeProjectName",
|
name: "changeProjectName",
|
||||||
@@ -40,14 +41,12 @@ export const actionChangeExportBackground = register({
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
PanelComponent: ({ appState, updateData }) => (
|
PanelComponent: ({ appState, updateData }) => (
|
||||||
<label>
|
<CheckboxItem
|
||||||
<input
|
checked={appState.exportBackground}
|
||||||
type="checkbox"
|
onChange={(checked) => updateData(checked)}
|
||||||
checked={appState.exportBackground}
|
>
|
||||||
onChange={(event) => updateData(event.target.checked)}
|
|
||||||
/>{" "}
|
|
||||||
{t("labels.withBackground")}
|
{t("labels.withBackground")}
|
||||||
</label>
|
</CheckboxItem>
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -60,46 +59,20 @@ export const actionChangeExportEmbedScene = register({
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
PanelComponent: ({ appState, updateData }) => (
|
PanelComponent: ({ appState, updateData }) => (
|
||||||
<label style={{ display: "flex" }}>
|
<CheckboxItem
|
||||||
<input
|
checked={appState.exportEmbedScene}
|
||||||
type="checkbox"
|
onChange={(checked) => updateData(checked)}
|
||||||
checked={appState.exportEmbedScene}
|
>
|
||||||
onChange={(event) => updateData(event.target.checked)}
|
|
||||||
/>{" "}
|
|
||||||
{t("labels.exportEmbedScene")}
|
{t("labels.exportEmbedScene")}
|
||||||
<Tooltip
|
<Tooltip label={t("labels.exportEmbedScene_details")} long={true}>
|
||||||
label={t("labels.exportEmbedScene_details")}
|
<div className="Tooltip-icon">{questionCircle}</div>
|
||||||
position="above"
|
|
||||||
long={true}
|
|
||||||
>
|
|
||||||
<div className="TooltipIcon">{questionCircle}</div>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</label>
|
</CheckboxItem>
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const actionChangeShouldAddWatermark = register({
|
export const actionSaveToActiveFile = register({
|
||||||
name: "changeShouldAddWatermark",
|
name: "saveToActiveFile",
|
||||||
perform: (_elements, appState, value) => {
|
|
||||||
return {
|
|
||||||
appState: { ...appState, shouldAddWatermark: value },
|
|
||||||
commitToHistory: false,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
PanelComponent: ({ appState, updateData }) => (
|
|
||||||
<label>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={appState.shouldAddWatermark}
|
|
||||||
onChange={(event) => updateData(event.target.checked)}
|
|
||||||
/>{" "}
|
|
||||||
{t("labels.addWatermark")}
|
|
||||||
</label>
|
|
||||||
),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const actionSaveScene = register({
|
|
||||||
name: "saveScene",
|
|
||||||
perform: async (elements, appState, value) => {
|
perform: async (elements, appState, value) => {
|
||||||
const fileHandleExists = !!appState.fileHandle;
|
const fileHandleExists = !!appState.fileHandle;
|
||||||
try {
|
try {
|
||||||
@@ -130,19 +103,18 @@ export const actionSaveScene = register({
|
|||||||
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 }) => (
|
||||||
<ToolButton
|
<ToolButton
|
||||||
type="button"
|
type="icon"
|
||||||
icon={save}
|
icon={save}
|
||||||
title={t("buttons.save")}
|
title={t("buttons.save")}
|
||||||
aria-label={t("buttons.save")}
|
aria-label={t("buttons.save")}
|
||||||
showAriaLabel={useIsMobile()}
|
|
||||||
onClick={() => updateData(null)}
|
onClick={() => updateData(null)}
|
||||||
data-testid="save-button"
|
data-testid="save-button"
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const actionSaveAsScene = register({
|
export const actionSaveFileToDisk = register({
|
||||||
name: "saveAsScene",
|
name: "saveFileToDisk",
|
||||||
perform: async (elements, appState, value) => {
|
perform: async (elements, appState, value) => {
|
||||||
try {
|
try {
|
||||||
const { fileHandle } = await saveAsJSON(elements, {
|
const { fileHandle } = await saveAsJSON(elements, {
|
||||||
@@ -166,7 +138,7 @@ export const actionSaveAsScene = register({
|
|||||||
title={t("buttons.saveAs")}
|
title={t("buttons.saveAs")}
|
||||||
aria-label={t("buttons.saveAs")}
|
aria-label={t("buttons.saveAs")}
|
||||||
showAriaLabel={useIsMobile()}
|
showAriaLabel={useIsMobile()}
|
||||||
hidden={!supported}
|
hidden={!fsSupported}
|
||||||
onClick={() => updateData(null)}
|
onClick={() => updateData(null)}
|
||||||
data-testid="save-as-button"
|
data-testid="save-as-button"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import { isBindingElement } from "../element/typeChecks";
|
|||||||
|
|
||||||
export const actionFinalize = register({
|
export const actionFinalize = register({
|
||||||
name: "finalize",
|
name: "finalize",
|
||||||
perform: (elements, appState, _, { canvas }) => {
|
perform: (elements, appState, _, { canvas, focusContainer }) => {
|
||||||
if (appState.editingLinearElement) {
|
if (appState.editingLinearElement) {
|
||||||
const {
|
const {
|
||||||
elementId,
|
elementId,
|
||||||
@@ -51,19 +51,19 @@ export const actionFinalize = register({
|
|||||||
|
|
||||||
let newElements = elements;
|
let newElements = elements;
|
||||||
if (window.document.activeElement instanceof HTMLElement) {
|
if (window.document.activeElement instanceof HTMLElement) {
|
||||||
window.document.activeElement.blur();
|
focusContainer();
|
||||||
}
|
}
|
||||||
|
|
||||||
const multiPointElement = appState.multiElement
|
const multiPointElement = appState.multiElement
|
||||||
? appState.multiElement
|
? appState.multiElement
|
||||||
: appState.editingElement?.type === "draw"
|
: appState.editingElement?.type === "freedraw"
|
||||||
? appState.editingElement
|
? appState.editingElement
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
if (multiPointElement) {
|
if (multiPointElement) {
|
||||||
// pen and mouse have hover
|
// pen and mouse have hover
|
||||||
if (
|
if (
|
||||||
multiPointElement.type !== "draw" &&
|
multiPointElement.type !== "freedraw" &&
|
||||||
appState.lastPointerDownWith !== "touch"
|
appState.lastPointerDownWith !== "touch"
|
||||||
) {
|
) {
|
||||||
const { points, lastCommittedPoint } = multiPointElement;
|
const { points, lastCommittedPoint } = multiPointElement;
|
||||||
@@ -86,7 +86,7 @@ export const actionFinalize = register({
|
|||||||
const isLoop = isPathALoop(multiPointElement.points, appState.zoom.value);
|
const isLoop = isPathALoop(multiPointElement.points, appState.zoom.value);
|
||||||
if (
|
if (
|
||||||
multiPointElement.type === "line" ||
|
multiPointElement.type === "line" ||
|
||||||
multiPointElement.type === "draw"
|
multiPointElement.type === "freedraw"
|
||||||
) {
|
) {
|
||||||
if (isLoop) {
|
if (isLoop) {
|
||||||
const linePoints = multiPointElement.points;
|
const linePoints = multiPointElement.points;
|
||||||
@@ -118,22 +118,24 @@ export const actionFinalize = register({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!appState.elementLocked && appState.elementType !== "draw") {
|
if (!appState.elementLocked && appState.elementType !== "freedraw") {
|
||||||
appState.selectedElementIds[multiPointElement.id] = true;
|
appState.selectedElementIds[multiPointElement.id] = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
(!appState.elementLocked && appState.elementType !== "draw") ||
|
(!appState.elementLocked && appState.elementType !== "freedraw") ||
|
||||||
!multiPointElement
|
!multiPointElement
|
||||||
) {
|
) {
|
||||||
resetCursor(canvas);
|
resetCursor(canvas);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
elements: newElements,
|
elements: newElements,
|
||||||
appState: {
|
appState: {
|
||||||
...appState,
|
...appState,
|
||||||
elementType:
|
elementType:
|
||||||
(appState.elementLocked || appState.elementType === "draw") &&
|
(appState.elementLocked || appState.elementType === "freedraw") &&
|
||||||
multiPointElement
|
multiPointElement
|
||||||
? appState.elementType
|
? appState.elementType
|
||||||
: "selection",
|
: "selection",
|
||||||
@@ -145,14 +147,14 @@ export const actionFinalize = register({
|
|||||||
selectedElementIds:
|
selectedElementIds:
|
||||||
multiPointElement &&
|
multiPointElement &&
|
||||||
!appState.elementLocked &&
|
!appState.elementLocked &&
|
||||||
appState.elementType !== "draw"
|
appState.elementType !== "freedraw"
|
||||||
? {
|
? {
|
||||||
...appState.selectedElementIds,
|
...appState.selectedElementIds,
|
||||||
[multiPointElement.id]: true,
|
[multiPointElement.id]: true,
|
||||||
}
|
}
|
||||||
: appState.selectedElementIds,
|
: appState.selectedElementIds,
|
||||||
},
|
},
|
||||||
commitToHistory: appState.elementType === "draw",
|
commitToHistory: appState.elementType === "freedraw",
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
keyTest: (event, appState) =>
|
keyTest: (event, appState) =>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { ExcalidrawElement, NonDeleted } from "../element/types";
|
|||||||
import { normalizeAngle, resizeSingleElement } from "../element/resizeElements";
|
import { normalizeAngle, resizeSingleElement } from "../element/resizeElements";
|
||||||
import { AppState } from "../types";
|
import { AppState } from "../types";
|
||||||
import { getTransformHandles } from "../element/transformHandles";
|
import { getTransformHandles } from "../element/transformHandles";
|
||||||
import { isLinearElement } from "../element/typeChecks";
|
import { isFreeDrawElement, isLinearElement } from "../element/typeChecks";
|
||||||
import { updateBoundElements } from "../element/binding";
|
import { updateBoundElements } from "../element/binding";
|
||||||
import { LinearElementEditor } from "../element/linearElementEditor";
|
import { LinearElementEditor } from "../element/linearElementEditor";
|
||||||
|
|
||||||
@@ -114,7 +114,7 @@ const flipElement = (
|
|||||||
const originalAngle = normalizeAngle(element.angle);
|
const originalAngle = normalizeAngle(element.angle);
|
||||||
|
|
||||||
let finalOffsetX = 0;
|
let finalOffsetX = 0;
|
||||||
if (isLinearElement(element)) {
|
if (isLinearElement(element) || isFreeDrawElement(element)) {
|
||||||
finalOffsetX =
|
finalOffsetX =
|
||||||
element.points.reduce((max, point) => Math.max(max, point[0]), 0) * 2 -
|
element.points.reduce((max, point) => Math.max(max, point[0]), 0) * 2 -
|
||||||
element.width;
|
element.width;
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ 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";
|
||||||
import { SceneHistory, HistoryEntry } from "../history";
|
import History, { HistoryEntry } from "../history";
|
||||||
import { ExcalidrawElement } from "../element/types";
|
import { ExcalidrawElement } from "../element/types";
|
||||||
import { AppState } from "../types";
|
import { AppState } from "../types";
|
||||||
import { isWindows, KEYS } from "../keys";
|
import { isWindows, KEYS } from "../keys";
|
||||||
@@ -59,7 +59,7 @@ const writeData = (
|
|||||||
return { commitToHistory };
|
return { commitToHistory };
|
||||||
};
|
};
|
||||||
|
|
||||||
type ActionCreator = (history: SceneHistory) => Action;
|
type ActionCreator = (history: History) => Action;
|
||||||
|
|
||||||
export const createUndoAction: ActionCreator = (history) => ({
|
export const createUndoAction: ActionCreator = (history) => ({
|
||||||
name: "undo",
|
name: "undo",
|
||||||
|
|||||||
@@ -70,7 +70,10 @@ export const actionFullScreen = register({
|
|||||||
|
|
||||||
export const actionShortcuts = register({
|
export const actionShortcuts = register({
|
||||||
name: "toggleShortcuts",
|
name: "toggleShortcuts",
|
||||||
perform: (_elements, appState) => {
|
perform: (_elements, appState, _, { focusContainer }) => {
|
||||||
|
if (appState.showHelpDialog) {
|
||||||
|
focusContainer();
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
appState: {
|
appState: {
|
||||||
...appState,
|
...appState,
|
||||||
|
|||||||
@@ -99,13 +99,18 @@ export const actionChangeStrokeColor = register({
|
|||||||
name: "changeStrokeColor",
|
name: "changeStrokeColor",
|
||||||
perform: (elements, appState, value) => {
|
perform: (elements, appState, value) => {
|
||||||
return {
|
return {
|
||||||
elements: changeProperty(elements, appState, (el) =>
|
...(value.currentItemStrokeColor && {
|
||||||
newElementWith(el, {
|
elements: changeProperty(elements, appState, (el) =>
|
||||||
strokeColor: value,
|
newElementWith(el, {
|
||||||
}),
|
strokeColor: value.currentItemStrokeColor,
|
||||||
),
|
}),
|
||||||
appState: { ...appState, currentItemStrokeColor: value },
|
),
|
||||||
commitToHistory: true,
|
}),
|
||||||
|
appState: {
|
||||||
|
...appState,
|
||||||
|
...value,
|
||||||
|
},
|
||||||
|
commitToHistory: !!value.currentItemStrokeColor,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
PanelComponent: ({ elements, appState, updateData }) => (
|
PanelComponent: ({ elements, appState, updateData }) => (
|
||||||
@@ -120,7 +125,11 @@ export const actionChangeStrokeColor = register({
|
|||||||
(element) => element.strokeColor,
|
(element) => element.strokeColor,
|
||||||
appState.currentItemStrokeColor,
|
appState.currentItemStrokeColor,
|
||||||
)}
|
)}
|
||||||
onChange={updateData}
|
onChange={(color) => updateData({ currentItemStrokeColor: color })}
|
||||||
|
isActive={appState.openMenu === "strokeColorPicker"}
|
||||||
|
setActive={(active) =>
|
||||||
|
updateData({ openMenu: active ? "strokeColorPicker" : null })
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
@@ -130,13 +139,18 @@ export const actionChangeBackgroundColor = register({
|
|||||||
name: "changeBackgroundColor",
|
name: "changeBackgroundColor",
|
||||||
perform: (elements, appState, value) => {
|
perform: (elements, appState, value) => {
|
||||||
return {
|
return {
|
||||||
elements: changeProperty(elements, appState, (el) =>
|
...(value.currentItemBackgroundColor && {
|
||||||
newElementWith(el, {
|
elements: changeProperty(elements, appState, (el) =>
|
||||||
backgroundColor: value,
|
newElementWith(el, {
|
||||||
}),
|
backgroundColor: value.currentItemBackgroundColor,
|
||||||
),
|
}),
|
||||||
appState: { ...appState, currentItemBackgroundColor: value },
|
),
|
||||||
commitToHistory: true,
|
}),
|
||||||
|
appState: {
|
||||||
|
...appState,
|
||||||
|
...value,
|
||||||
|
},
|
||||||
|
commitToHistory: !!value.currentItemBackgroundColor,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
PanelComponent: ({ elements, appState, updateData }) => (
|
PanelComponent: ({ elements, appState, updateData }) => (
|
||||||
@@ -151,7 +165,11 @@ export const actionChangeBackgroundColor = register({
|
|||||||
(element) => element.backgroundColor,
|
(element) => element.backgroundColor,
|
||||||
appState.currentItemBackgroundColor,
|
appState.currentItemBackgroundColor,
|
||||||
)}
|
)}
|
||||||
onChange={updateData}
|
onChange={(color) => updateData({ currentItemBackgroundColor: color })}
|
||||||
|
isActive={appState.openMenu === "backgroundColorPicker"}
|
||||||
|
setActive={(active) =>
|
||||||
|
updateData({ openMenu: active ? "backgroundColorPicker" : null })
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
|
import { CODES, KEYS } from "../keys";
|
||||||
|
|
||||||
export const actionToggleStats = register({
|
export const actionToggleStats = register({
|
||||||
name: "stats",
|
name: "stats",
|
||||||
@@ -13,4 +14,6 @@ export const actionToggleStats = register({
|
|||||||
},
|
},
|
||||||
checked: (appState) => appState.showStats,
|
checked: (appState) => appState.showStats,
|
||||||
contextItemLabel: "stats.title",
|
contextItemLabel: "stats.title",
|
||||||
|
keyTest: (event) =>
|
||||||
|
!event[KEYS.CTRL_OR_CMD] && event.altKey && event.code === CODES.SLASH,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ export {
|
|||||||
actionZoomOut,
|
actionZoomOut,
|
||||||
actionResetZoom,
|
actionResetZoom,
|
||||||
actionZoomToFit,
|
actionZoomToFit,
|
||||||
|
actionToggleTheme,
|
||||||
} from "./actionCanvas";
|
} from "./actionCanvas";
|
||||||
|
|
||||||
export { actionFinalize } from "./actionFinalize";
|
export { actionFinalize } from "./actionFinalize";
|
||||||
@@ -33,8 +34,8 @@ export { actionFinalize } from "./actionFinalize";
|
|||||||
export {
|
export {
|
||||||
actionChangeProjectName,
|
actionChangeProjectName,
|
||||||
actionChangeExportBackground,
|
actionChangeExportBackground,
|
||||||
actionSaveScene,
|
actionSaveToActiveFile,
|
||||||
actionSaveAsScene,
|
actionSaveFileToDisk,
|
||||||
actionLoadScene,
|
actionLoadScene,
|
||||||
} from "./actionExport";
|
} from "./actionExport";
|
||||||
|
|
||||||
|
|||||||
@@ -9,10 +9,16 @@ import {
|
|||||||
import { ExcalidrawElement } from "../element/types";
|
import { ExcalidrawElement } from "../element/types";
|
||||||
import { AppProps, AppState } from "../types";
|
import { AppProps, AppState } from "../types";
|
||||||
import { MODES } from "../constants";
|
import { MODES } from "../constants";
|
||||||
|
import Library from "../data/library";
|
||||||
|
|
||||||
// This is the <App> component, but for now we don't care about anything but its
|
// This is the <App> component, but for now we don't care about anything but its
|
||||||
// `canvas` state.
|
// `canvas` state.
|
||||||
type App = { canvas: HTMLCanvasElement | null; props: AppProps };
|
type App = {
|
||||||
|
canvas: HTMLCanvasElement | null;
|
||||||
|
focusContainer: () => void;
|
||||||
|
props: AppProps;
|
||||||
|
library: Library;
|
||||||
|
};
|
||||||
|
|
||||||
export class ActionManager implements ActionsManagerInterface {
|
export class ActionManager implements ActionsManagerInterface {
|
||||||
actions = {} as ActionsManagerInterface["actions"];
|
actions = {} as ActionsManagerInterface["actions"];
|
||||||
@@ -51,7 +57,7 @@ export class ActionManager implements ActionsManagerInterface {
|
|||||||
actions.forEach((action) => this.registerAction(action));
|
actions.forEach((action) => this.registerAction(action));
|
||||||
}
|
}
|
||||||
|
|
||||||
handleKeyDown(event: KeyboardEvent) {
|
handleKeyDown(event: React.KeyboardEvent | KeyboardEvent) {
|
||||||
const canvasActions = this.app.props.UIOptions.canvasActions;
|
const canvasActions = this.app.props.UIOptions.canvasActions;
|
||||||
const data = Object.values(this.actions)
|
const data = Object.values(this.actions)
|
||||||
.sort((a, b) => (b.keyPriority || 0) - (a.keyPriority || 0))
|
.sort((a, b) => (b.keyPriority || 0) - (a.keyPriority || 0))
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ const shortcutMap: Record<ShortcutName, string[]> = {
|
|||||||
ungroup: [getShortcutKey("CtrlOrCmd+Shift+G")],
|
ungroup: [getShortcutKey("CtrlOrCmd+Shift+G")],
|
||||||
gridMode: [getShortcutKey("CtrlOrCmd+'")],
|
gridMode: [getShortcutKey("CtrlOrCmd+'")],
|
||||||
zenMode: [getShortcutKey("Alt+Z")],
|
zenMode: [getShortcutKey("Alt+Z")],
|
||||||
stats: [],
|
stats: [getShortcutKey("Alt+/")],
|
||||||
addToLibrary: [],
|
addToLibrary: [],
|
||||||
flipHorizontal: [getShortcutKey("Shift+H")],
|
flipHorizontal: [getShortcutKey("Shift+H")],
|
||||||
flipVertical: [getShortcutKey("Shift+V")],
|
flipVertical: [getShortcutKey("Shift+V")],
|
||||||
|
|||||||
+16
-7
@@ -1,6 +1,7 @@
|
|||||||
import React from "react";
|
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";
|
||||||
|
|
||||||
/** if false, the action should be prevented */
|
/** if false, the action should be prevented */
|
||||||
export type ActionResult =
|
export type ActionResult =
|
||||||
@@ -15,11 +16,17 @@ export type ActionResult =
|
|||||||
}
|
}
|
||||||
| false;
|
| false;
|
||||||
|
|
||||||
|
type AppAPI = {
|
||||||
|
canvas: HTMLCanvasElement | null;
|
||||||
|
focusContainer(): void;
|
||||||
|
library: Library;
|
||||||
|
};
|
||||||
|
|
||||||
type ActionFn = (
|
type ActionFn = (
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly ExcalidrawElement[],
|
||||||
appState: Readonly<AppState>,
|
appState: Readonly<AppState>,
|
||||||
formData: any,
|
formData: any,
|
||||||
app: { canvas: HTMLCanvasElement | null },
|
app: AppAPI,
|
||||||
) => ActionResult | Promise<ActionResult>;
|
) => ActionResult | Promise<ActionResult>;
|
||||||
|
|
||||||
export type UpdaterFn = (res: ActionResult) => void;
|
export type UpdaterFn = (res: ActionResult) => void;
|
||||||
@@ -45,6 +52,7 @@ export type ActionName =
|
|||||||
| "changeBackgroundColor"
|
| "changeBackgroundColor"
|
||||||
| "changeFillStyle"
|
| "changeFillStyle"
|
||||||
| "changeStrokeWidth"
|
| "changeStrokeWidth"
|
||||||
|
| "changeStrokeShape"
|
||||||
| "changeSloppiness"
|
| "changeSloppiness"
|
||||||
| "changeStrokeStyle"
|
| "changeStrokeStyle"
|
||||||
| "changeArrowhead"
|
| "changeArrowhead"
|
||||||
@@ -58,9 +66,8 @@ export type ActionName =
|
|||||||
| "changeProjectName"
|
| "changeProjectName"
|
||||||
| "changeExportBackground"
|
| "changeExportBackground"
|
||||||
| "changeExportEmbedScene"
|
| "changeExportEmbedScene"
|
||||||
| "changeShouldAddWatermark"
|
| "saveToActiveFile"
|
||||||
| "saveScene"
|
| "saveFileToDisk"
|
||||||
| "saveAsScene"
|
|
||||||
| "loadScene"
|
| "loadScene"
|
||||||
| "duplicateSelection"
|
| "duplicateSelection"
|
||||||
| "deleteSelectedElements"
|
| "deleteSelectedElements"
|
||||||
@@ -91,7 +98,8 @@ export type ActionName =
|
|||||||
| "flipHorizontal"
|
| "flipHorizontal"
|
||||||
| "flipVertical"
|
| "flipVertical"
|
||||||
| "viewMode"
|
| "viewMode"
|
||||||
| "exportWithDarkMode";
|
| "exportWithDarkMode"
|
||||||
|
| "toggleTheme";
|
||||||
|
|
||||||
export interface Action {
|
export interface Action {
|
||||||
name: ActionName;
|
name: ActionName;
|
||||||
@@ -105,7 +113,7 @@ export interface Action {
|
|||||||
perform: ActionFn;
|
perform: ActionFn;
|
||||||
keyPriority?: number;
|
keyPriority?: number;
|
||||||
keyTest?: (
|
keyTest?: (
|
||||||
event: KeyboardEvent,
|
event: React.KeyboardEvent | KeyboardEvent,
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly ExcalidrawElement[],
|
||||||
) => boolean;
|
) => boolean;
|
||||||
@@ -120,6 +128,7 @@ export interface Action {
|
|||||||
export interface ActionsManagerInterface {
|
export interface ActionsManagerInterface {
|
||||||
actions: Record<ActionName, Action>;
|
actions: Record<ActionName, Action>;
|
||||||
registerAction: (action: Action) => void;
|
registerAction: (action: Action) => void;
|
||||||
handleKeyDown: (event: KeyboardEvent) => boolean;
|
handleKeyDown: (event: React.KeyboardEvent | KeyboardEvent) => boolean;
|
||||||
renderAction: (name: ActionName) => React.ReactElement | null;
|
renderAction: (name: ActionName) => React.ReactElement | null;
|
||||||
|
executeAction: (action: Action) => void;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,7 +61,6 @@ export const getDefaultAppState = (): Omit<
|
|||||||
selectedElementIds: {},
|
selectedElementIds: {},
|
||||||
selectedGroupIds: {},
|
selectedGroupIds: {},
|
||||||
selectionElement: null,
|
selectionElement: null,
|
||||||
shouldAddWatermark: false,
|
|
||||||
shouldCacheIgnoreZoom: false,
|
shouldCacheIgnoreZoom: false,
|
||||||
showHelpDialog: false,
|
showHelpDialog: false,
|
||||||
showStats: false,
|
showStats: false,
|
||||||
@@ -141,7 +140,6 @@ const APP_STATE_STORAGE_CONF = (<
|
|||||||
selectedElementIds: { browser: true, export: false },
|
selectedElementIds: { browser: true, export: false },
|
||||||
selectedGroupIds: { browser: true, export: false },
|
selectedGroupIds: { browser: true, export: false },
|
||||||
selectionElement: { browser: false, export: false },
|
selectionElement: { browser: false, export: false },
|
||||||
shouldAddWatermark: { browser: true, export: false },
|
|
||||||
shouldCacheIgnoreZoom: { browser: true, export: false },
|
shouldCacheIgnoreZoom: { browser: true, export: false },
|
||||||
showHelpDialog: { browser: false, export: false },
|
showHelpDialog: { browser: false, export: false },
|
||||||
showStats: { browser: true, export: false },
|
showStats: { browser: true, export: false },
|
||||||
|
|||||||
+1
-3
@@ -6,7 +6,6 @@ import { getSelectedElements } from "./scene";
|
|||||||
import { AppState } from "./types";
|
import { AppState } from "./types";
|
||||||
import { SVG_EXPORT_TAG } from "./scene/export";
|
import { SVG_EXPORT_TAG } from "./scene/export";
|
||||||
import { tryParseSpreadsheet, Spreadsheet, VALID_SPREADSHEET } from "./charts";
|
import { tryParseSpreadsheet, Spreadsheet, VALID_SPREADSHEET } from "./charts";
|
||||||
import { canvasToBlob } from "./data/blob";
|
|
||||||
import { EXPORT_DATA_TYPES } from "./constants";
|
import { EXPORT_DATA_TYPES } from "./constants";
|
||||||
|
|
||||||
type ElementsClipboard = {
|
type ElementsClipboard = {
|
||||||
@@ -152,8 +151,7 @@ export const parseClipboard = async (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const copyCanvasToClipboardAsPng = async (canvas: HTMLCanvasElement) => {
|
export const copyBlobToClipboardAsPng = async (blob: Blob) => {
|
||||||
const blob = await canvasToBlob(canvas);
|
|
||||||
await navigator.clipboard.write([
|
await navigator.clipboard.write([
|
||||||
new window.ClipboardItem({ "image/png": blob }),
|
new window.ClipboardItem({ "image/png": blob }),
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ import {
|
|||||||
canHaveArrowheads,
|
canHaveArrowheads,
|
||||||
getTargetElements,
|
getTargetElements,
|
||||||
hasBackground,
|
hasBackground,
|
||||||
hasStroke,
|
hasStrokeStyle,
|
||||||
|
hasStrokeWidth,
|
||||||
hasText,
|
hasText,
|
||||||
} from "../scene";
|
} from "../scene";
|
||||||
import { SHAPES } from "../shapes";
|
import { SHAPES } from "../shapes";
|
||||||
@@ -53,10 +54,17 @@ export const SelectedShapeActions = ({
|
|||||||
{showChangeBackgroundIcons && renderAction("changeBackgroundColor")}
|
{showChangeBackgroundIcons && renderAction("changeBackgroundColor")}
|
||||||
{showFillIcons && renderAction("changeFillStyle")}
|
{showFillIcons && renderAction("changeFillStyle")}
|
||||||
|
|
||||||
{(hasStroke(elementType) ||
|
{(hasStrokeWidth(elementType) ||
|
||||||
targetElements.some((element) => hasStroke(element.type))) && (
|
targetElements.some((element) => hasStrokeWidth(element.type))) &&
|
||||||
|
renderAction("changeStrokeWidth")}
|
||||||
|
|
||||||
|
{(elementType === "freedraw" ||
|
||||||
|
targetElements.some((element) => element.type === "freedraw")) &&
|
||||||
|
renderAction("changeStrokeShape")}
|
||||||
|
|
||||||
|
{(hasStrokeStyle(elementType) ||
|
||||||
|
targetElements.some((element) => hasStrokeStyle(element.type))) && (
|
||||||
<>
|
<>
|
||||||
{renderAction("changeStrokeWidth")}
|
|
||||||
{renderAction("changeStrokeStyle")}
|
{renderAction("changeStrokeStyle")}
|
||||||
{renderAction("changeSloppiness")}
|
{renderAction("changeSloppiness")}
|
||||||
</>
|
</>
|
||||||
|
|||||||
+579
-351
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,6 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { ActionManager } from "../actions/manager";
|
import { ActionManager } from "../actions/manager";
|
||||||
import { AppState } from "../types";
|
import { AppState } from "../types";
|
||||||
import { DarkModeToggle } from "./DarkModeToggle";
|
|
||||||
|
|
||||||
export const BackgroundPickerAndDarkModeToggle = ({
|
export const BackgroundPickerAndDarkModeToggle = ({
|
||||||
appState,
|
appState,
|
||||||
@@ -16,14 +15,10 @@ export const BackgroundPickerAndDarkModeToggle = ({
|
|||||||
}) => (
|
}) => (
|
||||||
<div style={{ display: "flex" }}>
|
<div style={{ display: "flex" }}>
|
||||||
{actionManager.renderAction("changeViewBackgroundColor")}
|
{actionManager.renderAction("changeViewBackgroundColor")}
|
||||||
{showThemeBtn && (
|
{showThemeBtn && actionManager.renderAction("toggleTheme")}
|
||||||
|
{appState.fileHandle && (
|
||||||
<div style={{ marginInlineStart: "0.25rem" }}>
|
<div style={{ marginInlineStart: "0.25rem" }}>
|
||||||
<DarkModeToggle
|
{actionManager.renderAction("saveToActiveFile")}
|
||||||
value={appState.theme}
|
|
||||||
onChange={(theme) => {
|
|
||||||
setAppState({ theme });
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
@import "../css/variables.module";
|
||||||
|
|
||||||
|
.excalidraw {
|
||||||
|
.Card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
max-width: 290px;
|
||||||
|
|
||||||
|
margin: 1em;
|
||||||
|
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
.Card-icon {
|
||||||
|
font-size: 2.6em;
|
||||||
|
display: flex;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
padding: 1.4rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--card-color);
|
||||||
|
color: $oc-white;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 2.8rem;
|
||||||
|
height: 2.8rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.Card-details {
|
||||||
|
font-size: 0.96em;
|
||||||
|
min-height: 90px;
|
||||||
|
padding: 0 1em;
|
||||||
|
margin-bottom: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
& .Card-button.ToolIcon_type_button {
|
||||||
|
height: 2.5rem;
|
||||||
|
margin-top: 1em;
|
||||||
|
margin-bottom: 0.3em;
|
||||||
|
background-color: var(--card-color);
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--card-color-darker);
|
||||||
|
}
|
||||||
|
&:active {
|
||||||
|
background-color: var(--card-color-darkest);
|
||||||
|
}
|
||||||
|
.ToolIcon__label {
|
||||||
|
color: $oc-white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import OpenColor from "open-color";
|
||||||
|
|
||||||
|
import "./Card.scss";
|
||||||
|
|
||||||
|
export const Card: React.FC<{
|
||||||
|
color: keyof OpenColor;
|
||||||
|
}> = ({ children, color }) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="Card"
|
||||||
|
style={{
|
||||||
|
["--card-color" as any]: OpenColor[color][7],
|
||||||
|
["--card-color-darker" as any]: OpenColor[color][8],
|
||||||
|
["--card-color-darkest" as any]: OpenColor[color][9],
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
@import "../css/variables.module";
|
||||||
|
|
||||||
|
.excalidraw {
|
||||||
|
.Checkbox {
|
||||||
|
margin: 4px 0.3em;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
|
||||||
|
&:hover:not(.is-checked) .Checkbox-box:not(:focus) {
|
||||||
|
box-shadow: 0 0 0 2px #{$oc-blue-4};
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover:not(.is-checked) .Checkbox-box:not(:focus) {
|
||||||
|
svg {
|
||||||
|
display: block;
|
||||||
|
opacity: 0.3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
.Checkbox-box {
|
||||||
|
box-shadow: 0 0 2px 1px inset #{$oc-blue-7} !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
.Checkbox-box {
|
||||||
|
background-color: fade-out($oc-blue-1, 0.8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-checked {
|
||||||
|
.Checkbox-box {
|
||||||
|
background-color: #{$oc-blue-1};
|
||||||
|
svg {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&:hover .Checkbox-box {
|
||||||
|
background-color: #{$oc-blue-2};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.Checkbox-box {
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
padding: 0;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
|
||||||
|
margin: 0 1em;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
box-shadow: 0 0 0 2px #{$oc-blue-7};
|
||||||
|
background-color: transparent;
|
||||||
|
border-radius: 4px;
|
||||||
|
|
||||||
|
color: #{$oc-blue-7};
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
box-shadow: 0 0 0 3px #{$oc-blue-7};
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
display: none;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
stroke-width: 3px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.Checkbox-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Tooltip-icon {
|
||||||
|
width: 1em;
|
||||||
|
height: 1em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import clsx from "clsx";
|
||||||
|
import { checkIcon } from "./icons";
|
||||||
|
|
||||||
|
import "./CheckboxItem.scss";
|
||||||
|
|
||||||
|
export const CheckboxItem: React.FC<{
|
||||||
|
checked: boolean;
|
||||||
|
onChange: (checked: boolean) => void;
|
||||||
|
}> = ({ children, checked, onChange }) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={clsx("Checkbox", { "is-checked": checked })}
|
||||||
|
onClick={(event) => {
|
||||||
|
onChange(!checked);
|
||||||
|
((event.currentTarget as HTMLDivElement).querySelector(
|
||||||
|
".Checkbox-box",
|
||||||
|
) as HTMLButtonElement).focus();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<button className="Checkbox-box" role="checkbox" aria-checked={checked}>
|
||||||
|
{checkIcon}
|
||||||
|
</button>
|
||||||
|
<div className="Checkbox-label">{children}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -160,7 +160,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.color-picker-input {
|
.color-picker-input {
|
||||||
width: 12ch; /* length of `transparent` + 1 */
|
width: 11ch; /* length of `transparent` */
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
background-color: var(--input-bg-color);
|
background-color: var(--input-bg-color);
|
||||||
|
|||||||
@@ -115,6 +115,7 @@ const Picker = ({
|
|||||||
onClose();
|
onClose();
|
||||||
}
|
}
|
||||||
event.nativeEvent.stopImmediatePropagation();
|
event.nativeEvent.stopImmediatePropagation();
|
||||||
|
event.stopPropagation();
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -237,13 +238,16 @@ export const ColorPicker = ({
|
|||||||
color,
|
color,
|
||||||
onChange,
|
onChange,
|
||||||
label,
|
label,
|
||||||
|
isActive,
|
||||||
|
setActive,
|
||||||
}: {
|
}: {
|
||||||
type: "canvasBackground" | "elementBackground" | "elementStroke";
|
type: "canvasBackground" | "elementBackground" | "elementStroke";
|
||||||
color: string | null;
|
color: string | null;
|
||||||
onChange: (color: string) => void;
|
onChange: (color: string) => void;
|
||||||
label: string;
|
label: string;
|
||||||
|
isActive: boolean;
|
||||||
|
setActive: (active: boolean) => void;
|
||||||
}) => {
|
}) => {
|
||||||
const [isActive, setActive] = React.useState(false);
|
|
||||||
const pickerButton = React.useRef<HTMLButtonElement>(null);
|
const pickerButton = React.useRef<HTMLButtonElement>(null);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import "./ToolIcon.scss";
|
|||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
|
import { ToolButton } from "./ToolButton";
|
||||||
|
|
||||||
export type Appearence = "light" | "dark";
|
export type Appearence = "light" | "dark";
|
||||||
|
|
||||||
@@ -12,31 +13,19 @@ export const DarkModeToggle = (props: {
|
|||||||
onChange: (value: Appearence) => void;
|
onChange: (value: Appearence) => void;
|
||||||
title?: string;
|
title?: string;
|
||||||
}) => {
|
}) => {
|
||||||
const title = props.title
|
const title =
|
||||||
? props.title
|
props.title ||
|
||||||
: props.value === "dark"
|
(props.value === "dark" ? t("buttons.lightMode") : t("buttons.darkMode"));
|
||||||
? t("buttons.lightMode")
|
|
||||||
: t("buttons.darkMode");
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<label
|
<ToolButton
|
||||||
className="ToolIcon ToolIcon_type_floating ToolIcon_size_M"
|
type="icon"
|
||||||
data-testid="toggle-dark-mode"
|
icon={props.value === "light" ? ICONS.MOON : ICONS.SUN}
|
||||||
title={title}
|
title={title}
|
||||||
>
|
aria-label={title}
|
||||||
<input
|
onClick={() => props.onChange(props.value === "dark" ? "light" : "dark")}
|
||||||
className="ToolIcon_type_checkbox ToolIcon_toggle_opaque"
|
data-testid="toggle-dark-mode"
|
||||||
type="checkbox"
|
/>
|
||||||
onChange={(event) =>
|
|
||||||
props.onChange(event.target.checked ? "dark" : "light")
|
|
||||||
}
|
|
||||||
checked={props.value === "dark"}
|
|
||||||
aria-label={title}
|
|
||||||
/>
|
|
||||||
<div className="ToolIcon__icon">
|
|
||||||
{props.value === "light" ? ICONS.MOON : ICONS.SUN}
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import React, { useEffect } 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 { useIsMobile } from "../components/App";
|
||||||
@@ -8,6 +8,7 @@ import "./Dialog.scss";
|
|||||||
import { back, close } from "./icons";
|
import { back, close } from "./icons";
|
||||||
import { Island } from "./Island";
|
import { Island } from "./Island";
|
||||||
import { Modal } from "./Modal";
|
import { Modal } from "./Modal";
|
||||||
|
import { AppState } from "../types";
|
||||||
|
|
||||||
export const Dialog = (props: {
|
export const Dialog = (props: {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
@@ -16,8 +17,10 @@ export const Dialog = (props: {
|
|||||||
onCloseRequest(): void;
|
onCloseRequest(): void;
|
||||||
title: React.ReactNode;
|
title: React.ReactNode;
|
||||||
autofocus?: boolean;
|
autofocus?: boolean;
|
||||||
|
theme?: AppState["theme"];
|
||||||
}) => {
|
}) => {
|
||||||
const [islandNode, setIslandNode] = useCallbackRefState<HTMLDivElement>();
|
const [islandNode, setIslandNode] = useCallbackRefState<HTMLDivElement>();
|
||||||
|
const [lastActiveElement] = useState(document.activeElement);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!islandNode) {
|
if (!islandNode) {
|
||||||
@@ -65,19 +68,25 @@ export const Dialog = (props: {
|
|||||||
return focusableElements ? Array.from(focusableElements) : [];
|
return focusableElements ? Array.from(focusableElements) : [];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onClose = () => {
|
||||||
|
(lastActiveElement as HTMLElement).focus();
|
||||||
|
props.onCloseRequest();
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
className={clsx("Dialog", props.className)}
|
className={clsx("Dialog", props.className)}
|
||||||
labelledBy="dialog-title"
|
labelledBy="dialog-title"
|
||||||
maxWidth={props.small ? 550 : 800}
|
maxWidth={props.small ? 550 : 800}
|
||||||
onCloseRequest={props.onCloseRequest}
|
onCloseRequest={onClose}
|
||||||
|
theme={props.theme}
|
||||||
>
|
>
|
||||||
<Island ref={setIslandNode}>
|
<Island ref={setIslandNode}>
|
||||||
<h2 id="dialog-title" className="Dialog__title">
|
<h2 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"
|
||||||
onClick={props.onCloseRequest}
|
onClick={onClose}
|
||||||
aria-label={t("buttons.close")}
|
aria-label={t("buttons.close")}
|
||||||
>
|
>
|
||||||
{useIsMobile() ? back : close}
|
{useIsMobile() ? back : close}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import React, { useState } from "react";
|
|||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
|
|
||||||
import { Dialog } from "./Dialog";
|
import { Dialog } from "./Dialog";
|
||||||
|
import { useExcalidrawContainer } from "./App";
|
||||||
|
|
||||||
export const ErrorDialog = ({
|
export const ErrorDialog = ({
|
||||||
message,
|
message,
|
||||||
@@ -11,6 +12,7 @@ export const ErrorDialog = ({
|
|||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
}) => {
|
}) => {
|
||||||
const [modalIsShown, setModalIsShown] = useState(!!message);
|
const [modalIsShown, setModalIsShown] = useState(!!message);
|
||||||
|
const excalidrawContainer = useExcalidrawContainer();
|
||||||
|
|
||||||
const handleClose = React.useCallback(() => {
|
const handleClose = React.useCallback(() => {
|
||||||
setModalIsShown(false);
|
setModalIsShown(false);
|
||||||
@@ -18,7 +20,9 @@ export const ErrorDialog = ({
|
|||||||
if (onClose) {
|
if (onClose) {
|
||||||
onClose();
|
onClose();
|
||||||
}
|
}
|
||||||
}, [onClose]);
|
// TODO: Fix the A11y issues so this is never needed since we should always focus on last active element
|
||||||
|
excalidrawContainer?.focus();
|
||||||
|
}, [onClose, excalidrawContainer]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -28,33 +28,6 @@
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ExportDialog__name {
|
|
||||||
grid-column: project-name;
|
|
||||||
margin: auto;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
.TextInput {
|
|
||||||
height: calc(1rem - 3px);
|
|
||||||
width: 200px;
|
|
||||||
overflow: hidden;
|
|
||||||
text-align: center;
|
|
||||||
margin-left: 8px;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
|
|
||||||
&--readonly {
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
&:hover {
|
|
||||||
background: none;
|
|
||||||
}
|
|
||||||
width: auto;
|
|
||||||
max-width: 200px;
|
|
||||||
padding-left: 2px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@include isMobile {
|
@include isMobile {
|
||||||
.ExportDialog {
|
.ExportDialog {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -84,4 +57,62 @@
|
|||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ExportDialog--json {
|
||||||
|
.ExportDialog-cards {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
|
justify-items: center;
|
||||||
|
row-gap: 2em;
|
||||||
|
|
||||||
|
@media (max-width: 460px) {
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
||||||
|
.Card-details {
|
||||||
|
min-height: 40px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProjectName {
|
||||||
|
width: fit-content;
|
||||||
|
margin: 1em auto;
|
||||||
|
align-items: flex-start;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.TextInput {
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProjectName-label {
|
||||||
|
margin: 0.625em 0;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
button.ExportDialog-imageExportButton {
|
||||||
|
width: 5rem;
|
||||||
|
height: 5rem;
|
||||||
|
margin: 0 0.2em;
|
||||||
|
|
||||||
|
border-radius: 1rem;
|
||||||
|
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%);
|
||||||
|
|
||||||
|
font-family: Cascadia;
|
||||||
|
font-size: 1.8em;
|
||||||
|
color: $oc-white;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--button-color-darker);
|
||||||
|
}
|
||||||
|
&:active {
|
||||||
|
background-color: var(--button-color-darkest);
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 0.9em;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
.excalidraw {
|
.excalidraw {
|
||||||
.FixedSideContainer {
|
.FixedSideContainer {
|
||||||
--margin: 0.25rem;
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
@@ -10,9 +9,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.FixedSideContainer_side_top {
|
.FixedSideContainer_side_top {
|
||||||
left: var(--margin);
|
left: var(--space-factor);
|
||||||
top: var(--margin);
|
top: var(--space-factor);
|
||||||
right: var(--margin);
|
right: var(--space-factor);
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -23,16 +22,16 @@
|
|||||||
|
|
||||||
/* TODO: if these are used, make sure to implement RTL support
|
/* TODO: if these are used, make sure to implement RTL support
|
||||||
.FixedSideContainer_side_left {
|
.FixedSideContainer_side_left {
|
||||||
left: var(--margin);
|
left: var(--space-factor);
|
||||||
top: var(--margin);
|
top: var(--space-factor);
|
||||||
bottom: var(--margin);
|
bottom: var(--space-factor);
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.FixedSideContainer_side_right {
|
.FixedSideContainer_side_right {
|
||||||
right: var(--margin);
|
right: var(--space-factor);
|
||||||
top: var(--margin);
|
top: var(--space-factor);
|
||||||
bottom: var(--margin);
|
bottom: var(--space-factor);
|
||||||
z-index: 3;
|
z-index: 3;
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -153,7 +153,7 @@ export const HelpDialog = ({ onClose }: { onClose?: () => void }) => {
|
|||||||
<Shortcut label={t("toolBar.arrow")} shortcuts={["A", "5"]} />
|
<Shortcut label={t("toolBar.arrow")} shortcuts={["A", "5"]} />
|
||||||
<Shortcut label={t("toolBar.line")} shortcuts={["P", "6"]} />
|
<Shortcut label={t("toolBar.line")} shortcuts={["P", "6"]} />
|
||||||
<Shortcut
|
<Shortcut
|
||||||
label={t("toolBar.draw")}
|
label={t("toolBar.freedraw")}
|
||||||
shortcuts={["Shift+P", "7"]}
|
shortcuts={["Shift+P", "7"]}
|
||||||
/>
|
/>
|
||||||
<Shortcut label={t("toolBar.text")} shortcuts={["T", "8"]} />
|
<Shortcut label={t("toolBar.text")} shortcuts={["T", "8"]} />
|
||||||
@@ -231,6 +231,14 @@ export const HelpDialog = ({ onClose }: { onClose?: () => void }) => {
|
|||||||
label={t("labels.viewMode")}
|
label={t("labels.viewMode")}
|
||||||
shortcuts={[getShortcutKey("Alt+R")]}
|
shortcuts={[getShortcutKey("Alt+R")]}
|
||||||
/>
|
/>
|
||||||
|
<Shortcut
|
||||||
|
label={t("labels.toggleTheme")}
|
||||||
|
shortcuts={[getShortcutKey("Alt+Shift+D")]}
|
||||||
|
/>
|
||||||
|
<Shortcut
|
||||||
|
label={t("stats.title")}
|
||||||
|
shortcuts={[getShortcutKey("Alt+/")]}
|
||||||
|
/>
|
||||||
</ShortcutIsland>
|
</ShortcutIsland>
|
||||||
</Column>
|
</Column>
|
||||||
<Column>
|
<Column>
|
||||||
@@ -357,6 +365,14 @@ export const HelpDialog = ({ onClose }: { onClose?: () => void }) => {
|
|||||||
label={t("labels.flipVertical")}
|
label={t("labels.flipVertical")}
|
||||||
shortcuts={[getShortcutKey("Shift+V")]}
|
shortcuts={[getShortcutKey("Shift+V")]}
|
||||||
/>
|
/>
|
||||||
|
<Shortcut
|
||||||
|
label={t("labels.showStroke")}
|
||||||
|
shortcuts={[getShortcutKey("S")]}
|
||||||
|
/>
|
||||||
|
<Shortcut
|
||||||
|
label={t("labels.showBackground")}
|
||||||
|
shortcuts={[getShortcutKey("G")]}
|
||||||
|
/>
|
||||||
</ShortcutIsland>
|
</ShortcutIsland>
|
||||||
</Column>
|
</Column>
|
||||||
</Columns>
|
</Columns>
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ const getHints = ({ appState, elements }: Hint) => {
|
|||||||
return t("hints.linearElementMulti");
|
return t("hints.linearElementMulti");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (elementType === "draw") {
|
if (elementType === "freedraw") {
|
||||||
return t("hints.freeDraw");
|
return t("hints.freeDraw");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -88,6 +88,7 @@ function Picker<T>({
|
|||||||
onClose();
|
onClose();
|
||||||
}
|
}
|
||||||
event.nativeEvent.stopImmediatePropagation();
|
event.nativeEvent.stopImmediatePropagation();
|
||||||
|
event.stopPropagation();
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -6,16 +6,20 @@ import { canvasToBlob } from "../data/blob";
|
|||||||
import { NonDeletedExcalidrawElement } from "../element/types";
|
import { NonDeletedExcalidrawElement } from "../element/types";
|
||||||
import { CanvasError } from "../errors";
|
import { CanvasError } from "../errors";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { useIsMobile } from "../components/App";
|
import { useIsMobile } from "./App";
|
||||||
import { getSelectedElements, isSomeElementSelected } from "../scene";
|
import { getSelectedElements, isSomeElementSelected } from "../scene";
|
||||||
import { exportToCanvas, getExportSize } from "../scene/export";
|
import { exportToCanvas, getExportSize } from "../scene/export";
|
||||||
import { AppState } from "../types";
|
import { AppState } from "../types";
|
||||||
import { Dialog } from "./Dialog";
|
import { Dialog } from "./Dialog";
|
||||||
import "./ExportDialog.scss";
|
import { clipboard, exportImage } from "./icons";
|
||||||
import { clipboard, exportFile, link } from "./icons";
|
|
||||||
import Stack from "./Stack";
|
import Stack from "./Stack";
|
||||||
import { ToolButton } from "./ToolButton";
|
import { ToolButton } from "./ToolButton";
|
||||||
|
|
||||||
|
import "./ExportDialog.scss";
|
||||||
|
import { supported as fsSupported } from "browser-fs-access";
|
||||||
|
import OpenColor from "open-color";
|
||||||
|
import { CheckboxItem } from "./CheckboxItem";
|
||||||
|
|
||||||
const scales = [1, 2, 3];
|
const scales = [1, 2, 3];
|
||||||
const defaultScale = scales.includes(devicePixelRatio) ? devicePixelRatio : 1;
|
const defaultScale = scales.includes(devicePixelRatio) ? devicePixelRatio : 1;
|
||||||
|
|
||||||
@@ -52,7 +56,30 @@ export type ExportCB = (
|
|||||||
scale?: number,
|
scale?: number,
|
||||||
) => void;
|
) => void;
|
||||||
|
|
||||||
const ExportModal = ({
|
const ExportButton: React.FC<{
|
||||||
|
color: keyof OpenColor;
|
||||||
|
onClick: () => void;
|
||||||
|
title: string;
|
||||||
|
shade?: number;
|
||||||
|
}> = ({ children, title, onClick, color, shade = 6 }) => {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
className="ExportDialog-imageExportButton"
|
||||||
|
style={{
|
||||||
|
["--button-color" as any]: OpenColor[color][shade],
|
||||||
|
["--button-color-darker" as any]: OpenColor[color][shade + 1],
|
||||||
|
["--button-color-darkest" as any]: OpenColor[color][shade + 2],
|
||||||
|
}}
|
||||||
|
title={title}
|
||||||
|
aria-label={title}
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ImageExportModal = ({
|
||||||
elements,
|
elements,
|
||||||
appState,
|
appState,
|
||||||
exportPadding = 10,
|
exportPadding = 10,
|
||||||
@@ -60,7 +87,6 @@ const ExportModal = ({
|
|||||||
onExportToPng,
|
onExportToPng,
|
||||||
onExportToSvg,
|
onExportToSvg,
|
||||||
onExportToClipboard,
|
onExportToClipboard,
|
||||||
onExportToBackend,
|
|
||||||
}: {
|
}: {
|
||||||
appState: AppState;
|
appState: AppState;
|
||||||
elements: readonly NonDeletedExcalidrawElement[];
|
elements: readonly NonDeletedExcalidrawElement[];
|
||||||
@@ -69,18 +95,13 @@ const ExportModal = ({
|
|||||||
onExportToPng: ExportCB;
|
onExportToPng: ExportCB;
|
||||||
onExportToSvg: ExportCB;
|
onExportToSvg: ExportCB;
|
||||||
onExportToClipboard: ExportCB;
|
onExportToClipboard: ExportCB;
|
||||||
onExportToBackend?: ExportCB;
|
|
||||||
onCloseRequest: () => void;
|
onCloseRequest: () => void;
|
||||||
}) => {
|
}) => {
|
||||||
const someElementIsSelected = isSomeElementSelected(elements, appState);
|
const someElementIsSelected = isSomeElementSelected(elements, appState);
|
||||||
const [scale, setScale] = useState(defaultScale);
|
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 {
|
const { exportBackground, viewBackgroundColor } = appState;
|
||||||
exportBackground,
|
|
||||||
viewBackgroundColor,
|
|
||||||
shouldAddWatermark,
|
|
||||||
} = appState;
|
|
||||||
|
|
||||||
const exportedElements = exportSelected
|
const exportedElements = exportSelected
|
||||||
? getSelectedElements(elements, appState)
|
? getSelectedElements(elements, appState)
|
||||||
@@ -101,7 +122,6 @@ const ExportModal = ({
|
|||||||
viewBackgroundColor,
|
viewBackgroundColor,
|
||||||
exportPadding,
|
exportPadding,
|
||||||
scale,
|
scale,
|
||||||
shouldAddWatermark,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// if converting to blob fails, there's some problem that will
|
// if converting to blob fails, there's some problem that will
|
||||||
@@ -125,7 +145,6 @@ const ExportModal = ({
|
|||||||
exportPadding,
|
exportPadding,
|
||||||
viewBackgroundColor,
|
viewBackgroundColor,
|
||||||
scale,
|
scale,
|
||||||
shouldAddWatermark,
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -133,98 +152,102 @@ const ExportModal = ({
|
|||||||
<div className="ExportDialog__preview" ref={previewRef} />
|
<div className="ExportDialog__preview" ref={previewRef} />
|
||||||
{supportsContextFilters &&
|
{supportsContextFilters &&
|
||||||
actionManager.renderAction("exportWithDarkMode")}
|
actionManager.renderAction("exportWithDarkMode")}
|
||||||
<Stack.Col gap={2} align="center">
|
<div style={{ display: "grid", gridTemplateColumns: "1fr" }}>
|
||||||
<div className="ExportDialog__actions">
|
<div
|
||||||
<Stack.Row gap={2}>
|
style={{
|
||||||
<ToolButton
|
display: "grid",
|
||||||
type="button"
|
gridTemplateColumns: "repeat(auto-fit, minmax(190px, 1fr))",
|
||||||
label="PNG"
|
// dunno why this is needed, but when the items wrap it creates
|
||||||
title={t("buttons.exportToPng")}
|
// an overflow
|
||||||
aria-label={t("buttons.exportToPng")}
|
overflow: "hidden",
|
||||||
onClick={() => onExportToPng(exportedElements, scale)}
|
}}
|
||||||
/>
|
>
|
||||||
<ToolButton
|
{actionManager.renderAction("changeExportBackground")}
|
||||||
type="button"
|
{someElementIsSelected && (
|
||||||
label="SVG"
|
<CheckboxItem
|
||||||
title={t("buttons.exportToSvg")}
|
checked={exportSelected}
|
||||||
aria-label={t("buttons.exportToSvg")}
|
onChange={(checked) => setExportSelected(checked)}
|
||||||
onClick={() => onExportToSvg(exportedElements, scale)}
|
>
|
||||||
/>
|
|
||||||
{probablySupportsClipboardBlob && (
|
|
||||||
<ToolButton
|
|
||||||
type="button"
|
|
||||||
icon={clipboard}
|
|
||||||
title={t("buttons.copyPngToClipboard")}
|
|
||||||
aria-label={t("buttons.copyPngToClipboard")}
|
|
||||||
onClick={() => onExportToClipboard(exportedElements, scale)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{onExportToBackend && (
|
|
||||||
<ToolButton
|
|
||||||
type="button"
|
|
||||||
icon={link}
|
|
||||||
title={t("buttons.getShareableLink")}
|
|
||||||
aria-label={t("buttons.getShareableLink")}
|
|
||||||
onClick={() => onExportToBackend(exportedElements)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Stack.Row>
|
|
||||||
<div className="ExportDialog__name">
|
|
||||||
{actionManager.renderAction("changeProjectName")}
|
|
||||||
</div>
|
|
||||||
<Stack.Row gap={2}>
|
|
||||||
{scales.map((s) => {
|
|
||||||
const [width, height] = getExportSize(
|
|
||||||
exportedElements,
|
|
||||||
exportPadding,
|
|
||||||
shouldAddWatermark,
|
|
||||||
s,
|
|
||||||
);
|
|
||||||
|
|
||||||
const scaleButtonTitle = `${t(
|
|
||||||
"buttons.scale",
|
|
||||||
)} ${s}x (${width}x${height})`;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ToolButton
|
|
||||||
key={s}
|
|
||||||
size="s"
|
|
||||||
type="radio"
|
|
||||||
icon={`${s}x`}
|
|
||||||
name="export-canvas-scale"
|
|
||||||
title={scaleButtonTitle}
|
|
||||||
aria-label={scaleButtonTitle}
|
|
||||||
id="export-canvas-scale"
|
|
||||||
checked={s === scale}
|
|
||||||
onChange={() => setScale(s)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Stack.Row>
|
|
||||||
</div>
|
|
||||||
{actionManager.renderAction("changeExportBackground")}
|
|
||||||
{someElementIsSelected && (
|
|
||||||
<div>
|
|
||||||
<label>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={exportSelected}
|
|
||||||
onChange={(event) =>
|
|
||||||
setExportSelected(event.currentTarget.checked)
|
|
||||||
}
|
|
||||||
/>{" "}
|
|
||||||
{t("labels.onlySelected")}
|
{t("labels.onlySelected")}
|
||||||
</label>
|
</CheckboxItem>
|
||||||
</div>
|
)}
|
||||||
|
{actionManager.renderAction("changeExportEmbedScene")}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: "flex", alignItems: "center", marginTop: ".6em" }}>
|
||||||
|
<Stack.Row gap={2} justifyContent={"center"}>
|
||||||
|
{scales.map((_scale) => {
|
||||||
|
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>
|
||||||
|
<p style={{ marginLeft: "1em", userSelect: "none" }}>Scale</p>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
margin: ".6em 0",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{!fsSupported && actionManager.renderAction("changeProjectName")}
|
||||||
|
</div>
|
||||||
|
<Stack.Row gap={2} justifyContent="center" style={{ margin: "2em 0" }}>
|
||||||
|
<ExportButton
|
||||||
|
color="indigo"
|
||||||
|
title={t("buttons.exportToPng")}
|
||||||
|
aria-label={t("buttons.exportToPng")}
|
||||||
|
onClick={() => onExportToPng(exportedElements, scale)}
|
||||||
|
>
|
||||||
|
PNG
|
||||||
|
</ExportButton>
|
||||||
|
<ExportButton
|
||||||
|
color="red"
|
||||||
|
title={t("buttons.exportToSvg")}
|
||||||
|
aria-label={t("buttons.exportToSvg")}
|
||||||
|
onClick={() => onExportToSvg(exportedElements, scale)}
|
||||||
|
>
|
||||||
|
SVG
|
||||||
|
</ExportButton>
|
||||||
|
{probablySupportsClipboardBlob && (
|
||||||
|
<ExportButton
|
||||||
|
title={t("buttons.copyPngToClipboard")}
|
||||||
|
onClick={() => onExportToClipboard(exportedElements, scale)}
|
||||||
|
color="gray"
|
||||||
|
shade={7}
|
||||||
|
>
|
||||||
|
{clipboard}
|
||||||
|
</ExportButton>
|
||||||
)}
|
)}
|
||||||
{actionManager.renderAction("changeExportEmbedScene")}
|
</Stack.Row>
|
||||||
{actionManager.renderAction("changeShouldAddWatermark")}
|
|
||||||
</Stack.Col>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ExportDialog = ({
|
export const ImageExportDialog = ({
|
||||||
elements,
|
elements,
|
||||||
appState,
|
appState,
|
||||||
exportPadding = 10,
|
exportPadding = 10,
|
||||||
@@ -232,7 +255,6 @@ export const ExportDialog = ({
|
|||||||
onExportToPng,
|
onExportToPng,
|
||||||
onExportToSvg,
|
onExportToSvg,
|
||||||
onExportToClipboard,
|
onExportToClipboard,
|
||||||
onExportToBackend,
|
|
||||||
}: {
|
}: {
|
||||||
appState: AppState;
|
appState: AppState;
|
||||||
elements: readonly NonDeletedExcalidrawElement[];
|
elements: readonly NonDeletedExcalidrawElement[];
|
||||||
@@ -241,14 +263,11 @@ export const ExportDialog = ({
|
|||||||
onExportToPng: ExportCB;
|
onExportToPng: ExportCB;
|
||||||
onExportToSvg: ExportCB;
|
onExportToSvg: ExportCB;
|
||||||
onExportToClipboard: ExportCB;
|
onExportToClipboard: ExportCB;
|
||||||
onExportToBackend?: ExportCB;
|
|
||||||
}) => {
|
}) => {
|
||||||
const [modalIsShown, setModalIsShown] = useState(false);
|
const [modalIsShown, setModalIsShown] = useState(false);
|
||||||
const triggerButton = useRef<HTMLButtonElement>(null);
|
|
||||||
|
|
||||||
const handleClose = React.useCallback(() => {
|
const handleClose = React.useCallback(() => {
|
||||||
setModalIsShown(false);
|
setModalIsShown(false);
|
||||||
triggerButton.current?.focus();
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -257,17 +276,16 @@ export const ExportDialog = ({
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
setModalIsShown(true);
|
setModalIsShown(true);
|
||||||
}}
|
}}
|
||||||
data-testid="export-button"
|
data-testid="image-export-button"
|
||||||
icon={exportFile}
|
icon={exportImage}
|
||||||
type="button"
|
type="button"
|
||||||
aria-label={t("buttons.export")}
|
aria-label={t("buttons.exportImage")}
|
||||||
showAriaLabel={useIsMobile()}
|
showAriaLabel={useIsMobile()}
|
||||||
title={t("buttons.export")}
|
title={t("buttons.exportImage")}
|
||||||
ref={triggerButton}
|
|
||||||
/>
|
/>
|
||||||
{modalIsShown && (
|
{modalIsShown && (
|
||||||
<Dialog onCloseRequest={handleClose} title={t("buttons.export")}>
|
<Dialog onCloseRequest={handleClose} title={t("buttons.exportImage")}>
|
||||||
<ExportModal
|
<ImageExportModal
|
||||||
elements={elements}
|
elements={elements}
|
||||||
appState={appState}
|
appState={appState}
|
||||||
exportPadding={exportPadding}
|
exportPadding={exportPadding}
|
||||||
@@ -275,7 +293,6 @@ export const ExportDialog = ({
|
|||||||
onExportToPng={onExportToPng}
|
onExportToPng={onExportToPng}
|
||||||
onExportToSvg={onExportToSvg}
|
onExportToSvg={onExportToSvg}
|
||||||
onExportToClipboard={onExportToClipboard}
|
onExportToClipboard={onExportToClipboard}
|
||||||
onExportToBackend={onExportToBackend}
|
|
||||||
onCloseRequest={handleClose}
|
onCloseRequest={handleClose}
|
||||||
/>
|
/>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
@@ -2,7 +2,6 @@
|
|||||||
.Island {
|
.Island {
|
||||||
--padding: 0;
|
--padding: 0;
|
||||||
background-color: var(--island-bg-color);
|
background-color: var(--island-bg-color);
|
||||||
backdrop-filter: saturate(100%) blur(10px);
|
|
||||||
box-shadow: var(--shadow-island);
|
box-shadow: var(--shadow-island);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
padding: calc(var(--padding) * var(--space-factor));
|
padding: calc(var(--padding) * var(--space-factor));
|
||||||
|
|||||||
@@ -0,0 +1,127 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import { ActionsManagerInterface } from "../actions/types";
|
||||||
|
import { NonDeletedExcalidrawElement } from "../element/types";
|
||||||
|
import { t } from "../i18n";
|
||||||
|
import { useIsMobile } from "./App";
|
||||||
|
import { AppState, ExportOpts } from "../types";
|
||||||
|
import { Dialog } from "./Dialog";
|
||||||
|
import { exportFile, exportToFileIcon, link } from "./icons";
|
||||||
|
import { ToolButton } from "./ToolButton";
|
||||||
|
import { actionSaveFileToDisk } from "../actions/actionExport";
|
||||||
|
import { Card } from "./Card";
|
||||||
|
|
||||||
|
import "./ExportDialog.scss";
|
||||||
|
import { supported as fsSupported } from "browser-fs-access";
|
||||||
|
|
||||||
|
export type ExportCB = (
|
||||||
|
elements: readonly NonDeletedExcalidrawElement[],
|
||||||
|
scale?: number,
|
||||||
|
) => void;
|
||||||
|
|
||||||
|
const JSONExportModal = ({
|
||||||
|
elements,
|
||||||
|
appState,
|
||||||
|
actionManager,
|
||||||
|
exportOpts,
|
||||||
|
canvas,
|
||||||
|
}: {
|
||||||
|
appState: AppState;
|
||||||
|
elements: readonly NonDeletedExcalidrawElement[];
|
||||||
|
actionManager: ActionsManagerInterface;
|
||||||
|
onCloseRequest: () => void;
|
||||||
|
exportOpts: ExportOpts;
|
||||||
|
canvas: HTMLCanvasElement | null;
|
||||||
|
}) => {
|
||||||
|
const { onExportToBackend } = exportOpts;
|
||||||
|
return (
|
||||||
|
<div className="ExportDialog ExportDialog--json">
|
||||||
|
<div className="ExportDialog-cards">
|
||||||
|
{exportOpts.saveFileToDisk && (
|
||||||
|
<Card color="lime">
|
||||||
|
<div className="Card-icon">{exportToFileIcon}</div>
|
||||||
|
<h2>{t("exportDialog.disk_title")}</h2>
|
||||||
|
<div className="Card-details">
|
||||||
|
{t("exportDialog.disk_details")}
|
||||||
|
{!fsSupported && actionManager.renderAction("changeProjectName")}
|
||||||
|
</div>
|
||||||
|
<ToolButton
|
||||||
|
className="Card-button"
|
||||||
|
type="button"
|
||||||
|
title={t("exportDialog.disk_button")}
|
||||||
|
aria-label={t("exportDialog.disk_button")}
|
||||||
|
showAriaLabel={true}
|
||||||
|
onClick={() => {
|
||||||
|
actionManager.executeAction(actionSaveFileToDisk);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
{onExportToBackend && (
|
||||||
|
<Card color="pink">
|
||||||
|
<div className="Card-icon">{link}</div>
|
||||||
|
<h2>{t("exportDialog.link_title")}</h2>
|
||||||
|
<div className="Card-details">{t("exportDialog.link_details")}</div>
|
||||||
|
<ToolButton
|
||||||
|
className="Card-button"
|
||||||
|
type="button"
|
||||||
|
title={t("exportDialog.link_button")}
|
||||||
|
aria-label={t("exportDialog.link_button")}
|
||||||
|
showAriaLabel={true}
|
||||||
|
onClick={() => onExportToBackend(elements, appState, canvas)}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
{exportOpts.renderCustomUI &&
|
||||||
|
exportOpts.renderCustomUI(elements, appState, canvas)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const JSONExportDialog = ({
|
||||||
|
elements,
|
||||||
|
appState,
|
||||||
|
actionManager,
|
||||||
|
exportOpts,
|
||||||
|
canvas,
|
||||||
|
}: {
|
||||||
|
appState: AppState;
|
||||||
|
elements: readonly NonDeletedExcalidrawElement[];
|
||||||
|
actionManager: ActionsManagerInterface;
|
||||||
|
exportOpts: ExportOpts;
|
||||||
|
canvas: HTMLCanvasElement | null;
|
||||||
|
}) => {
|
||||||
|
const [modalIsShown, setModalIsShown] = useState(false);
|
||||||
|
|
||||||
|
const handleClose = React.useCallback(() => {
|
||||||
|
setModalIsShown(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ToolButton
|
||||||
|
onClick={() => {
|
||||||
|
setModalIsShown(true);
|
||||||
|
}}
|
||||||
|
data-testid="json-export-button"
|
||||||
|
icon={exportFile}
|
||||||
|
type="button"
|
||||||
|
aria-label={t("buttons.export")}
|
||||||
|
showAriaLabel={useIsMobile()}
|
||||||
|
title={t("buttons.export")}
|
||||||
|
/>
|
||||||
|
{modalIsShown && (
|
||||||
|
<Dialog onCloseRequest={handleClose} title={t("buttons.export")}>
|
||||||
|
<JSONExportModal
|
||||||
|
elements={elements}
|
||||||
|
appState={appState}
|
||||||
|
actionManager={actionManager}
|
||||||
|
onCloseRequest={handleClose}
|
||||||
|
exportOpts={exportOpts}
|
||||||
|
canvas={canvas}
|
||||||
|
/>
|
||||||
|
</Dialog>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
+22
-40
@@ -40,50 +40,17 @@
|
|||||||
.layer-ui__wrapper {
|
.layer-ui__wrapper {
|
||||||
z-index: var(--zIndex-layerUI);
|
z-index: var(--zIndex-layerUI);
|
||||||
|
|
||||||
.encrypted-icon {
|
&__top-right {
|
||||||
position: relative;
|
|
||||||
margin-inline-start: 15px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
border-radius: var(--space-factor);
|
|
||||||
color: $oc-green-9;
|
|
||||||
|
|
||||||
svg {
|
|
||||||
width: 1.2rem;
|
|
||||||
height: 1.2rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__github-corner {
|
|
||||||
top: 0;
|
|
||||||
|
|
||||||
:root[dir="ltr"] & {
|
|
||||||
right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[dir="rtl"] & {
|
|
||||||
left: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
position: absolute;
|
|
||||||
width: 40px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&__footer {
|
&__footer {
|
||||||
position: absolute;
|
width: 100%;
|
||||||
z-index: 100;
|
|
||||||
bottom: 0;
|
|
||||||
|
|
||||||
:root[dir="ltr"] & {
|
&-right {
|
||||||
right: 0;
|
z-index: 100;
|
||||||
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
:root[dir="rtl"] & {
|
|
||||||
left: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
width: 190px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.zen-mode-transition {
|
.zen-mode-transition {
|
||||||
@@ -105,12 +72,16 @@
|
|||||||
transform: translate(-999px, 0);
|
transform: translate(-999px, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
:root[dir="ltr"] &.App-menu_bottom--transition-left {
|
:root[dir="ltr"] &.layer-ui__wrapper__footer-left--transition-left {
|
||||||
transform: translate(-92px, 0);
|
transform: translate(-92px, 0);
|
||||||
}
|
}
|
||||||
:root[dir="rtl"] &.App-menu_bottom--transition-left {
|
:root[dir="rtl"] &.layer-ui__wrapper__footer-left--transition-left {
|
||||||
transform: translate(92px, 0);
|
transform: translate(92px, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.layer-ui__wrapper__footer-left--transition-bottom {
|
||||||
|
transform: translate(0, 92px);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.disable-zen-mode {
|
.disable-zen-mode {
|
||||||
@@ -137,5 +108,16 @@
|
|||||||
transition-delay: 0.8s;
|
transition-delay: 0.8s;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.layer-ui__wrapper__footer-center {
|
||||||
|
pointer-events: none;
|
||||||
|
& > * {
|
||||||
|
pointer-events: all;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.layer-ui__wrapper__footer-left,
|
||||||
|
.layer-ui__wrapper__footer-right {
|
||||||
|
pointer-events: all;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+185
-153
@@ -10,7 +10,6 @@ import { ActionManager } from "../actions/manager";
|
|||||||
import { CLASSES } from "../constants";
|
import { CLASSES } from "../constants";
|
||||||
import { exportCanvas } from "../data";
|
import { exportCanvas } from "../data";
|
||||||
import { importLibraryFromJSON, saveLibraryAsJSON } from "../data/json";
|
import { importLibraryFromJSON, saveLibraryAsJSON } from "../data/json";
|
||||||
import { Library } from "../data/library";
|
|
||||||
import { isTextElement, showSelectedShapeActions } from "../element";
|
import { isTextElement, showSelectedShapeActions } from "../element";
|
||||||
import { NonDeletedExcalidrawElement } from "../element/types";
|
import { NonDeletedExcalidrawElement } from "../element/types";
|
||||||
import { Language, t } from "../i18n";
|
import { Language, t } from "../i18n";
|
||||||
@@ -29,11 +28,10 @@ import { SelectedShapeActions, ShapesSwitcher, ZoomActions } from "./Actions";
|
|||||||
import { BackgroundPickerAndDarkModeToggle } from "./BackgroundPickerAndDarkModeToggle";
|
import { BackgroundPickerAndDarkModeToggle } from "./BackgroundPickerAndDarkModeToggle";
|
||||||
import CollabButton from "./CollabButton";
|
import CollabButton from "./CollabButton";
|
||||||
import { ErrorDialog } from "./ErrorDialog";
|
import { ErrorDialog } from "./ErrorDialog";
|
||||||
import { ExportCB, ExportDialog } from "./ExportDialog";
|
import { ExportCB, ImageExportDialog } from "./ImageExportDialog";
|
||||||
import { FixedSideContainer } from "./FixedSideContainer";
|
import { FixedSideContainer } from "./FixedSideContainer";
|
||||||
import { GitHubCorner } from "./GitHubCorner";
|
|
||||||
import { HintViewer } from "./HintViewer";
|
import { HintViewer } from "./HintViewer";
|
||||||
import { exportFile, load, shield, trash } from "./icons";
|
import { exportFile, load, trash } from "./icons";
|
||||||
import { Island } from "./Island";
|
import { Island } from "./Island";
|
||||||
import "./LayerUI.scss";
|
import "./LayerUI.scss";
|
||||||
import { LibraryUnit } from "./LibraryUnit";
|
import { LibraryUnit } from "./LibraryUnit";
|
||||||
@@ -47,6 +45,8 @@ import Stack from "./Stack";
|
|||||||
import { ToolButton } from "./ToolButton";
|
import { ToolButton } from "./ToolButton";
|
||||||
import { Tooltip } from "./Tooltip";
|
import { Tooltip } from "./Tooltip";
|
||||||
import { UserList } from "./UserList";
|
import { UserList } from "./UserList";
|
||||||
|
import Library from "../data/library";
|
||||||
|
import { JSONExportDialog } from "./JSONExportDialog";
|
||||||
|
|
||||||
interface LayerUIProps {
|
interface LayerUIProps {
|
||||||
actionManager: ActionManager;
|
actionManager: ActionManager;
|
||||||
@@ -63,15 +63,14 @@ interface LayerUIProps {
|
|||||||
toggleZenMode: () => void;
|
toggleZenMode: () => void;
|
||||||
langCode: Language["code"];
|
langCode: Language["code"];
|
||||||
isCollaborating: boolean;
|
isCollaborating: boolean;
|
||||||
onExportToBackend?: (
|
renderTopRightUI?: (isMobile: boolean, appState: AppState) => JSX.Element;
|
||||||
exportedElements: readonly NonDeletedExcalidrawElement[],
|
renderCustomFooter?: (isMobile: boolean, appState: AppState) => JSX.Element;
|
||||||
appState: AppState,
|
|
||||||
canvas: HTMLCanvasElement | null,
|
|
||||||
) => void;
|
|
||||||
renderCustomFooter?: (isMobile: boolean) => JSX.Element;
|
|
||||||
viewModeEnabled: boolean;
|
viewModeEnabled: boolean;
|
||||||
libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"];
|
libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"];
|
||||||
UIOptions: AppProps["UIOptions"];
|
UIOptions: AppProps["UIOptions"];
|
||||||
|
focusContainer: () => void;
|
||||||
|
library: Library;
|
||||||
|
id: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const useOnClickOutside = (
|
const useOnClickOutside = (
|
||||||
@@ -103,7 +102,7 @@ const useOnClickOutside = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
const LibraryMenuItems = ({
|
const LibraryMenuItems = ({
|
||||||
library,
|
libraryItems,
|
||||||
onRemoveFromLibrary,
|
onRemoveFromLibrary,
|
||||||
onAddToLibrary,
|
onAddToLibrary,
|
||||||
onInsertShape,
|
onInsertShape,
|
||||||
@@ -111,8 +110,11 @@ const LibraryMenuItems = ({
|
|||||||
setAppState,
|
setAppState,
|
||||||
setLibraryItems,
|
setLibraryItems,
|
||||||
libraryReturnUrl,
|
libraryReturnUrl,
|
||||||
|
focusContainer,
|
||||||
|
library,
|
||||||
|
id,
|
||||||
}: {
|
}: {
|
||||||
library: LibraryItems;
|
libraryItems: LibraryItems;
|
||||||
pendingElements: LibraryItem;
|
pendingElements: LibraryItem;
|
||||||
onRemoveFromLibrary: (index: number) => void;
|
onRemoveFromLibrary: (index: number) => void;
|
||||||
onInsertShape: (elements: LibraryItem) => void;
|
onInsertShape: (elements: LibraryItem) => void;
|
||||||
@@ -120,9 +122,12 @@ const LibraryMenuItems = ({
|
|||||||
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"];
|
||||||
|
focusContainer: () => void;
|
||||||
|
library: Library;
|
||||||
|
id: string;
|
||||||
}) => {
|
}) => {
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
const numCells = library.length + (pendingElements.length > 0 ? 1 : 0);
|
const numCells = libraryItems.length + (pendingElements.length > 0 ? 1 : 0);
|
||||||
const CELLS_PER_ROW = isMobile ? 4 : 6;
|
const CELLS_PER_ROW = isMobile ? 4 : 6;
|
||||||
const numRows = Math.max(1, Math.ceil(numCells / CELLS_PER_ROW));
|
const numRows = Math.max(1, Math.ceil(numCells / CELLS_PER_ROW));
|
||||||
const rows = [];
|
const rows = [];
|
||||||
@@ -140,7 +145,7 @@ const LibraryMenuItems = ({
|
|||||||
aria-label={t("buttons.load")}
|
aria-label={t("buttons.load")}
|
||||||
icon={load}
|
icon={load}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
importLibraryFromJSON()
|
importLibraryFromJSON(library)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
// Close and then open to get the libraries updated
|
// Close and then open to get the libraries updated
|
||||||
setAppState({ isLibraryOpen: false });
|
setAppState({ isLibraryOpen: false });
|
||||||
@@ -152,7 +157,7 @@ const LibraryMenuItems = ({
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{!!library.length && (
|
{!!libraryItems.length && (
|
||||||
<>
|
<>
|
||||||
<ToolButton
|
<ToolButton
|
||||||
key="export"
|
key="export"
|
||||||
@@ -161,7 +166,7 @@ const LibraryMenuItems = ({
|
|||||||
aria-label={t("buttons.export")}
|
aria-label={t("buttons.export")}
|
||||||
icon={exportFile}
|
icon={exportFile}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
saveLibraryAsJSON()
|
saveLibraryAsJSON(library)
|
||||||
.catch(muteFSAbortError)
|
.catch(muteFSAbortError)
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
setAppState({ errorMessage: error.message });
|
setAppState({ errorMessage: error.message });
|
||||||
@@ -176,8 +181,9 @@ const LibraryMenuItems = ({
|
|||||||
icon={trash}
|
icon={trash}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (window.confirm(t("alerts.resetLibrary"))) {
|
if (window.confirm(t("alerts.resetLibrary"))) {
|
||||||
Library.resetLibrary();
|
library.resetLibrary();
|
||||||
setLibraryItems([]);
|
setLibraryItems([]);
|
||||||
|
focusContainer();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -186,7 +192,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=${Library.csrfToken}`}
|
}&referrer=${referrer}&useHash=true&token=${id}`}
|
||||||
target="_excalidraw_libraries"
|
target="_excalidraw_libraries"
|
||||||
>
|
>
|
||||||
{t("labels.libraries")}
|
{t("labels.libraries")}
|
||||||
@@ -201,13 +207,13 @@ const LibraryMenuItems = ({
|
|||||||
const shouldAddPendingElements: boolean =
|
const shouldAddPendingElements: boolean =
|
||||||
pendingElements.length > 0 &&
|
pendingElements.length > 0 &&
|
||||||
!addedPendingElements &&
|
!addedPendingElements &&
|
||||||
y + x >= library.length;
|
y + x >= libraryItems.length;
|
||||||
addedPendingElements = addedPendingElements || shouldAddPendingElements;
|
addedPendingElements = addedPendingElements || shouldAddPendingElements;
|
||||||
|
|
||||||
children.push(
|
children.push(
|
||||||
<Stack.Col key={x}>
|
<Stack.Col key={x}>
|
||||||
<LibraryUnit
|
<LibraryUnit
|
||||||
elements={library[y + x]}
|
elements={libraryItems[y + x]}
|
||||||
pendingElements={
|
pendingElements={
|
||||||
shouldAddPendingElements ? pendingElements : undefined
|
shouldAddPendingElements ? pendingElements : undefined
|
||||||
}
|
}
|
||||||
@@ -215,7 +221,7 @@ const LibraryMenuItems = ({
|
|||||||
onClick={
|
onClick={
|
||||||
shouldAddPendingElements
|
shouldAddPendingElements
|
||||||
? onAddToLibrary.bind(null, pendingElements)
|
? onAddToLibrary.bind(null, pendingElements)
|
||||||
: onInsertShape.bind(null, library[y + x])
|
: onInsertShape.bind(null, libraryItems[y + x])
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</Stack.Col>,
|
</Stack.Col>,
|
||||||
@@ -242,6 +248,9 @@ const LibraryMenu = ({
|
|||||||
onAddToLibrary,
|
onAddToLibrary,
|
||||||
setAppState,
|
setAppState,
|
||||||
libraryReturnUrl,
|
libraryReturnUrl,
|
||||||
|
focusContainer,
|
||||||
|
library,
|
||||||
|
id,
|
||||||
}: {
|
}: {
|
||||||
pendingElements: LibraryItem;
|
pendingElements: LibraryItem;
|
||||||
onClickOutside: (event: MouseEvent) => void;
|
onClickOutside: (event: MouseEvent) => void;
|
||||||
@@ -249,6 +258,9 @@ const LibraryMenu = ({
|
|||||||
onAddToLibrary: () => void;
|
onAddToLibrary: () => void;
|
||||||
setAppState: React.Component<any, AppState>["setState"];
|
setAppState: React.Component<any, AppState>["setState"];
|
||||||
libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"];
|
libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"];
|
||||||
|
focusContainer: () => void;
|
||||||
|
library: Library;
|
||||||
|
id: string;
|
||||||
}) => {
|
}) => {
|
||||||
const ref = useRef<HTMLDivElement | null>(null);
|
const ref = useRef<HTMLDivElement | null>(null);
|
||||||
useOnClickOutside(ref, (event) => {
|
useOnClickOutside(ref, (event) => {
|
||||||
@@ -274,7 +286,7 @@ const LibraryMenu = ({
|
|||||||
resolve("loading");
|
resolve("loading");
|
||||||
}, 100);
|
}, 100);
|
||||||
}),
|
}),
|
||||||
Library.loadLibrary().then((items) => {
|
library.loadLibrary().then((items) => {
|
||||||
setLibraryItems(items);
|
setLibraryItems(items);
|
||||||
setIsLoading("ready");
|
setIsLoading("ready");
|
||||||
}),
|
}),
|
||||||
@@ -286,24 +298,33 @@ const LibraryMenu = ({
|
|||||||
return () => {
|
return () => {
|
||||||
clearTimeout(loadingTimerRef.current!);
|
clearTimeout(loadingTimerRef.current!);
|
||||||
};
|
};
|
||||||
}, []);
|
}, [library]);
|
||||||
|
|
||||||
const removeFromLibrary = useCallback(async (indexToRemove) => {
|
const removeFromLibrary = useCallback(
|
||||||
const items = await Library.loadLibrary();
|
async (indexToRemove) => {
|
||||||
const nextItems = items.filter((_, index) => index !== indexToRemove);
|
const items = await library.loadLibrary();
|
||||||
Library.saveLibrary(nextItems);
|
const nextItems = items.filter((_, index) => index !== indexToRemove);
|
||||||
setLibraryItems(nextItems);
|
library.saveLibrary(nextItems).catch((error) => {
|
||||||
}, []);
|
setLibraryItems(items);
|
||||||
|
setAppState({ errorMessage: t("alerts.errorRemovingFromLibrary") });
|
||||||
|
});
|
||||||
|
setLibraryItems(nextItems);
|
||||||
|
},
|
||||||
|
[library, setAppState],
|
||||||
|
);
|
||||||
|
|
||||||
const addToLibrary = useCallback(
|
const addToLibrary = useCallback(
|
||||||
async (elements: LibraryItem) => {
|
async (elements: LibraryItem) => {
|
||||||
const items = await Library.loadLibrary();
|
const items = await library.loadLibrary();
|
||||||
const nextItems = [...items, elements];
|
const nextItems = [...items, elements];
|
||||||
onAddToLibrary();
|
onAddToLibrary();
|
||||||
Library.saveLibrary(nextItems);
|
library.saveLibrary(nextItems).catch((error) => {
|
||||||
|
setLibraryItems(items);
|
||||||
|
setAppState({ errorMessage: t("alerts.errorAddingToLibrary") });
|
||||||
|
});
|
||||||
setLibraryItems(nextItems);
|
setLibraryItems(nextItems);
|
||||||
},
|
},
|
||||||
[onAddToLibrary],
|
[onAddToLibrary, library, setAppState],
|
||||||
);
|
);
|
||||||
|
|
||||||
return loadingState === "preloading" ? null : (
|
return loadingState === "preloading" ? null : (
|
||||||
@@ -314,7 +335,7 @@ const LibraryMenu = ({
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<LibraryMenuItems
|
<LibraryMenuItems
|
||||||
library={libraryItems}
|
libraryItems={libraryItems}
|
||||||
onRemoveFromLibrary={removeFromLibrary}
|
onRemoveFromLibrary={removeFromLibrary}
|
||||||
onAddToLibrary={addToLibrary}
|
onAddToLibrary={addToLibrary}
|
||||||
onInsertShape={onInsertShape}
|
onInsertShape={onInsertShape}
|
||||||
@@ -322,6 +343,9 @@ const LibraryMenu = ({
|
|||||||
setAppState={setAppState}
|
setAppState={setAppState}
|
||||||
setLibraryItems={setLibraryItems}
|
setLibraryItems={setLibraryItems}
|
||||||
libraryReturnUrl={libraryReturnUrl}
|
libraryReturnUrl={libraryReturnUrl}
|
||||||
|
focusContainer={focusContainer}
|
||||||
|
library={library}
|
||||||
|
id={id}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Island>
|
</Island>
|
||||||
@@ -342,75 +366,71 @@ const LayerUI = ({
|
|||||||
showThemeBtn,
|
showThemeBtn,
|
||||||
toggleZenMode,
|
toggleZenMode,
|
||||||
isCollaborating,
|
isCollaborating,
|
||||||
onExportToBackend,
|
renderTopRightUI,
|
||||||
renderCustomFooter,
|
renderCustomFooter,
|
||||||
viewModeEnabled,
|
viewModeEnabled,
|
||||||
libraryReturnUrl,
|
libraryReturnUrl,
|
||||||
UIOptions,
|
UIOptions,
|
||||||
|
focusContainer,
|
||||||
|
library,
|
||||||
|
id,
|
||||||
}: LayerUIProps) => {
|
}: LayerUIProps) => {
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
|
|
||||||
const renderEncryptedIcon = () => (
|
const renderJSONExportDialog = () => {
|
||||||
<a
|
|
||||||
className={clsx("encrypted-icon tooltip zen-mode-visibility", {
|
|
||||||
"zen-mode-visibility--hidden": zenModeEnabled,
|
|
||||||
})}
|
|
||||||
href="https://blog.excalidraw.com/end-to-end-encryption/"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
aria-label={t("encrypted.link")}
|
|
||||||
>
|
|
||||||
<Tooltip label={t("encrypted.tooltip")} position="above" long={true}>
|
|
||||||
{shield}
|
|
||||||
</Tooltip>
|
|
||||||
</a>
|
|
||||||
);
|
|
||||||
|
|
||||||
const renderExportDialog = () => {
|
|
||||||
if (!UIOptions.canvasActions.export) {
|
if (!UIOptions.canvasActions.export) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<JSONExportDialog
|
||||||
|
elements={elements}
|
||||||
|
appState={appState}
|
||||||
|
actionManager={actionManager}
|
||||||
|
exportOpts={UIOptions.canvasActions.export}
|
||||||
|
canvas={canvas}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderImageExportDialog = () => {
|
||||||
|
if (!UIOptions.canvasActions.saveAsImage) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const createExporter = (type: ExportType): ExportCB => async (
|
const createExporter = (type: ExportType): ExportCB => async (
|
||||||
exportedElements,
|
exportedElements,
|
||||||
scale,
|
scale,
|
||||||
) => {
|
) => {
|
||||||
if (canvas) {
|
await exportCanvas(type, exportedElements, appState, {
|
||||||
await exportCanvas(type, exportedElements, appState, canvas, {
|
exportBackground: appState.exportBackground,
|
||||||
exportBackground: appState.exportBackground,
|
name: appState.name,
|
||||||
name: appState.name,
|
viewBackgroundColor: appState.viewBackgroundColor,
|
||||||
viewBackgroundColor: appState.viewBackgroundColor,
|
scale,
|
||||||
scale,
|
})
|
||||||
shouldAddWatermark: appState.shouldAddWatermark,
|
.catch(muteFSAbortError)
|
||||||
})
|
.catch((error) => {
|
||||||
.catch(muteFSAbortError)
|
console.error(error);
|
||||||
.catch((error) => {
|
setAppState({ errorMessage: error.message });
|
||||||
console.error(error);
|
});
|
||||||
setAppState({ errorMessage: error.message });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ExportDialog
|
<ImageExportDialog
|
||||||
elements={elements}
|
elements={elements}
|
||||||
appState={appState}
|
appState={appState}
|
||||||
actionManager={actionManager}
|
actionManager={actionManager}
|
||||||
onExportToPng={createExporter("png")}
|
onExportToPng={createExporter("png")}
|
||||||
onExportToSvg={createExporter("svg")}
|
onExportToSvg={createExporter("svg")}
|
||||||
onExportToClipboard={createExporter("clipboard")}
|
onExportToClipboard={createExporter("clipboard")}
|
||||||
onExportToBackend={
|
|
||||||
onExportToBackend
|
|
||||||
? (elements) => {
|
|
||||||
onExportToBackend &&
|
|
||||||
onExportToBackend(elements, appState, canvas);
|
|
||||||
}
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const Separator = () => {
|
||||||
|
return <div style={{ width: ".625em" }} />;
|
||||||
|
};
|
||||||
|
|
||||||
const renderViewModeCanvasActions = () => {
|
const renderViewModeCanvasActions = () => {
|
||||||
return (
|
return (
|
||||||
<Section
|
<Section
|
||||||
@@ -424,9 +444,8 @@ const LayerUI = ({
|
|||||||
<Island padding={2} style={{ zIndex: 1 }}>
|
<Island padding={2} style={{ zIndex: 1 }}>
|
||||||
<Stack.Col gap={4}>
|
<Stack.Col gap={4}>
|
||||||
<Stack.Row gap={1} justifyContent="space-between">
|
<Stack.Row gap={1} justifyContent="space-between">
|
||||||
{actionManager.renderAction("saveScene")}
|
{renderJSONExportDialog()}
|
||||||
{actionManager.renderAction("saveAsScene")}
|
{renderImageExportDialog()}
|
||||||
{renderExportDialog()}
|
|
||||||
</Stack.Row>
|
</Stack.Row>
|
||||||
</Stack.Col>
|
</Stack.Col>
|
||||||
</Island>
|
</Island>
|
||||||
@@ -445,11 +464,12 @@ const LayerUI = ({
|
|||||||
<Island padding={2} style={{ zIndex: 1 }}>
|
<Island padding={2} style={{ zIndex: 1 }}>
|
||||||
<Stack.Col gap={4}>
|
<Stack.Col gap={4}>
|
||||||
<Stack.Row gap={1} justifyContent="space-between">
|
<Stack.Row gap={1} justifyContent="space-between">
|
||||||
{actionManager.renderAction("loadScene")}
|
|
||||||
{actionManager.renderAction("saveScene")}
|
|
||||||
{actionManager.renderAction("saveAsScene")}
|
|
||||||
{renderExportDialog()}
|
|
||||||
{actionManager.renderAction("clearCanvas")}
|
{actionManager.renderAction("clearCanvas")}
|
||||||
|
<Separator />
|
||||||
|
{actionManager.renderAction("loadScene")}
|
||||||
|
{renderJSONExportDialog()}
|
||||||
|
{renderImageExportDialog()}
|
||||||
|
<Separator />
|
||||||
{onCollabButtonClick && (
|
{onCollabButtonClick && (
|
||||||
<CollabButton
|
<CollabButton
|
||||||
isCollaborating={isCollaborating}
|
isCollaborating={isCollaborating}
|
||||||
@@ -517,6 +537,9 @@ const LayerUI = ({
|
|||||||
onAddToLibrary={deselectItems}
|
onAddToLibrary={deselectItems}
|
||||||
setAppState={setAppState}
|
setAppState={setAppState}
|
||||||
libraryReturnUrl={libraryReturnUrl}
|
libraryReturnUrl={libraryReturnUrl}
|
||||||
|
focusContainer={focusContainer}
|
||||||
|
library={library}
|
||||||
|
id={id}
|
||||||
/>
|
/>
|
||||||
) : null;
|
) : null;
|
||||||
|
|
||||||
@@ -570,24 +593,30 @@ const LayerUI = ({
|
|||||||
)}
|
)}
|
||||||
</Section>
|
</Section>
|
||||||
)}
|
)}
|
||||||
<UserList
|
<div
|
||||||
className={clsx("zen-mode-transition", {
|
className={clsx(
|
||||||
"transition-right": zenModeEnabled,
|
"layer-ui__wrapper__top-right zen-mode-transition",
|
||||||
})}
|
{
|
||||||
|
"transition-right": zenModeEnabled,
|
||||||
|
},
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
{appState.collaborators.size > 0 &&
|
<UserList>
|
||||||
Array.from(appState.collaborators)
|
{appState.collaborators.size > 0 &&
|
||||||
// Collaborator is either not initialized or is actually the current user.
|
Array.from(appState.collaborators)
|
||||||
.filter(([_, client]) => Object.keys(client).length !== 0)
|
// Collaborator is either not initialized or is actually the current user.
|
||||||
.map(([clientId, client]) => (
|
.filter(([_, client]) => Object.keys(client).length !== 0)
|
||||||
<Tooltip
|
.map(([clientId, client]) => (
|
||||||
label={client.username || "Unknown user"}
|
<Tooltip
|
||||||
key={clientId}
|
label={client.username || "Unknown user"}
|
||||||
>
|
key={clientId}
|
||||||
{actionManager.renderAction("goToCollaborator", clientId)}
|
>
|
||||||
</Tooltip>
|
{actionManager.renderAction("goToCollaborator", clientId)}
|
||||||
))}
|
</Tooltip>
|
||||||
</UserList>
|
))}
|
||||||
|
</UserList>
|
||||||
|
{renderTopRightUI?.(isMobile, appState)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</FixedSideContainer>
|
</FixedSideContainer>
|
||||||
);
|
);
|
||||||
@@ -595,61 +624,61 @@ const LayerUI = ({
|
|||||||
|
|
||||||
const renderBottomAppMenu = () => {
|
const renderBottomAppMenu = () => {
|
||||||
return (
|
return (
|
||||||
<div
|
<footer
|
||||||
className={clsx("App-menu App-menu_bottom zen-mode-transition", {
|
role="contentinfo"
|
||||||
"App-menu_bottom--transition-left": zenModeEnabled,
|
className="layer-ui__wrapper__footer App-menu App-menu_bottom"
|
||||||
})}
|
|
||||||
>
|
>
|
||||||
<Stack.Col gap={2}>
|
<div
|
||||||
<Section heading="canvasActions">
|
className={clsx(
|
||||||
<Island padding={1}>
|
"layer-ui__wrapper__footer-left zen-mode-transition",
|
||||||
<ZoomActions
|
{
|
||||||
renderAction={actionManager.renderAction}
|
"layer-ui__wrapper__footer-left--transition-left": zenModeEnabled,
|
||||||
zoom={appState.zoom}
|
},
|
||||||
/>
|
)}
|
||||||
</Island>
|
>
|
||||||
{renderEncryptedIcon()}
|
<Stack.Col gap={2}>
|
||||||
</Section>
|
<Section heading="canvasActions">
|
||||||
</Stack.Col>
|
<Island padding={1}>
|
||||||
</div>
|
<ZoomActions
|
||||||
|
renderAction={actionManager.renderAction}
|
||||||
|
zoom={appState.zoom}
|
||||||
|
/>
|
||||||
|
</Island>
|
||||||
|
</Section>
|
||||||
|
</Stack.Col>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={clsx(
|
||||||
|
"layer-ui__wrapper__footer-center zen-mode-transition",
|
||||||
|
{
|
||||||
|
"layer-ui__wrapper__footer-left--transition-bottom": zenModeEnabled,
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{renderCustomFooter?.(false, appState)}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={clsx(
|
||||||
|
"layer-ui__wrapper__footer-right zen-mode-transition",
|
||||||
|
{
|
||||||
|
"transition-right disable-pointerEvents": zenModeEnabled,
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{actionManager.renderAction("toggleShortcuts")}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
className={clsx("disable-zen-mode", {
|
||||||
|
"disable-zen-mode--visible": showExitZenModeBtn,
|
||||||
|
})}
|
||||||
|
onClick={toggleZenMode}
|
||||||
|
>
|
||||||
|
{t("buttons.exitZenMode")}
|
||||||
|
</button>
|
||||||
|
</footer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderGitHubCorner = () => {
|
|
||||||
return (
|
|
||||||
<aside
|
|
||||||
className={clsx(
|
|
||||||
"layer-ui__wrapper__github-corner zen-mode-transition",
|
|
||||||
{
|
|
||||||
"transition-right": zenModeEnabled,
|
|
||||||
},
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<GitHubCorner theme={appState.theme} />
|
|
||||||
</aside>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
const renderFooter = () => (
|
|
||||||
<footer role="contentinfo" className="layer-ui__wrapper__footer">
|
|
||||||
<div
|
|
||||||
className={clsx("zen-mode-transition", {
|
|
||||||
"transition-right disable-pointerEvents": zenModeEnabled,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{renderCustomFooter?.(false)}
|
|
||||||
{actionManager.renderAction("toggleShortcuts")}
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
className={clsx("disable-zen-mode", {
|
|
||||||
"disable-zen-mode--visible": showExitZenModeBtn,
|
|
||||||
})}
|
|
||||||
onClick={toggleZenMode}
|
|
||||||
>
|
|
||||||
{t("buttons.exitZenMode")}
|
|
||||||
</button>
|
|
||||||
</footer>
|
|
||||||
);
|
|
||||||
|
|
||||||
const dialogs = (
|
const dialogs = (
|
||||||
<>
|
<>
|
||||||
{appState.isLoading && <LoadingMessage />}
|
{appState.isLoading && <LoadingMessage />}
|
||||||
@@ -660,7 +689,11 @@ const LayerUI = ({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{appState.showHelpDialog && (
|
{appState.showHelpDialog && (
|
||||||
<HelpDialog onClose={() => setAppState({ showHelpDialog: false })} />
|
<HelpDialog
|
||||||
|
onClose={() => {
|
||||||
|
setAppState({ showHelpDialog: false });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
{appState.pasteDialog.shown && (
|
{appState.pasteDialog.shown && (
|
||||||
<PasteChartDialog
|
<PasteChartDialog
|
||||||
@@ -685,7 +718,8 @@ const LayerUI = ({
|
|||||||
elements={elements}
|
elements={elements}
|
||||||
actionManager={actionManager}
|
actionManager={actionManager}
|
||||||
libraryMenu={libraryMenu}
|
libraryMenu={libraryMenu}
|
||||||
exportButton={renderExportDialog()}
|
renderJSONExportDialog={renderJSONExportDialog}
|
||||||
|
renderImageExportDialog={renderImageExportDialog}
|
||||||
setAppState={setAppState}
|
setAppState={setAppState}
|
||||||
onCollabButtonClick={onCollabButtonClick}
|
onCollabButtonClick={onCollabButtonClick}
|
||||||
onLockToggle={onLockToggle}
|
onLockToggle={onLockToggle}
|
||||||
@@ -708,8 +742,6 @@ const LayerUI = ({
|
|||||||
{dialogs}
|
{dialogs}
|
||||||
{renderFixedSideContainer()}
|
{renderFixedSideContainer()}
|
||||||
{renderBottomAppMenu()}
|
{renderBottomAppMenu()}
|
||||||
{renderGitHubCorner()}
|
|
||||||
{renderFooter()}
|
|
||||||
{appState.scrolledOutside && (
|
{appState.scrolledOutside && (
|
||||||
<button
|
<button
|
||||||
className="scroll-back-to-content"
|
className="scroll-back-to-content"
|
||||||
|
|||||||
@@ -39,7 +39,6 @@ export const LibraryUnit = ({
|
|||||||
const svg = exportToSvg(elementsToRender, {
|
const svg = exportToSvg(elementsToRender, {
|
||||||
exportBackground: false,
|
exportBackground: false,
|
||||||
viewBackgroundColor: oc.white,
|
viewBackgroundColor: oc.white,
|
||||||
shouldAddWatermark: false,
|
|
||||||
});
|
});
|
||||||
for (const child of ref.current!.children) {
|
for (const child of ref.current!.children) {
|
||||||
if (child.tagName !== "svg") {
|
if (child.tagName !== "svg") {
|
||||||
|
|||||||
@@ -20,7 +20,8 @@ import { BackgroundPickerAndDarkModeToggle } from "./BackgroundPickerAndDarkMode
|
|||||||
type MobileMenuProps = {
|
type MobileMenuProps = {
|
||||||
appState: AppState;
|
appState: AppState;
|
||||||
actionManager: ActionManager;
|
actionManager: ActionManager;
|
||||||
exportButton: React.ReactNode;
|
renderJSONExportDialog: () => React.ReactNode;
|
||||||
|
renderImageExportDialog: () => React.ReactNode;
|
||||||
setAppState: React.Component<any, AppState>["setState"];
|
setAppState: React.Component<any, AppState>["setState"];
|
||||||
elements: readonly NonDeletedExcalidrawElement[];
|
elements: readonly NonDeletedExcalidrawElement[];
|
||||||
libraryMenu: JSX.Element | null;
|
libraryMenu: JSX.Element | null;
|
||||||
@@ -28,7 +29,7 @@ type MobileMenuProps = {
|
|||||||
onLockToggle: () => void;
|
onLockToggle: () => void;
|
||||||
canvas: HTMLCanvasElement | null;
|
canvas: HTMLCanvasElement | null;
|
||||||
isCollaborating: boolean;
|
isCollaborating: boolean;
|
||||||
renderCustomFooter?: (isMobile: boolean) => JSX.Element;
|
renderCustomFooter?: (isMobile: boolean, appState: AppState) => JSX.Element;
|
||||||
viewModeEnabled: boolean;
|
viewModeEnabled: boolean;
|
||||||
showThemeBtn: boolean;
|
showThemeBtn: boolean;
|
||||||
};
|
};
|
||||||
@@ -38,7 +39,8 @@ export const MobileMenu = ({
|
|||||||
elements,
|
elements,
|
||||||
libraryMenu,
|
libraryMenu,
|
||||||
actionManager,
|
actionManager,
|
||||||
exportButton,
|
renderJSONExportDialog,
|
||||||
|
renderImageExportDialog,
|
||||||
setAppState,
|
setAppState,
|
||||||
onCollabButtonClick,
|
onCollabButtonClick,
|
||||||
onLockToggle,
|
onLockToggle,
|
||||||
@@ -107,19 +109,17 @@ export const MobileMenu = ({
|
|||||||
if (viewModeEnabled) {
|
if (viewModeEnabled) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{actionManager.renderAction("saveScene")}
|
{renderJSONExportDialog()}
|
||||||
{actionManager.renderAction("saveAsScene")}
|
{renderImageExportDialog()}
|
||||||
{exportButton}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{actionManager.renderAction("loadScene")}
|
|
||||||
{actionManager.renderAction("saveScene")}
|
|
||||||
{actionManager.renderAction("saveAsScene")}
|
|
||||||
{exportButton}
|
|
||||||
{actionManager.renderAction("clearCanvas")}
|
{actionManager.renderAction("clearCanvas")}
|
||||||
|
{actionManager.renderAction("loadScene")}
|
||||||
|
{renderJSONExportDialog()}
|
||||||
|
{renderImageExportDialog()}
|
||||||
{onCollabButtonClick && (
|
{onCollabButtonClick && (
|
||||||
<CollabButton
|
<CollabButton
|
||||||
isCollaborating={isCollaborating}
|
isCollaborating={isCollaborating}
|
||||||
@@ -155,7 +155,7 @@ export const MobileMenu = ({
|
|||||||
<div className="panelColumn">
|
<div className="panelColumn">
|
||||||
<Stack.Col gap={4}>
|
<Stack.Col gap={4}>
|
||||||
{renderCanvasActions()}
|
{renderCanvasActions()}
|
||||||
{renderCustomFooter?.(true)}
|
{renderCustomFooter?.(true, appState)}
|
||||||
{appState.collaborators.size > 0 && (
|
{appState.collaborators.size > 0 && (
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>{t("labels.collaborators")}</legend>
|
<legend>{t("labels.collaborators")}</legend>
|
||||||
|
|||||||
@@ -26,8 +26,7 @@
|
|||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
background-color: transparentize($oc-black, 0.7);
|
background-color: transparentize($oc-black, 0.3);
|
||||||
backdrop-filter: blur(2px);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.Modal__content {
|
.Modal__content {
|
||||||
@@ -45,13 +44,16 @@
|
|||||||
|
|
||||||
// for modals, reset blurry bg
|
// for modals, reset blurry bg
|
||||||
background: var(--island-bg-color);
|
background: var(--island-bg-color);
|
||||||
backdrop-filter: none;
|
|
||||||
|
|
||||||
border: 1px solid var(--dialog-border-color);
|
border: 1px solid var(--dialog-border-color);
|
||||||
box-shadow: 0 2px 10px transparentize($oc-black, 0.75);
|
box-shadow: 0 2px 10px transparentize($oc-black, 0.75);
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
@include isMobile {
|
@include isMobile {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
border: 0;
|
border: 0;
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ import React, { useState, useLayoutEffect, useRef } from "react";
|
|||||||
import { createPortal } from "react-dom";
|
import { createPortal } from "react-dom";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { KEYS } from "../keys";
|
import { KEYS } from "../keys";
|
||||||
import { useIsMobile } from "../components/App";
|
import { useExcalidrawContainer, useIsMobile } from "./App";
|
||||||
|
import { AppState } from "../types";
|
||||||
|
|
||||||
export const Modal = (props: {
|
export const Modal = (props: {
|
||||||
className?: string;
|
className?: string;
|
||||||
@@ -12,8 +13,10 @@ export const Modal = (props: {
|
|||||||
maxWidth?: number;
|
maxWidth?: number;
|
||||||
onCloseRequest(): void;
|
onCloseRequest(): void;
|
||||||
labelledBy: string;
|
labelledBy: string;
|
||||||
|
theme?: AppState["theme"];
|
||||||
}) => {
|
}) => {
|
||||||
const modalRoot = useBodyRoot();
|
const { theme = "light" } = props;
|
||||||
|
const modalRoot = useBodyRoot(theme);
|
||||||
|
|
||||||
if (!modalRoot) {
|
if (!modalRoot) {
|
||||||
return null;
|
return null;
|
||||||
@@ -22,6 +25,7 @@ export const Modal = (props: {
|
|||||||
const handleKeydown = (event: React.KeyboardEvent) => {
|
const handleKeydown = (event: React.KeyboardEvent) => {
|
||||||
if (event.key === KEYS.ESCAPE) {
|
if (event.key === KEYS.ESCAPE) {
|
||||||
event.nativeEvent.stopImmediatePropagation();
|
event.nativeEvent.stopImmediatePropagation();
|
||||||
|
event.stopPropagation();
|
||||||
props.onCloseRequest();
|
props.onCloseRequest();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -38,6 +42,7 @@ export const Modal = (props: {
|
|||||||
<div
|
<div
|
||||||
className="Modal__content"
|
className="Modal__content"
|
||||||
style={{ "--max-width": `${props.maxWidth}px` }}
|
style={{ "--max-width": `${props.maxWidth}px` }}
|
||||||
|
tabIndex={0}
|
||||||
>
|
>
|
||||||
{props.children}
|
{props.children}
|
||||||
</div>
|
</div>
|
||||||
@@ -46,13 +51,15 @@ export const Modal = (props: {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const useBodyRoot = () => {
|
const useBodyRoot = (theme: AppState["theme"]) => {
|
||||||
const [div, setDiv] = useState<HTMLDivElement | null>(null);
|
const [div, setDiv] = useState<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
const isMobileRef = useRef(isMobile);
|
const isMobileRef = useRef(isMobile);
|
||||||
isMobileRef.current = isMobile;
|
isMobileRef.current = isMobile;
|
||||||
|
|
||||||
|
const excalidrawContainer = useExcalidrawContainer();
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
if (div) {
|
if (div) {
|
||||||
div.classList.toggle("excalidraw--mobile", isMobile);
|
div.classList.toggle("excalidraw--mobile", isMobile);
|
||||||
@@ -60,9 +67,9 @@ const useBodyRoot = () => {
|
|||||||
}, [div, isMobile]);
|
}, [div, isMobile]);
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
const isDarkTheme = !!document
|
const isDarkTheme =
|
||||||
.querySelector(".excalidraw")
|
!!excalidrawContainer?.classList.contains("theme--dark") ||
|
||||||
?.classList.contains("theme--dark");
|
theme === "dark";
|
||||||
const div = document.createElement("div");
|
const div = document.createElement("div");
|
||||||
|
|
||||||
div.classList.add("excalidraw", "excalidraw-modal-container");
|
div.classList.add("excalidraw", "excalidraw-modal-container");
|
||||||
@@ -79,7 +86,7 @@ const useBodyRoot = () => {
|
|||||||
return () => {
|
return () => {
|
||||||
document.body.removeChild(div);
|
document.body.removeChild(div);
|
||||||
};
|
};
|
||||||
}, []);
|
}, [excalidrawContainer, theme]);
|
||||||
|
|
||||||
return div;
|
return div;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -38,7 +38,6 @@ const ChartPreviewBtn = (props: {
|
|||||||
const svg = exportToSvg(elements, {
|
const svg = exportToSvg(elements, {
|
||||||
exportBackground: false,
|
exportBackground: false,
|
||||||
viewBackgroundColor: oc.white,
|
viewBackgroundColor: oc.white,
|
||||||
shouldAddWatermark: false,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const previewNode = previewRef.current!;
|
const previewNode = previewRef.current!;
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
.ProjectName {
|
||||||
|
margin: auto;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.TextInput {
|
||||||
|
height: calc(1rem - 3px);
|
||||||
|
width: 200px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-align: center;
|
||||||
|
margin-left: 8px;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
|
||||||
|
&--readonly {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
&:hover {
|
||||||
|
background: none;
|
||||||
|
}
|
||||||
|
width: auto;
|
||||||
|
max-width: 200px;
|
||||||
|
padding-left: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
import "./TextInput.scss";
|
import "./TextInput.scss";
|
||||||
|
|
||||||
import React, { Component } from "react";
|
import React, { Component } from "react";
|
||||||
|
import { focusNearestParent } from "../utils";
|
||||||
|
|
||||||
|
import "./ProjectName.scss";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
value: string;
|
value: string;
|
||||||
@@ -17,6 +20,7 @@ export class ProjectName extends Component<Props, State> {
|
|||||||
fileName: this.props.value,
|
fileName: this.props.value,
|
||||||
};
|
};
|
||||||
private handleBlur = (event: any) => {
|
private handleBlur = (event: any) => {
|
||||||
|
focusNearestParent(event.target);
|
||||||
const value = event.target.value;
|
const value = event.target.value;
|
||||||
if (value !== this.props.value) {
|
if (value !== this.props.value) {
|
||||||
this.props.onChange(value);
|
this.props.onChange(value);
|
||||||
@@ -35,8 +39,8 @@ export class ProjectName extends Component<Props, State> {
|
|||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="ProjectName">
|
||||||
<label htmlFor="file-name">
|
<label className="ProjectName-label" htmlFor="filename">
|
||||||
{`${this.props.label}${this.props.isNameEditable ? "" : ":"}`}
|
{`${this.props.label}${this.props.isNameEditable ? "" : ":"}`}
|
||||||
</label>
|
</label>
|
||||||
{this.props.isNameEditable ? (
|
{this.props.isNameEditable ? (
|
||||||
@@ -44,18 +48,18 @@ export class ProjectName extends Component<Props, State> {
|
|||||||
className="TextInput"
|
className="TextInput"
|
||||||
onBlur={this.handleBlur}
|
onBlur={this.handleBlur}
|
||||||
onKeyDown={this.handleKeyDown}
|
onKeyDown={this.handleKeyDown}
|
||||||
id="file-name"
|
id="filename"
|
||||||
value={this.state.fileName}
|
value={this.state.fileName}
|
||||||
onChange={(event) =>
|
onChange={(event) =>
|
||||||
this.setState({ fileName: event.target.value })
|
this.setState({ fileName: event.target.value })
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<span className="TextInput TextInput--readonly" id="file-name">
|
<span className="TextInput TextInput--readonly" id="filename">
|
||||||
{this.props.value}
|
{this.props.value}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
top: 64px;
|
top: 64px;
|
||||||
right: 12px;
|
right: 12px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
z-index: 999;
|
z-index: 10;
|
||||||
|
|
||||||
h3 {
|
h3 {
|
||||||
margin: 0 24px 8px 0;
|
margin: 0 24px 8px 0;
|
||||||
|
|||||||
@@ -29,9 +29,13 @@ type ToolButtonProps =
|
|||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
onClick?(): void;
|
onClick?(): void;
|
||||||
})
|
})
|
||||||
|
| (ToolButtonBaseProps & {
|
||||||
|
type: "icon";
|
||||||
|
children?: React.ReactNode;
|
||||||
|
onClick?(): void;
|
||||||
|
})
|
||||||
| (ToolButtonBaseProps & {
|
| (ToolButtonBaseProps & {
|
||||||
type: "radio";
|
type: "radio";
|
||||||
|
|
||||||
checked: boolean;
|
checked: boolean;
|
||||||
onChange?(): void;
|
onChange?(): void;
|
||||||
});
|
});
|
||||||
@@ -43,7 +47,7 @@ export const ToolButton = React.forwardRef((props: ToolButtonProps, ref) => {
|
|||||||
React.useImperativeHandle(ref, () => innerRef.current);
|
React.useImperativeHandle(ref, () => innerRef.current);
|
||||||
const sizeCn = `ToolIcon_size_${props.size || DEFAULT_SIZE}`;
|
const sizeCn = `ToolIcon_size_${props.size || DEFAULT_SIZE}`;
|
||||||
|
|
||||||
if (props.type === "button") {
|
if (props.type === "button" || props.type === "icon") {
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
className={clsx(
|
className={clsx(
|
||||||
@@ -56,6 +60,7 @@ export const ToolButton = React.forwardRef((props: ToolButtonProps, ref) => {
|
|||||||
{
|
{
|
||||||
ToolIcon: !props.hidden,
|
ToolIcon: !props.hidden,
|
||||||
"ToolIcon--selected": props.selected,
|
"ToolIcon--selected": props.selected,
|
||||||
|
"ToolIcon--plain": props.type === "icon",
|
||||||
},
|
},
|
||||||
)}
|
)}
|
||||||
data-testid={props["data-testid"]}
|
data-testid={props["data-testid"]}
|
||||||
@@ -66,14 +71,16 @@ export const ToolButton = React.forwardRef((props: ToolButtonProps, ref) => {
|
|||||||
onClick={props.onClick}
|
onClick={props.onClick}
|
||||||
ref={innerRef}
|
ref={innerRef}
|
||||||
>
|
>
|
||||||
<div className="ToolIcon__icon" aria-hidden="true">
|
{(props.icon || props.label) && (
|
||||||
{props.icon || props.label}
|
<div className="ToolIcon__icon" aria-hidden="true">
|
||||||
{props.keyBindingLabel && (
|
{props.icon || props.label}
|
||||||
<span className="ToolIcon__keybinding">
|
{props.keyBindingLabel && (
|
||||||
{props.keyBindingLabel}
|
<span className="ToolIcon__keybinding">
|
||||||
</span>
|
{props.keyBindingLabel}
|
||||||
)}
|
</span>
|
||||||
</div>
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{props.showAriaLabel && (
|
{props.showAriaLabel && (
|
||||||
<div className="ToolIcon__label">{props["aria-label"]}</div>
|
<div className="ToolIcon__label">{props["aria-label"]}</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -11,6 +11,15 @@
|
|||||||
background-color: var(--button-gray-1);
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ToolIcon--plain {
|
||||||
|
background-color: transparent;
|
||||||
|
.ToolIcon__icon {
|
||||||
|
width: 2rem;
|
||||||
|
height: 2rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.ToolIcon__icon {
|
.ToolIcon__icon {
|
||||||
@@ -187,17 +196,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.TooltipIcon {
|
|
||||||
width: 0.9em;
|
|
||||||
height: 0.9em;
|
|
||||||
margin-left: 5px;
|
|
||||||
margin-top: 1px;
|
|
||||||
|
|
||||||
@include isMobile {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.unlocked-icon {
|
.unlocked-icon {
|
||||||
:root[dir="ltr"] & {
|
:root[dir="ltr"] & {
|
||||||
left: 2px;
|
left: 2px;
|
||||||
|
|||||||
+31
-50
@@ -1,58 +1,39 @@
|
|||||||
@import "../css/variables.module";
|
@import "../css/variables.module";
|
||||||
.excalidraw {
|
.excalidraw-tooltip {
|
||||||
.Tooltip {
|
position: absolute;
|
||||||
position: relative;
|
z-index: 1000;
|
||||||
}
|
|
||||||
|
|
||||||
.Tooltip__label {
|
padding: 8px;
|
||||||
--arrow-size: 4px;
|
border-radius: 6px;
|
||||||
visibility: hidden;
|
box-sizing: border-box;
|
||||||
background: $oc-black;
|
pointer-events: none;
|
||||||
color: $oc-white;
|
word-wrap: break-word;
|
||||||
text-align: center;
|
|
||||||
border-radius: 6px;
|
|
||||||
padding: 8px;
|
|
||||||
position: absolute;
|
|
||||||
z-index: 10;
|
|
||||||
font-size: 13px;
|
|
||||||
line-height: 1.5;
|
|
||||||
font-weight: 500;
|
|
||||||
// extra pixel offset for unknown reasons
|
|
||||||
left: calc(50% + var(--arrow-size) / 2 - 1px);
|
|
||||||
transform: translateX(-50%);
|
|
||||||
word-wrap: break-word;
|
|
||||||
|
|
||||||
&::after {
|
background: $oc-black;
|
||||||
content: "";
|
|
||||||
border: var(--arrow-size) solid transparent;
|
|
||||||
position: absolute;
|
|
||||||
left: calc(50% - var(--arrow-size));
|
|
||||||
}
|
|
||||||
|
|
||||||
&--above {
|
line-height: 1.5;
|
||||||
bottom: calc(100% + var(--arrow-size) + 3px);
|
text-align: center;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: $oc-white;
|
||||||
|
|
||||||
&::after {
|
display: none;
|
||||||
border-top-color: $oc-black;
|
|
||||||
top: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&--below {
|
&.excalidraw-tooltip--visible {
|
||||||
top: calc(100% + var(--arrow-size) + 3px);
|
display: block;
|
||||||
|
}
|
||||||
&::after {
|
}
|
||||||
border-bottom-color: $oc-black;
|
|
||||||
bottom: 100%;
|
.excalidraw {
|
||||||
}
|
.Tooltip-icon {
|
||||||
}
|
width: 0.9em;
|
||||||
}
|
height: 0.9em;
|
||||||
|
margin-left: 5px;
|
||||||
.Tooltip:hover .Tooltip__label {
|
margin-top: 1px;
|
||||||
visibility: visible;
|
display: flex;
|
||||||
}
|
|
||||||
|
@include isMobile {
|
||||||
.Tooltip__label:hover {
|
display: none;
|
||||||
visibility: visible;
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+81
-20
@@ -1,31 +1,92 @@
|
|||||||
import "./Tooltip.scss";
|
import "./Tooltip.scss";
|
||||||
|
|
||||||
import React from "react";
|
import React, { useEffect } from "react";
|
||||||
|
|
||||||
|
const getTooltipDiv = () => {
|
||||||
|
const existingDiv = document.querySelector<HTMLDivElement>(
|
||||||
|
".excalidraw-tooltip",
|
||||||
|
);
|
||||||
|
if (existingDiv) {
|
||||||
|
return existingDiv;
|
||||||
|
}
|
||||||
|
const div = document.createElement("div");
|
||||||
|
document.body.appendChild(div);
|
||||||
|
div.classList.add("excalidraw-tooltip");
|
||||||
|
return div;
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateTooltip = (
|
||||||
|
item: HTMLDivElement,
|
||||||
|
tooltip: HTMLDivElement,
|
||||||
|
label: string,
|
||||||
|
long: boolean,
|
||||||
|
) => {
|
||||||
|
tooltip.classList.add("excalidraw-tooltip--visible");
|
||||||
|
tooltip.style.minWidth = long ? "50ch" : "10ch";
|
||||||
|
tooltip.style.maxWidth = long ? "50ch" : "15ch";
|
||||||
|
|
||||||
|
tooltip.textContent = label;
|
||||||
|
|
||||||
|
const {
|
||||||
|
x: itemX,
|
||||||
|
bottom: itemBottom,
|
||||||
|
top: itemTop,
|
||||||
|
width: itemWidth,
|
||||||
|
} = item.getBoundingClientRect();
|
||||||
|
|
||||||
|
const {
|
||||||
|
width: labelWidth,
|
||||||
|
height: labelHeight,
|
||||||
|
} = tooltip.getBoundingClientRect();
|
||||||
|
|
||||||
|
const viewportWidth = window.innerWidth;
|
||||||
|
const viewportHeight = window.innerHeight;
|
||||||
|
|
||||||
|
const margin = 5;
|
||||||
|
|
||||||
|
const left = itemX + itemWidth / 2 - labelWidth / 2;
|
||||||
|
const offsetLeft =
|
||||||
|
left + labelWidth >= viewportWidth ? left + labelWidth - viewportWidth : 0;
|
||||||
|
|
||||||
|
const top = itemBottom + margin;
|
||||||
|
const offsetTop =
|
||||||
|
top + labelHeight >= viewportHeight
|
||||||
|
? itemBottom - itemTop + labelHeight + margin * 2
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
Object.assign(tooltip.style, {
|
||||||
|
top: `${top - offsetTop}px`,
|
||||||
|
left: `${left - offsetLeft}px`,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
type TooltipProps = {
|
type TooltipProps = {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
label: string;
|
label: string;
|
||||||
position?: "above" | "below";
|
|
||||||
long?: boolean;
|
long?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Tooltip = ({
|
export const Tooltip = ({ children, label, long = false }: TooltipProps) => {
|
||||||
children,
|
useEffect(() => {
|
||||||
label,
|
return () =>
|
||||||
position = "below",
|
getTooltipDiv().classList.remove("excalidraw-tooltip--visible");
|
||||||
long = false,
|
}, []);
|
||||||
}: TooltipProps) => (
|
|
||||||
<div className="Tooltip">
|
return (
|
||||||
<span
|
<div
|
||||||
className={
|
onPointerEnter={(event) =>
|
||||||
position === "above"
|
updateTooltip(
|
||||||
? "Tooltip__label Tooltip__label--above"
|
event.currentTarget as HTMLDivElement,
|
||||||
: "Tooltip__label Tooltip__label--below"
|
getTooltipDiv(),
|
||||||
|
label,
|
||||||
|
long,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
onPointerLeave={() =>
|
||||||
|
getTooltipDiv().classList.remove("excalidraw-tooltip--visible")
|
||||||
}
|
}
|
||||||
style={{ width: long ? "50ch" : "10ch" }}
|
|
||||||
>
|
>
|
||||||
{label}
|
{children}
|
||||||
</span>
|
</div>
|
||||||
{children}
|
);
|
||||||
</div>
|
};
|
||||||
);
|
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
.UserList {
|
.UserList {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
/*github corner*/
|
/*github corner*/
|
||||||
padding: var(--space-factor) 40px var(--space-factor) var(--space-factor);
|
padding: var(--space-factor) var(--space-factor) var(--space-factor)
|
||||||
|
var(--space-factor);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
|
|||||||
+51
-98
@@ -41,6 +41,14 @@ const createIcon = (d: string | React.ReactNode, opts: number | Opts = 512) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const checkIcon = createIcon(
|
||||||
|
<polyline fill="none" stroke="currentColor" points="20 6 9 17 4 12" />,
|
||||||
|
{
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
export const link = createIcon(
|
export const link = createIcon(
|
||||||
"M326.612 185.391c59.747 59.809 58.927 155.698.36 214.59-.11.12-.24.25-.36.37l-67.2 67.2c-59.27 59.27-155.699 59.262-214.96 0-59.27-59.26-59.27-155.7 0-214.96l37.106-37.106c9.84-9.84 26.786-3.3 27.294 10.606.648 17.722 3.826 35.527 9.69 52.721 1.986 5.822.567 12.262-3.783 16.612l-13.087 13.087c-28.026 28.026-28.905 73.66-1.155 101.96 28.024 28.579 74.086 28.749 102.325.51l67.2-67.19c28.191-28.191 28.073-73.757 0-101.83-3.701-3.694-7.429-6.564-10.341-8.569a16.037 16.037 0 0 1-6.947-12.606c-.396-10.567 3.348-21.456 11.698-29.806l21.054-21.055c5.521-5.521 14.182-6.199 20.584-1.731a152.482 152.482 0 0 1 20.522 17.197zM467.547 44.449c-59.261-59.262-155.69-59.27-214.96 0l-67.2 67.2c-.12.12-.25.25-.36.37-58.566 58.892-59.387 154.781.36 214.59a152.454 152.454 0 0 0 20.521 17.196c6.402 4.468 15.064 3.789 20.584-1.731l21.054-21.055c8.35-8.35 12.094-19.239 11.698-29.806a16.037 16.037 0 0 0-6.947-12.606c-2.912-2.005-6.64-4.875-10.341-8.569-28.073-28.073-28.191-73.639 0-101.83l67.2-67.19c28.239-28.239 74.3-28.069 102.325.51 27.75 28.3 26.872 73.934-1.155 101.96l-13.087 13.087c-4.35 4.35-5.769 10.79-3.783 16.612 5.864 17.194 9.042 34.999 9.69 52.721.509 13.906 17.454 20.446 27.294 10.606l37.106-37.106c59.271-59.259 59.271-155.699.001-214.959z",
|
"M326.612 185.391c59.747 59.809 58.927 155.698.36 214.59-.11.12-.24.25-.36.37l-67.2 67.2c-59.27 59.27-155.699 59.262-214.96 0-59.27-59.26-59.27-155.7 0-214.96l37.106-37.106c9.84-9.84 26.786-3.3 27.294 10.606.648 17.722 3.826 35.527 9.69 52.721 1.986 5.822.567 12.262-3.783 16.612l-13.087 13.087c-28.026 28.026-28.905 73.66-1.155 101.96 28.024 28.579 74.086 28.749 102.325.51l67.2-67.19c28.191-28.191 28.073-73.757 0-101.83-3.701-3.694-7.429-6.564-10.341-8.569a16.037 16.037 0 0 1-6.947-12.606c-.396-10.567 3.348-21.456 11.698-29.806l21.054-21.055c5.521-5.521 14.182-6.199 20.584-1.731a152.482 152.482 0 0 1 20.522 17.197zM467.547 44.449c-59.261-59.262-155.69-59.27-214.96 0l-67.2 67.2c-.12.12-.25.25-.36.37-58.566 58.892-59.387 154.781.36 214.59a152.454 152.454 0 0 0 20.521 17.196c6.402 4.468 15.064 3.789 20.584-1.731l21.054-21.055c8.35-8.35 12.094-19.239 11.698-29.806a16.037 16.037 0 0 0-6.947-12.606c-2.912-2.005-6.64-4.875-10.341-8.569-28.073-28.073-28.191-73.639 0-101.83l67.2-67.19c28.239-28.239 74.3-28.069 102.325.51 27.75 28.3 26.872 73.934-1.155 101.96l-13.087 13.087c-4.35 4.35-5.769 10.79-3.783 16.612 5.864 17.194 9.042 34.999 9.69 52.721.509 13.906 17.454 20.446 27.294 10.606l37.106-37.106c59.271-59.259 59.271-155.699.001-214.959z",
|
||||||
{ mirror: true },
|
{ mirror: true },
|
||||||
@@ -80,6 +88,19 @@ export const exportFile = createIcon(
|
|||||||
{ width: 576, height: 512, mirror: true },
|
{ width: 576, height: 512, mirror: true },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const exportImage = createIcon(
|
||||||
|
<>
|
||||||
|
<path d="M571 308l-95.7-96.4c-10.1-10.1-27.4-3-27.4 11.3V288h-64v64h64v65.2c0 14.3 17.3 21.4 27.4 11.3L571 332c6.6-6.6 6.6-17.4 0-24zm-187 44v-64 64z" />
|
||||||
|
<path d="M384 121.941V128H256V0h6.059c6.362 0 12.471 2.53 16.97 7.029l97.941 97.941a24.01 24.01 0 017.03 16.971zM248 160c-13.2 0-24-10.8-24-24V0H24C10.745 0 0 10.745 0 24v464c0 13.255 10.745 24 24 24h336c13.255 0 24-10.745 24-24V160H248zm-135.455 16c26.51 0 48 21.49 48 48s-21.49 48-48 48-48-21.49-48-48 21.491-48 48-48zm208 240h-256l.485-48.485L104.545 328c4.686-4.686 11.799-4.201 16.485.485L160.545 368 264.06 264.485c4.686-4.686 12.284-4.686 16.971 0L320.545 304v112z" />
|
||||||
|
</>,
|
||||||
|
{ width: 576, height: 512, mirror: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
export const exportToFileIcon = createIcon(
|
||||||
|
"M216 0h80c13.3 0 24 10.7 24 24v168h87.7c17.8 0 26.7 21.5 14.1 34.1L269.7 378.3c-7.5 7.5-19.8 7.5-27.3 0L90.1 226.1c-12.6-12.6-3.7-34.1 14.1-34.1H192V24c0-13.3 10.7-24 24-24zm296 376v112c0 13.3-10.7 24-24 24H24c-13.3 0-24-10.7-24-24V376c0-13.3 10.7-24 24-24h146.7l49 49c20.1 20.1 52.5 20.1 72.6 0l49-49H488c13.3 0 24 10.7 24 24zm-124 88c0-11-9-20-20-20s-20 9-20 20 9 20 20 20 20-9 20-20zm64 0c0-11-9-20-20-20s-20 9-20 20 9 20 20 20 20-9 20-20z",
|
||||||
|
{ width: 512, height: 512 },
|
||||||
|
);
|
||||||
|
|
||||||
export const zoomIn = createIcon(
|
export const zoomIn = createIcon(
|
||||||
"M416 208H272V64c0-17.67-14.33-32-32-32h-32c-17.67 0-32 14.33-32 32v144H32c-17.67 0-32 14.33-32 32v32c0 17.67 14.33 32 32 32h144v144c0 17.67 14.33 32 32 32h32c17.67 0 32-14.33 32-32V304h144c17.67 0 32-14.33 32-32v-32c0-17.67-14.33-32-32-32z",
|
"M416 208H272V64c0-17.67-14.33-32-32-32h-32c-17.67 0-32 14.33-32 32v144H32c-17.67 0-32 14.33-32 32v32c0 17.67 14.33 32 32 32h144v144c0 17.67 14.33 32 32 32h32c17.67 0 32-14.33 32-32V304h144c17.67 0 32-14.33 32-32v-32c0-17.67-14.33-32-32-32z",
|
||||||
{ width: 448, height: 512 },
|
{ width: 448, height: 512 },
|
||||||
@@ -222,14 +243,12 @@ export const SendToBackIcon = React.memo(
|
|||||||
d="M18 7.333C18 6.597 17.403 6 16.667 6H7.333C6.597 6 6 6.597 6 7.333v9.334C6 17.403 6.597 18 7.333 18h9.334c.736 0 1.333-.597 1.333-1.333V7.333z"
|
d="M18 7.333C18 6.597 17.403 6 16.667 6H7.333C6.597 6 6 6.597 6 7.333v9.334C6 17.403 6.597 18 7.333 18h9.334c.736 0 1.333-.597 1.333-1.333V7.333z"
|
||||||
fill={activeElementColor(theme)}
|
fill={activeElementColor(theme)}
|
||||||
stroke={activeElementColor(theme)}
|
stroke={activeElementColor(theme)}
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth="2"
|
strokeWidth="2"
|
||||||
/>
|
/>
|
||||||
<path
|
<path
|
||||||
d="M11 3a1 1 0 00-1-1H3a1 1 0 00-1 1v7a1 1 0 001 1h8V3zM22 14a1 1 0 00-1-1h-7a1 1 0 00-1 1v7a1 1 0 001 1h8v-8z"
|
d="M11 3a1 1 0 00-1-1H3a1 1 0 00-1 1v7a1 1 0 001 1h8V3zM22 14a1 1 0 00-1-1h-7a1 1 0 00-1 1v7a1 1 0 001 1h8v-8z"
|
||||||
fill={iconFillColor(theme)}
|
fill={iconFillColor(theme)}
|
||||||
stroke={iconFillColor(theme)}
|
stroke={iconFillColor(theme)}
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth="2"
|
strokeWidth="2"
|
||||||
/>
|
/>
|
||||||
</>,
|
</>,
|
||||||
@@ -335,7 +354,6 @@ export const DistributeHorizontallyIcon = React.memo(
|
|||||||
({ theme }: { theme: "light" | "dark" }) =>
|
({ theme }: { theme: "light" | "dark" }) =>
|
||||||
createIcon(
|
createIcon(
|
||||||
<>
|
<>
|
||||||
<path d="M5 5V19Z" fill="black" />
|
|
||||||
<path
|
<path
|
||||||
d="M19 5V19M5 5V19"
|
d="M19 5V19M5 5V19"
|
||||||
stroke={iconFillColor(theme)}
|
stroke={iconFillColor(theme)}
|
||||||
@@ -353,14 +371,6 @@ export const DistributeHorizontallyIcon = React.memo(
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
<svg
|
|
||||||
width="24"
|
|
||||||
height="24"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
></svg>;
|
|
||||||
|
|
||||||
export const DistributeVerticallyIcon = React.memo(
|
export const DistributeVerticallyIcon = React.memo(
|
||||||
({ theme }: { theme: "light" | "dark" }) =>
|
({ theme }: { theme: "light" | "dark" }) =>
|
||||||
createIcon(
|
createIcon(
|
||||||
@@ -479,42 +489,16 @@ export const GroupIcon = React.memo(({ theme }: { theme: "light" | "dark" }) =>
|
|||||||
stroke={iconFillColor(theme)}
|
stroke={iconFillColor(theme)}
|
||||||
strokeWidth="2"
|
strokeWidth="2"
|
||||||
/>
|
/>
|
||||||
<rect
|
<g
|
||||||
x="2.5"
|
|
||||||
y="2.5"
|
|
||||||
width="30"
|
|
||||||
height="30"
|
|
||||||
fill={handlerColor(theme)}
|
fill={handlerColor(theme)}
|
||||||
stroke={iconFillColor(theme)}
|
stroke={iconFillColor(theme)}
|
||||||
strokeWidth="6"
|
strokeWidth="6"
|
||||||
/>
|
>
|
||||||
<rect
|
<rect x="2.5" y="2.5" width="30" height="30" />
|
||||||
x="2.5"
|
<rect x="2.5" y="149.5" width="30" height="30" />
|
||||||
y="149.5"
|
<rect x="147.5" y="149.5" width="30" height="30" />
|
||||||
width="30"
|
<rect x="147.5" y="2.5" width="30" height="30" />
|
||||||
height="30"
|
</g>
|
||||||
fill={handlerColor(theme)}
|
|
||||||
stroke={iconFillColor(theme)}
|
|
||||||
strokeWidth="6"
|
|
||||||
/>
|
|
||||||
<rect
|
|
||||||
x="147.5"
|
|
||||||
y="149.5"
|
|
||||||
width="30"
|
|
||||||
height="30"
|
|
||||||
fill={handlerColor(theme)}
|
|
||||||
stroke={iconFillColor(theme)}
|
|
||||||
strokeWidth="6"
|
|
||||||
/>
|
|
||||||
<rect
|
|
||||||
x="147.5"
|
|
||||||
y="2.5"
|
|
||||||
width="30"
|
|
||||||
height="30"
|
|
||||||
fill={handlerColor(theme)}
|
|
||||||
stroke={iconFillColor(theme)}
|
|
||||||
strokeWidth="6"
|
|
||||||
/>
|
|
||||||
</>,
|
</>,
|
||||||
{ width: 182, height: 182, mirror: true },
|
{ width: 182, height: 182, mirror: true },
|
||||||
),
|
),
|
||||||
@@ -536,60 +520,18 @@ export const UngroupIcon = React.memo(
|
|||||||
stroke={iconFillColor(theme)}
|
stroke={iconFillColor(theme)}
|
||||||
strokeWidth="2"
|
strokeWidth="2"
|
||||||
/>
|
/>
|
||||||
<rect
|
<g
|
||||||
x="2.5"
|
|
||||||
y="2.5"
|
|
||||||
width="30"
|
|
||||||
height="30"
|
|
||||||
fill={handlerColor(theme)}
|
fill={handlerColor(theme)}
|
||||||
stroke={iconFillColor(theme)}
|
stroke={iconFillColor(theme)}
|
||||||
strokeWidth="6"
|
strokeWidth="6"
|
||||||
/>
|
>
|
||||||
<rect
|
<rect x="2.5" y="2.5" width="30" height="30" />
|
||||||
x="78.5"
|
<rect x="78.5" y="149.5" width="30" height="30" />
|
||||||
y="149.5"
|
<rect x="147.5" y="149.5" width="30" height="30" />
|
||||||
width="30"
|
<rect x="147.5" y="78.5" width="30" height="30" />
|
||||||
height="30"
|
<rect x="105.5" y="2.5" width="30" height="30" />
|
||||||
fill={handlerColor(theme)}
|
<rect x="2.5" y="102.5" width="30" height="30" />
|
||||||
stroke={iconFillColor(theme)}
|
</g>
|
||||||
strokeWidth="6"
|
|
||||||
/>
|
|
||||||
<rect
|
|
||||||
x="147.5"
|
|
||||||
y="149.5"
|
|
||||||
width="30"
|
|
||||||
height="30"
|
|
||||||
fill={handlerColor(theme)}
|
|
||||||
stroke={iconFillColor(theme)}
|
|
||||||
strokeWidth="6"
|
|
||||||
/>
|
|
||||||
<rect
|
|
||||||
x="147.5"
|
|
||||||
y="78.5"
|
|
||||||
width="30"
|
|
||||||
height="30"
|
|
||||||
fill={handlerColor(theme)}
|
|
||||||
stroke={iconFillColor(theme)}
|
|
||||||
strokeWidth="6"
|
|
||||||
/>
|
|
||||||
<rect
|
|
||||||
x="105.5"
|
|
||||||
y="2.5"
|
|
||||||
width="30"
|
|
||||||
height="30"
|
|
||||||
fill={handlerColor(theme)}
|
|
||||||
stroke={iconFillColor(theme)}
|
|
||||||
strokeWidth="6"
|
|
||||||
/>
|
|
||||||
<rect
|
|
||||||
x="2.5"
|
|
||||||
y="102.5"
|
|
||||||
width="30"
|
|
||||||
height="30"
|
|
||||||
fill={handlerColor(theme)}
|
|
||||||
stroke={iconFillColor(theme)}
|
|
||||||
strokeWidth="6"
|
|
||||||
/>
|
|
||||||
</>,
|
</>,
|
||||||
{ width: 182, height: 182, mirror: true },
|
{ width: 182, height: 182, mirror: true },
|
||||||
),
|
),
|
||||||
@@ -631,9 +573,10 @@ export const StrokeWidthIcon = React.memo(
|
|||||||
({ theme, strokeWidth }: { theme: "light" | "dark"; strokeWidth: number }) =>
|
({ theme, strokeWidth }: { theme: "light" | "dark"; strokeWidth: number }) =>
|
||||||
createIcon(
|
createIcon(
|
||||||
<path
|
<path
|
||||||
d="M6 10H34"
|
d="M6 10H32"
|
||||||
stroke={iconFillColor(theme)}
|
stroke={iconFillColor(theme)}
|
||||||
strokeWidth={strokeWidth}
|
strokeWidth={strokeWidth}
|
||||||
|
strokeLinecap="round"
|
||||||
fill="none"
|
fill="none"
|
||||||
/>,
|
/>,
|
||||||
{ width: 40, height: 20 },
|
{ width: 40, height: 20 },
|
||||||
@@ -648,6 +591,7 @@ export const StrokeStyleSolidIcon = React.memo(
|
|||||||
stroke={iconFillColor(theme)}
|
stroke={iconFillColor(theme)}
|
||||||
strokeWidth={2}
|
strokeWidth={2}
|
||||||
fill="none"
|
fill="none"
|
||||||
|
strokeLinecap="round"
|
||||||
/>,
|
/>,
|
||||||
{
|
{
|
||||||
width: 40,
|
width: 40,
|
||||||
@@ -665,6 +609,7 @@ export const StrokeStyleDashedIcon = React.memo(
|
|||||||
strokeWidth={2.5}
|
strokeWidth={2.5}
|
||||||
strokeDasharray={"10, 8"}
|
strokeDasharray={"10, 8"}
|
||||||
fill="none"
|
fill="none"
|
||||||
|
strokeLinecap="round"
|
||||||
/>,
|
/>,
|
||||||
{ width: 40, height: 20 },
|
{ width: 40, height: 20 },
|
||||||
),
|
),
|
||||||
@@ -674,11 +619,12 @@ export const StrokeStyleDottedIcon = React.memo(
|
|||||||
({ theme }: { theme: "light" | "dark" }) =>
|
({ theme }: { theme: "light" | "dark" }) =>
|
||||||
createIcon(
|
createIcon(
|
||||||
<path
|
<path
|
||||||
d="M6 10H34"
|
d="M6 10H36"
|
||||||
stroke={iconFillColor(theme)}
|
stroke={iconFillColor(theme)}
|
||||||
strokeWidth={2.5}
|
strokeWidth={2.5}
|
||||||
strokeDasharray={"4, 4"}
|
strokeDasharray={"2, 4.5"}
|
||||||
fill="none"
|
fill="none"
|
||||||
|
strokeLinecap="round"
|
||||||
/>,
|
/>,
|
||||||
{ width: 40, height: 20 },
|
{ width: 40, height: 20 },
|
||||||
),
|
),
|
||||||
@@ -691,6 +637,7 @@ export const SloppinessArchitectIcon = React.memo(
|
|||||||
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"
|
||||||
stroke={iconFillColor(theme)}
|
stroke={iconFillColor(theme)}
|
||||||
strokeWidth={2}
|
strokeWidth={2}
|
||||||
|
strokeLinecap="round"
|
||||||
fill="none"
|
fill="none"
|
||||||
/>,
|
/>,
|
||||||
{ width: 40, height: 20, mirror: true },
|
{ width: 40, height: 20, mirror: true },
|
||||||
@@ -704,6 +651,7 @@ export const SloppinessArtistIcon = React.memo(
|
|||||||
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"
|
||||||
stroke={iconFillColor(theme)}
|
stroke={iconFillColor(theme)}
|
||||||
strokeWidth={2}
|
strokeWidth={2}
|
||||||
|
strokeLinecap="round"
|
||||||
fill="none"
|
fill="none"
|
||||||
/>,
|
/>,
|
||||||
{ width: 40, height: 20, mirror: true },
|
{ width: 40, height: 20, mirror: true },
|
||||||
@@ -717,6 +665,7 @@ export const SloppinessCartoonistIcon = React.memo(
|
|||||||
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"
|
||||||
stroke={iconFillColor(theme)}
|
stroke={iconFillColor(theme)}
|
||||||
strokeWidth={2}
|
strokeWidth={2}
|
||||||
|
strokeLinecap="round"
|
||||||
fill="none"
|
fill="none"
|
||||||
/>,
|
/>,
|
||||||
{ width: 40, height: 20, mirror: true },
|
{ width: 40, height: 20, mirror: true },
|
||||||
@@ -730,6 +679,7 @@ export const EdgeSharpIcon = React.memo(
|
|||||||
d="M10 17L10 5L35 5"
|
d="M10 17L10 5L35 5"
|
||||||
stroke={iconFillColor(theme)}
|
stroke={iconFillColor(theme)}
|
||||||
strokeWidth={2}
|
strokeWidth={2}
|
||||||
|
strokeLinecap="round"
|
||||||
fill="none"
|
fill="none"
|
||||||
/>,
|
/>,
|
||||||
{ width: 40, height: 20, mirror: true },
|
{ width: 40, height: 20, mirror: true },
|
||||||
@@ -743,6 +693,7 @@ export const EdgeRoundIcon = React.memo(
|
|||||||
d="M10 17V15C10 8 13 5 21 5L33.5 5"
|
d="M10 17V15C10 8 13 5 21 5L33.5 5"
|
||||||
stroke={iconFillColor(theme)}
|
stroke={iconFillColor(theme)}
|
||||||
strokeWidth={2}
|
strokeWidth={2}
|
||||||
|
strokeLinecap="round"
|
||||||
fill="none"
|
fill="none"
|
||||||
/>,
|
/>,
|
||||||
{ width: 40, height: 20, mirror: true },
|
{ width: 40, height: 20, mirror: true },
|
||||||
@@ -902,6 +853,7 @@ export const TextAlignLeftIcon = React.memo(
|
|||||||
<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"
|
||||||
fill={iconFillColor(theme)}
|
fill={iconFillColor(theme)}
|
||||||
|
strokeLinecap="round"
|
||||||
/>,
|
/>,
|
||||||
{ width: 448, height: 512 },
|
{ width: 448, height: 512 },
|
||||||
),
|
),
|
||||||
@@ -924,6 +876,7 @@ export const TextAlignRightIcon = React.memo(
|
|||||||
<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"
|
||||||
fill={iconFillColor(theme)}
|
fill={iconFillColor(theme)}
|
||||||
|
strokeLinecap="round"
|
||||||
/>,
|
/>,
|
||||||
{ width: 448, height: 512 },
|
{ width: 448, height: 512 },
|
||||||
),
|
),
|
||||||
|
|||||||
+7
-3
@@ -91,6 +91,8 @@ export const EXPORT_DATA_TYPES = {
|
|||||||
excalidrawLibrary: "excalidrawlib",
|
excalidrawLibrary: "excalidrawlib",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
export const EXPORT_SOURCE = window.location.origin;
|
||||||
|
|
||||||
export const STORAGE_KEYS = {
|
export const STORAGE_KEYS = {
|
||||||
LOCAL_STORAGE_LIBRARY: "excalidraw-library",
|
LOCAL_STORAGE_LIBRARY: "excalidraw-library",
|
||||||
} as const;
|
} as const;
|
||||||
@@ -129,14 +131,16 @@ export const DEFAULT_UI_OPTIONS: AppProps["UIOptions"] = {
|
|||||||
canvasActions: {
|
canvasActions: {
|
||||||
changeViewBackgroundColor: true,
|
changeViewBackgroundColor: true,
|
||||||
clearCanvas: true,
|
clearCanvas: true,
|
||||||
export: true,
|
export: { saveFileToDisk: true },
|
||||||
loadScene: true,
|
loadScene: true,
|
||||||
saveAsScene: true,
|
saveToActiveFile: true,
|
||||||
saveScene: true,
|
|
||||||
theme: true,
|
theme: true,
|
||||||
|
saveAsImage: true,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const MQ_MAX_WIDTH_PORTRAIT = 730;
|
export const MQ_MAX_WIDTH_PORTRAIT = 730;
|
||||||
export const MQ_MAX_WIDTH_LANDSCAPE = 1000;
|
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;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
clip: rect(1px, 1px, 1px, 1px);
|
clip: rect(1px, 1px, 1px, 1px);
|
||||||
white-space: nowrap; /* added line */
|
white-space: nowrap; /* added line */
|
||||||
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.LoadingMessage {
|
.LoadingMessage {
|
||||||
|
|||||||
+9
-26
@@ -19,6 +19,10 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
// serves 2 purposes:
|
// serves 2 purposes:
|
||||||
// 1. prevent selecting text outside the component when double-clicking or
|
// 1. prevent selecting text outside the component when double-clicking or
|
||||||
// dragging inside it (e.g. on canvas)
|
// dragging inside it (e.g. on canvas)
|
||||||
@@ -328,8 +332,8 @@
|
|||||||
.App-menu_bottom {
|
.App-menu_bottom {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
grid-template-columns: 1fr auto 1fr;
|
grid-template-columns: min-content auto min-content;
|
||||||
grid-gap: 4px;
|
grid-gap: 15px;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
cursor: default;
|
cursor: default;
|
||||||
pointer-events: none !important;
|
pointer-events: none !important;
|
||||||
@@ -354,10 +358,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.layer-ui__wrapper:not(.disable-pointerEvents) .App-menu_bottom > * {
|
|
||||||
pointer-events: all;
|
|
||||||
}
|
|
||||||
|
|
||||||
.App-menu_bottom > *:first-child {
|
.App-menu_bottom > *:first-child {
|
||||||
justify-self: flex-start;
|
justify-self: flex-start;
|
||||||
}
|
}
|
||||||
@@ -415,13 +415,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&.dropdown-select--floating {
|
&.dropdown-select--floating {
|
||||||
position: absolute;
|
|
||||||
margin: 0.5em;
|
margin: 0.5em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.dropdown-select__language.dropdown-select--floating {
|
.dropdown-select__language.dropdown-select--floating {
|
||||||
position: absolute;
|
|
||||||
bottom: 10px;
|
bottom: 10px;
|
||||||
|
|
||||||
:root[dir="ltr"] & {
|
:root[dir="ltr"] & {
|
||||||
@@ -457,13 +455,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.help-icon {
|
.help-icon {
|
||||||
position: absolute;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
fill: $oc-gray-6;
|
fill: $oc-gray-6;
|
||||||
bottom: 14px;
|
|
||||||
width: 1.5rem;
|
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);
|
||||||
|
|
||||||
@@ -472,11 +469,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
:root[dir="ltr"] & {
|
:root[dir="ltr"] & {
|
||||||
right: 14px;
|
margin-right: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
:root[dir="rtl"] & {
|
:root[dir="rtl"] & {
|
||||||
left: 14px;
|
margin-left: 14px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -496,20 +493,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.github-corner {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
z-index: 2;
|
|
||||||
|
|
||||||
:root[dir="ltr"] & {
|
|
||||||
right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[dir="rtl"] & {
|
|
||||||
left: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.zen-mode-visibility {
|
.zen-mode-visibility {
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
|
|||||||
+4
-2
@@ -14,11 +14,12 @@
|
|||||||
--focus-highlight-color: #{$oc-blue-2};
|
--focus-highlight-color: #{$oc-blue-2};
|
||||||
--icon-fill-color: #{$oc-black};
|
--icon-fill-color: #{$oc-black};
|
||||||
--icon-green-fill-color: #{$oc-green-9};
|
--icon-green-fill-color: #{$oc-green-9};
|
||||||
|
--default-bg-color: #{$oc-white};
|
||||||
--input-bg-color: #{$oc-white};
|
--input-bg-color: #{$oc-white};
|
||||||
--input-border-color: #{$oc-gray-3};
|
--input-border-color: #{$oc-gray-3};
|
||||||
--input-hover-bg-color: #{$oc-gray-1};
|
--input-hover-bg-color: #{$oc-gray-1};
|
||||||
--input-label-color: #{$oc-gray-7};
|
--input-label-color: #{$oc-gray-7};
|
||||||
--island-bg-color: rgba(255, 255, 255, 0.9);
|
--island-bg-color: rgba(255, 255, 255, 0.96);
|
||||||
--keybinding-color: #{$oc-gray-5};
|
--keybinding-color: #{$oc-gray-5};
|
||||||
--link-color: #{$oc-blue-7};
|
--link-color: #{$oc-blue-7};
|
||||||
--overlay-bg-color: #{transparentize($oc-white, 0.12)};
|
--overlay-bg-color: #{transparentize($oc-white, 0.12)};
|
||||||
@@ -56,11 +57,12 @@
|
|||||||
--focus-highlight-color: #{$oc-blue-6};
|
--focus-highlight-color: #{$oc-blue-6};
|
||||||
--icon-fill-color: #{$oc-gray-4};
|
--icon-fill-color: #{$oc-gray-4};
|
||||||
--icon-green-fill-color: #{$oc-green-4};
|
--icon-green-fill-color: #{$oc-green-4};
|
||||||
|
--default-bg-color: #121212;
|
||||||
--input-bg-color: #121212;
|
--input-bg-color: #121212;
|
||||||
--input-border-color: #2e2e2e;
|
--input-border-color: #2e2e2e;
|
||||||
--input-hover-bg-color: #181818;
|
--input-hover-bg-color: #181818;
|
||||||
--input-label-color: #{$oc-gray-2};
|
--input-label-color: #{$oc-gray-2};
|
||||||
--island-bg-color: #1e1e1e;
|
--island-bg-color: rgba(30, 30, 30, 0.98);
|
||||||
--keybinding-color: #{$oc-gray-6};
|
--keybinding-color: #{$oc-gray-6};
|
||||||
--overlay-bg-color: #{transparentize($oc-gray-8, 0.88)};
|
--overlay-bg-color: #{transparentize($oc-gray-8, 0.88)};
|
||||||
--popup-bg-color: #2c2c2c;
|
--popup-bg-color: #2c2c2c;
|
||||||
|
|||||||
+2
-2
@@ -7,7 +7,7 @@ import { calculateScrollCenter } from "../scene";
|
|||||||
import { AppState } from "../types";
|
import { AppState } from "../types";
|
||||||
import { isValidExcalidrawData } from "./json";
|
import { isValidExcalidrawData } from "./json";
|
||||||
import { restore } from "./restore";
|
import { restore } from "./restore";
|
||||||
import { LibraryData } from "./types";
|
import { ImportedLibraryData } from "./types";
|
||||||
|
|
||||||
const parseFileContents = async (blob: Blob | File) => {
|
const parseFileContents = async (blob: Blob | File) => {
|
||||||
let contents: string;
|
let contents: string;
|
||||||
@@ -114,7 +114,7 @@ export const loadFromBlob = async (
|
|||||||
|
|
||||||
export const loadLibraryFromBlob = async (blob: Blob) => {
|
export const loadLibraryFromBlob = async (blob: Blob) => {
|
||||||
const contents = await parseFileContents(blob);
|
const contents = await parseFileContents(blob);
|
||||||
const data: LibraryData = JSON.parse(contents);
|
const data: ImportedLibraryData = JSON.parse(contents);
|
||||||
if (data.type !== EXPORT_DATA_TYPES.excalidrawLibrary) {
|
if (data.type !== EXPORT_DATA_TYPES.excalidrawLibrary) {
|
||||||
throw new Error(t("alerts.couldNotLoadInvalidFile"));
|
throw new Error(t("alerts.couldNotLoadInvalidFile"));
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-13
@@ -1,6 +1,6 @@
|
|||||||
import { fileSave } from "browser-fs-access";
|
import { fileSave } from "browser-fs-access";
|
||||||
import {
|
import {
|
||||||
copyCanvasToClipboardAsPng,
|
copyBlobToClipboardAsPng,
|
||||||
copyTextToSystemClipboard,
|
copyTextToSystemClipboard,
|
||||||
} from "../clipboard";
|
} from "../clipboard";
|
||||||
import { NonDeletedExcalidrawElement } from "../element/types";
|
import { NonDeletedExcalidrawElement } from "../element/types";
|
||||||
@@ -18,21 +18,18 @@ export const exportCanvas = async (
|
|||||||
type: ExportType,
|
type: ExportType,
|
||||||
elements: readonly NonDeletedExcalidrawElement[],
|
elements: readonly NonDeletedExcalidrawElement[],
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
canvas: HTMLCanvasElement,
|
|
||||||
{
|
{
|
||||||
exportBackground,
|
exportBackground,
|
||||||
exportPadding = 10,
|
exportPadding = 10,
|
||||||
viewBackgroundColor,
|
viewBackgroundColor,
|
||||||
name,
|
name,
|
||||||
scale = 1,
|
scale = 1,
|
||||||
shouldAddWatermark,
|
|
||||||
}: {
|
}: {
|
||||||
exportBackground: boolean;
|
exportBackground: boolean;
|
||||||
exportPadding?: number;
|
exportPadding?: number;
|
||||||
viewBackgroundColor: string;
|
viewBackgroundColor: string;
|
||||||
name: string;
|
name: string;
|
||||||
scale?: number;
|
scale?: number;
|
||||||
shouldAddWatermark: boolean;
|
|
||||||
},
|
},
|
||||||
) => {
|
) => {
|
||||||
if (elements.length === 0) {
|
if (elements.length === 0) {
|
||||||
@@ -45,7 +42,6 @@ export const exportCanvas = async (
|
|||||||
viewBackgroundColor,
|
viewBackgroundColor,
|
||||||
exportPadding,
|
exportPadding,
|
||||||
scale,
|
scale,
|
||||||
shouldAddWatermark,
|
|
||||||
metadata:
|
metadata:
|
||||||
appState.exportEmbedScene && type === "svg"
|
appState.exportEmbedScene && type === "svg"
|
||||||
? await (
|
? await (
|
||||||
@@ -72,14 +68,14 @@ export const exportCanvas = async (
|
|||||||
viewBackgroundColor,
|
viewBackgroundColor,
|
||||||
exportPadding,
|
exportPadding,
|
||||||
scale,
|
scale,
|
||||||
shouldAddWatermark,
|
|
||||||
});
|
});
|
||||||
tempCanvas.style.display = "none";
|
tempCanvas.style.display = "none";
|
||||||
document.body.appendChild(tempCanvas);
|
document.body.appendChild(tempCanvas);
|
||||||
|
let blob = await canvasToBlob(tempCanvas);
|
||||||
|
tempCanvas.remove();
|
||||||
|
|
||||||
if (type === "png") {
|
if (type === "png") {
|
||||||
const fileName = `${name}.png`;
|
const fileName = `${name}.png`;
|
||||||
let blob = await canvasToBlob(tempCanvas);
|
|
||||||
if (appState.exportEmbedScene) {
|
if (appState.exportEmbedScene) {
|
||||||
blob = await (
|
blob = await (
|
||||||
await import(/* webpackChunkName: "image" */ "./image")
|
await import(/* webpackChunkName: "image" */ "./image")
|
||||||
@@ -95,7 +91,7 @@ export const exportCanvas = async (
|
|||||||
});
|
});
|
||||||
} else if (type === "clipboard") {
|
} else if (type === "clipboard") {
|
||||||
try {
|
try {
|
||||||
await copyCanvasToClipboardAsPng(tempCanvas);
|
await copyBlobToClipboardAsPng(blob);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.name === "CANVAS_POSSIBLY_TOO_BIG") {
|
if (error.name === "CANVAS_POSSIBLY_TOO_BIG") {
|
||||||
throw error;
|
throw error;
|
||||||
@@ -103,9 +99,4 @@ export const exportCanvas = async (
|
|||||||
throw new Error(t("alerts.couldNotCopyToClipboard"));
|
throw new Error(t("alerts.couldNotCopyToClipboard"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// clean up the DOM
|
|
||||||
if (tempCanvas !== canvas) {
|
|
||||||
tempCanvas.remove();
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|||||||
+30
-28
@@ -1,28 +1,32 @@
|
|||||||
import { fileOpen, fileSave } from "browser-fs-access";
|
import { fileOpen, fileSave } from "browser-fs-access";
|
||||||
import { cleanAppStateForExport } from "../appState";
|
import { cleanAppStateForExport } from "../appState";
|
||||||
import { EXPORT_DATA_TYPES, 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 { loadFromBlob } from "./blob";
|
||||||
import { Library } from "./library";
|
|
||||||
import { ImportedDataState } from "./types";
|
import {
|
||||||
|
ExportedDataState,
|
||||||
|
ImportedDataState,
|
||||||
|
ExportedLibraryData,
|
||||||
|
} from "./types";
|
||||||
|
import Library from "./library";
|
||||||
|
|
||||||
export const serializeAsJSON = (
|
export const serializeAsJSON = (
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly ExcalidrawElement[],
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
): string =>
|
): string => {
|
||||||
JSON.stringify(
|
const data: ExportedDataState = {
|
||||||
{
|
type: EXPORT_DATA_TYPES.excalidraw,
|
||||||
type: EXPORT_DATA_TYPES.excalidraw,
|
version: 2,
|
||||||
version: 2,
|
source: EXPORT_SOURCE,
|
||||||
source: window.location.origin,
|
elements: clearElementsForExport(elements),
|
||||||
elements: clearElementsForExport(elements),
|
appState: cleanAppStateForExport(appState),
|
||||||
appState: cleanAppStateForExport(appState),
|
};
|
||||||
},
|
|
||||||
null,
|
return JSON.stringify(data, null, 2);
|
||||||
2,
|
};
|
||||||
);
|
|
||||||
|
|
||||||
export const saveAsJSON = async (
|
export const saveAsJSON = async (
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly ExcalidrawElement[],
|
||||||
@@ -85,17 +89,15 @@ export const isValidLibrary = (json: any) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const saveLibraryAsJSON = async () => {
|
export const saveLibraryAsJSON = async (library: Library) => {
|
||||||
const library = await Library.loadLibrary();
|
const libraryItems = await library.loadLibrary();
|
||||||
const serialized = JSON.stringify(
|
const data: ExportedLibraryData = {
|
||||||
{
|
type: EXPORT_DATA_TYPES.excalidrawLibrary,
|
||||||
type: EXPORT_DATA_TYPES.excalidrawLibrary,
|
version: 1,
|
||||||
version: 1,
|
source: EXPORT_SOURCE,
|
||||||
library,
|
library: libraryItems,
|
||||||
},
|
};
|
||||||
null,
|
const serialized = JSON.stringify(data, null, 2);
|
||||||
2,
|
|
||||||
);
|
|
||||||
const fileName = "library.excalidrawlib";
|
const fileName = "library.excalidrawlib";
|
||||||
const blob = new Blob([serialized], {
|
const blob = new Blob([serialized], {
|
||||||
type: MIME_TYPES.excalidrawlib,
|
type: MIME_TYPES.excalidrawlib,
|
||||||
@@ -107,7 +109,7 @@ export const saveLibraryAsJSON = async () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const importLibraryFromJSON = async () => {
|
export const importLibraryFromJSON = async (library: Library) => {
|
||||||
const blob = await fileOpen({
|
const blob = await fileOpen({
|
||||||
description: "Excalidraw library files",
|
description: "Excalidraw library 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
|
||||||
@@ -116,5 +118,5 @@ export const importLibraryFromJSON = async () => {
|
|||||||
extensions: [".json", ".excalidrawlib"],
|
extensions: [".json", ".excalidrawlib"],
|
||||||
*/
|
*/
|
||||||
});
|
});
|
||||||
await Library.importLibrary(blob);
|
await library.importLibrary(blob);
|
||||||
};
|
};
|
||||||
|
|||||||
+45
-32
@@ -1,22 +1,29 @@
|
|||||||
import { loadLibraryFromBlob } from "./blob";
|
import { loadLibraryFromBlob } from "./blob";
|
||||||
import { LibraryItems, LibraryItem } from "../types";
|
import { LibraryItems, LibraryItem } from "../types";
|
||||||
import { restoreElements } from "./restore";
|
import { restoreElements } from "./restore";
|
||||||
import { STORAGE_KEYS } from "../constants";
|
|
||||||
import { getNonDeletedElements } from "../element";
|
import { getNonDeletedElements } from "../element";
|
||||||
import { NonDeleted, ExcalidrawElement } from "../element/types";
|
import App from "../components/App";
|
||||||
import { nanoid } from "nanoid";
|
|
||||||
|
|
||||||
export class Library {
|
class Library {
|
||||||
private static libraryCache: LibraryItems | null = null;
|
private libraryCache: LibraryItems | null = null;
|
||||||
public static csrfToken = nanoid();
|
private app: App;
|
||||||
|
|
||||||
static resetLibrary = () => {
|
constructor(app: App) {
|
||||||
Library.libraryCache = null;
|
this.app = app;
|
||||||
localStorage.removeItem(STORAGE_KEYS.LOCAL_STORAGE_LIBRARY);
|
}
|
||||||
|
|
||||||
|
resetLibrary = async () => {
|
||||||
|
await this.app.props.onLibraryChange?.([]);
|
||||||
|
this.libraryCache = [];
|
||||||
|
};
|
||||||
|
|
||||||
|
restoreLibraryItem = (libraryItem: LibraryItem): LibraryItem | null => {
|
||||||
|
const elements = getNonDeletedElements(restoreElements(libraryItem));
|
||||||
|
return elements.length ? elements : null;
|
||||||
};
|
};
|
||||||
|
|
||||||
/** imports library (currently merges, removing duplicates) */
|
/** imports library (currently merges, removing duplicates) */
|
||||||
static async importLibrary(blob: Blob) {
|
async importLibrary(blob: Blob) {
|
||||||
const libraryFile = await loadLibraryFromBlob(blob);
|
const libraryFile = await loadLibraryFromBlob(blob);
|
||||||
if (!libraryFile || !libraryFile.library) {
|
if (!libraryFile || !libraryFile.library) {
|
||||||
return;
|
return;
|
||||||
@@ -46,37 +53,41 @@ export class Library {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const existingLibraryItems = await Library.loadLibrary();
|
const existingLibraryItems = await this.loadLibrary();
|
||||||
|
|
||||||
const filtered = libraryFile.library!.reduce((acc, libraryItem) => {
|
const filtered = libraryFile.library!.reduce((acc, libraryItem) => {
|
||||||
const restored = getNonDeletedElements(restoreElements(libraryItem));
|
const restoredItem = this.restoreLibraryItem(libraryItem);
|
||||||
if (isUniqueitem(existingLibraryItems, restored)) {
|
if (restoredItem && isUniqueitem(existingLibraryItems, restoredItem)) {
|
||||||
acc.push(restored);
|
acc.push(restoredItem);
|
||||||
}
|
}
|
||||||
return acc;
|
return acc;
|
||||||
}, [] as (readonly NonDeleted<ExcalidrawElement>[])[]);
|
}, [] as Mutable<LibraryItems>);
|
||||||
|
|
||||||
Library.saveLibrary([...existingLibraryItems, ...filtered]);
|
await this.saveLibrary([...existingLibraryItems, ...filtered]);
|
||||||
}
|
}
|
||||||
|
|
||||||
static loadLibrary = (): Promise<LibraryItems> => {
|
loadLibrary = (): Promise<LibraryItems> => {
|
||||||
return new Promise(async (resolve) => {
|
return new Promise(async (resolve) => {
|
||||||
if (Library.libraryCache) {
|
if (this.libraryCache) {
|
||||||
return resolve(JSON.parse(JSON.stringify(Library.libraryCache)));
|
return resolve(JSON.parse(JSON.stringify(this.libraryCache)));
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = localStorage.getItem(STORAGE_KEYS.LOCAL_STORAGE_LIBRARY);
|
const libraryItems = this.app.libraryItemsFromStorage;
|
||||||
if (!data) {
|
if (!libraryItems) {
|
||||||
return resolve([]);
|
return resolve([]);
|
||||||
}
|
}
|
||||||
|
|
||||||
const items = (JSON.parse(data) as LibraryItems).map((elements) =>
|
const items = libraryItems.reduce((acc, item) => {
|
||||||
restoreElements(elements),
|
const restoredItem = this.restoreLibraryItem(item);
|
||||||
) as Mutable<LibraryItems>;
|
if (restoredItem) {
|
||||||
|
acc.push(item);
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, [] as Mutable<LibraryItems>);
|
||||||
|
|
||||||
// clone to ensure we don't mutate the cached library elements in the app
|
// clone to ensure we don't mutate the cached library elements in the app
|
||||||
Library.libraryCache = JSON.parse(JSON.stringify(items));
|
this.libraryCache = JSON.parse(JSON.stringify(items));
|
||||||
|
|
||||||
resolve(items);
|
resolve(items);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -86,17 +97,19 @@ export class Library {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
static saveLibrary = (items: LibraryItems) => {
|
saveLibrary = async (items: LibraryItems) => {
|
||||||
const prevLibraryItems = Library.libraryCache;
|
const prevLibraryItems = this.libraryCache;
|
||||||
try {
|
try {
|
||||||
const serializedItems = JSON.stringify(items);
|
const serializedItems = JSON.stringify(items);
|
||||||
// cache optimistically so that consumers have access to the latest
|
// cache optimistically so that the app has access to the latest
|
||||||
// immediately
|
// immediately
|
||||||
Library.libraryCache = JSON.parse(serializedItems);
|
this.libraryCache = JSON.parse(serializedItems);
|
||||||
localStorage.setItem(STORAGE_KEYS.LOCAL_STORAGE_LIBRARY, serializedItems);
|
await this.app.props.onLibraryChange?.(items);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Library.libraryCache = prevLibraryItems;
|
this.libraryCache = prevLibraryItems;
|
||||||
console.error(error);
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default Library;
|
||||||
|
|||||||
+74
-17
@@ -4,7 +4,7 @@ import {
|
|||||||
ExcalidrawSelectionElement,
|
ExcalidrawSelectionElement,
|
||||||
} from "../element/types";
|
} from "../element/types";
|
||||||
import { AppState, NormalizedZoomValue } from "../types";
|
import { AppState, NormalizedZoomValue } from "../types";
|
||||||
import { DataState, ImportedDataState } from "./types";
|
import { ImportedDataState } from "./types";
|
||||||
import { isInvisiblySmallElement, getNormalizedDimensions } from "../element";
|
import { isInvisiblySmallElement, getNormalizedDimensions } from "../element";
|
||||||
import { isLinearElementType } from "../element/typeChecks";
|
import { isLinearElementType } from "../element/typeChecks";
|
||||||
import { randomId } from "../random";
|
import { randomId } from "../random";
|
||||||
@@ -15,6 +15,31 @@ import {
|
|||||||
DEFAULT_VERTICAL_ALIGN,
|
DEFAULT_VERTICAL_ALIGN,
|
||||||
} from "../constants";
|
} from "../constants";
|
||||||
import { getDefaultAppState } from "../appState";
|
import { getDefaultAppState } from "../appState";
|
||||||
|
import { LinearElementEditor } from "../element/linearElementEditor";
|
||||||
|
|
||||||
|
type RestoredAppState = Omit<
|
||||||
|
AppState,
|
||||||
|
"offsetTop" | "offsetLeft" | "width" | "height"
|
||||||
|
>;
|
||||||
|
|
||||||
|
export const AllowedExcalidrawElementTypes: Record<
|
||||||
|
ExcalidrawElement["type"],
|
||||||
|
true
|
||||||
|
> = {
|
||||||
|
selection: true,
|
||||||
|
text: true,
|
||||||
|
rectangle: true,
|
||||||
|
diamond: true,
|
||||||
|
ellipse: true,
|
||||||
|
line: true,
|
||||||
|
arrow: true,
|
||||||
|
freedraw: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RestoredDataState = {
|
||||||
|
elements: ExcalidrawElement[];
|
||||||
|
appState: RestoredAppState;
|
||||||
|
};
|
||||||
|
|
||||||
const getFontFamilyByName = (fontFamilyName: string): FontFamily => {
|
const getFontFamilyByName = (fontFamilyName: string): FontFamily => {
|
||||||
for (const [id, fontFamilyString] of Object.entries(FONT_FAMILY)) {
|
for (const [id, fontFamilyString] of Object.entries(FONT_FAMILY)) {
|
||||||
@@ -25,12 +50,18 @@ const getFontFamilyByName = (fontFamilyName: string): FontFamily => {
|
|||||||
return DEFAULT_FONT_FAMILY;
|
return DEFAULT_FONT_FAMILY;
|
||||||
};
|
};
|
||||||
|
|
||||||
const restoreElementWithProperties = <T extends ExcalidrawElement>(
|
const restoreElementWithProperties = <
|
||||||
|
T extends ExcalidrawElement,
|
||||||
|
K extends keyof Omit<
|
||||||
|
Required<T>,
|
||||||
|
Exclude<keyof ExcalidrawElement, "type" | "x" | "y">
|
||||||
|
>
|
||||||
|
>(
|
||||||
element: Required<T>,
|
element: Required<T>,
|
||||||
extra: Omit<Required<T>, keyof ExcalidrawElement>,
|
extra: Pick<T, K>,
|
||||||
): T => {
|
): T => {
|
||||||
const base: Pick<T, keyof ExcalidrawElement> = {
|
const base: Pick<T, keyof ExcalidrawElement> = {
|
||||||
type: element.type,
|
type: (extra as Partial<T>).type || element.type,
|
||||||
// all elements must have version > 0 so getSceneVersion() will pick up
|
// all elements must have version > 0 so getSceneVersion() will pick up
|
||||||
// newly added elements
|
// newly added elements
|
||||||
version: element.version || 1,
|
version: element.version || 1,
|
||||||
@@ -43,8 +74,8 @@ const restoreElementWithProperties = <T extends ExcalidrawElement>(
|
|||||||
roughness: element.roughness ?? 1,
|
roughness: element.roughness ?? 1,
|
||||||
opacity: element.opacity == null ? 100 : element.opacity,
|
opacity: element.opacity == null ? 100 : element.opacity,
|
||||||
angle: element.angle || 0,
|
angle: element.angle || 0,
|
||||||
x: element.x || 0,
|
x: (extra as Partial<T>).x ?? element.x ?? 0,
|
||||||
y: element.y || 0,
|
y: (extra as Partial<T>).y ?? element.y ?? 0,
|
||||||
strokeColor: element.strokeColor,
|
strokeColor: element.strokeColor,
|
||||||
backgroundColor: element.backgroundColor,
|
backgroundColor: element.backgroundColor,
|
||||||
width: element.width || 0,
|
width: element.width || 0,
|
||||||
@@ -87,28 +118,51 @@ const restoreElement = (
|
|||||||
textAlign: element.textAlign || DEFAULT_TEXT_ALIGN,
|
textAlign: element.textAlign || DEFAULT_TEXT_ALIGN,
|
||||||
verticalAlign: element.verticalAlign || DEFAULT_VERTICAL_ALIGN,
|
verticalAlign: element.verticalAlign || DEFAULT_VERTICAL_ALIGN,
|
||||||
});
|
});
|
||||||
case "draw":
|
case "freedraw": {
|
||||||
|
return restoreElementWithProperties(element, {
|
||||||
|
points: element.points,
|
||||||
|
lastCommittedPoint: null,
|
||||||
|
simulatePressure: element.simulatePressure,
|
||||||
|
pressures: element.pressures,
|
||||||
|
});
|
||||||
|
}
|
||||||
case "line":
|
case "line":
|
||||||
|
// @ts-ignore LEGACY type
|
||||||
|
// eslint-disable-next-line no-fallthrough
|
||||||
|
case "draw":
|
||||||
case "arrow": {
|
case "arrow": {
|
||||||
const {
|
const {
|
||||||
startArrowhead = null,
|
startArrowhead = null,
|
||||||
endArrowhead = element.type === "arrow" ? "arrow" : null,
|
endArrowhead = element.type === "arrow" ? "arrow" : null,
|
||||||
} = element;
|
} = element;
|
||||||
|
|
||||||
|
let x = element.x;
|
||||||
|
let y = element.y;
|
||||||
|
let points = // migrate old arrow model to new one
|
||||||
|
!Array.isArray(element.points) || element.points.length < 2
|
||||||
|
? [
|
||||||
|
[0, 0],
|
||||||
|
[element.width, element.height],
|
||||||
|
]
|
||||||
|
: element.points;
|
||||||
|
|
||||||
|
if (points[0][0] !== 0 || points[0][1] !== 0) {
|
||||||
|
({ points, x, y } = LinearElementEditor.getNormalizedPoints(element));
|
||||||
|
}
|
||||||
|
|
||||||
return restoreElementWithProperties(element, {
|
return restoreElementWithProperties(element, {
|
||||||
|
type:
|
||||||
|
(element.type as ExcalidrawElement["type"] | "draw") === "draw"
|
||||||
|
? "line"
|
||||||
|
: element.type,
|
||||||
startBinding: element.startBinding,
|
startBinding: element.startBinding,
|
||||||
endBinding: element.endBinding,
|
endBinding: element.endBinding,
|
||||||
points:
|
|
||||||
// migrate old arrow model to new one
|
|
||||||
!Array.isArray(element.points) || element.points.length < 2
|
|
||||||
? [
|
|
||||||
[0, 0],
|
|
||||||
[element.width, element.height],
|
|
||||||
]
|
|
||||||
: element.points,
|
|
||||||
lastCommittedPoint: null,
|
lastCommittedPoint: null,
|
||||||
startArrowhead,
|
startArrowhead,
|
||||||
endArrowhead,
|
endArrowhead,
|
||||||
|
points,
|
||||||
|
x,
|
||||||
|
y,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// generic elements
|
// generic elements
|
||||||
@@ -144,7 +198,7 @@ export const restoreElements = (
|
|||||||
export const restoreAppState = (
|
export const restoreAppState = (
|
||||||
appState: ImportedDataState["appState"],
|
appState: ImportedDataState["appState"],
|
||||||
localAppState: Partial<AppState> | null,
|
localAppState: Partial<AppState> | null,
|
||||||
): DataState["appState"] => {
|
): RestoredAppState => {
|
||||||
appState = appState || {};
|
appState = appState || {};
|
||||||
|
|
||||||
const defaultAppState = getDefaultAppState();
|
const defaultAppState = getDefaultAppState();
|
||||||
@@ -166,6 +220,9 @@ export const restoreAppState = (
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
...nextAppState,
|
...nextAppState,
|
||||||
|
elementType: AllowedExcalidrawElementTypes[nextAppState.elementType]
|
||||||
|
? nextAppState.elementType
|
||||||
|
: "selection",
|
||||||
// Migrates from previous version where appState.zoom was a number
|
// Migrates from previous version where appState.zoom was a number
|
||||||
zoom:
|
zoom:
|
||||||
typeof appState.zoom === "number"
|
typeof appState.zoom === "number"
|
||||||
@@ -186,7 +243,7 @@ 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,
|
||||||
): DataState => {
|
): RestoredDataState => {
|
||||||
return {
|
return {
|
||||||
elements: restoreElements(data?.elements),
|
elements: restoreElements(data?.elements),
|
||||||
appState: restoreAppState(data?.appState, localAppState || null),
|
appState: restoreAppState(data?.appState, localAppState || null),
|
||||||
|
|||||||
+19
-15
@@ -1,26 +1,30 @@
|
|||||||
import { ExcalidrawElement } from "../element/types";
|
import { ExcalidrawElement } from "../element/types";
|
||||||
import { AppState, LibraryItems } from "../types";
|
import { AppState, LibraryItems } from "../types";
|
||||||
|
import type { cleanAppStateForExport } from "../appState";
|
||||||
|
|
||||||
export interface DataState {
|
export interface ExportedDataState {
|
||||||
type?: string;
|
type: string;
|
||||||
version?: string;
|
version: number;
|
||||||
source?: string;
|
source: string;
|
||||||
elements: readonly ExcalidrawElement[];
|
elements: readonly ExcalidrawElement[];
|
||||||
appState: Omit<AppState, "offsetTop" | "offsetLeft" | "width" | "height">;
|
appState: ReturnType<typeof cleanAppStateForExport>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ImportedDataState {
|
export interface ImportedDataState {
|
||||||
type?: string;
|
|
||||||
version?: string;
|
|
||||||
source?: string;
|
|
||||||
elements?: DataState["elements"] | null;
|
|
||||||
appState?: Partial<DataState["appState"]> | null;
|
|
||||||
scrollToContent?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface LibraryData {
|
|
||||||
type?: string;
|
type?: string;
|
||||||
version?: number;
|
version?: number;
|
||||||
source?: string;
|
source?: string;
|
||||||
library?: LibraryItems;
|
elements?: readonly ExcalidrawElement[] | null;
|
||||||
|
appState?: Readonly<Partial<AppState>> | null;
|
||||||
|
scrollToContent?: boolean;
|
||||||
|
libraryItems?: LibraryItems;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ExportedLibraryData {
|
||||||
|
type: string;
|
||||||
|
version: number;
|
||||||
|
source: string;
|
||||||
|
library: LibraryItems;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ImportedLibraryData extends Partial<ExportedLibraryData> {}
|
||||||
|
|||||||
+138
-52
@@ -1,4 +1,9 @@
|
|||||||
import { ExcalidrawElement, ExcalidrawLinearElement, Arrowhead } from "./types";
|
import {
|
||||||
|
ExcalidrawElement,
|
||||||
|
ExcalidrawLinearElement,
|
||||||
|
Arrowhead,
|
||||||
|
ExcalidrawFreeDrawElement,
|
||||||
|
} from "./types";
|
||||||
import { distance2d, rotate } from "../math";
|
import { distance2d, rotate } from "../math";
|
||||||
import rough from "roughjs/bin/rough";
|
import rough from "roughjs/bin/rough";
|
||||||
import { Drawable, Op } from "roughjs/bin/core";
|
import { Drawable, Op } from "roughjs/bin/core";
|
||||||
@@ -7,7 +12,7 @@ import {
|
|||||||
getShapeForElement,
|
getShapeForElement,
|
||||||
generateRoughOptions,
|
generateRoughOptions,
|
||||||
} from "../renderer/renderElement";
|
} from "../renderer/renderElement";
|
||||||
import { isLinearElement } from "./typeChecks";
|
import { isFreeDrawElement, isLinearElement } from "./typeChecks";
|
||||||
import { rescalePoints } from "../points";
|
import { rescalePoints } from "../points";
|
||||||
|
|
||||||
// x and y position of top left corner, x and y position of bottom right corner
|
// x and y position of top left corner, x and y position of bottom right corner
|
||||||
@@ -18,7 +23,9 @@ export type Bounds = readonly [number, number, number, number];
|
|||||||
export const getElementAbsoluteCoords = (
|
export const getElementAbsoluteCoords = (
|
||||||
element: ExcalidrawElement,
|
element: ExcalidrawElement,
|
||||||
): Bounds => {
|
): Bounds => {
|
||||||
if (isLinearElement(element)) {
|
if (isFreeDrawElement(element)) {
|
||||||
|
return getFreeDrawElementAbsoluteCoords(element);
|
||||||
|
} else if (isLinearElement(element)) {
|
||||||
return getLinearElementAbsoluteCoords(element);
|
return getLinearElementAbsoluteCoords(element);
|
||||||
}
|
}
|
||||||
return [
|
return [
|
||||||
@@ -120,9 +127,42 @@ const getMinMaxXYFromCurvePathOps = (
|
|||||||
return [minX, minY, maxX, maxY];
|
return [minX, minY, maxX, maxY];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getBoundsFromPoints = (
|
||||||
|
points: ExcalidrawFreeDrawElement["points"],
|
||||||
|
): [number, number, number, number] => {
|
||||||
|
let minX = Infinity;
|
||||||
|
let minY = Infinity;
|
||||||
|
let maxX = -Infinity;
|
||||||
|
let maxY = -Infinity;
|
||||||
|
|
||||||
|
for (const [x, y] of points) {
|
||||||
|
minX = Math.min(minX, x);
|
||||||
|
minY = Math.min(minY, y);
|
||||||
|
maxX = Math.max(maxX, x);
|
||||||
|
maxY = Math.max(maxY, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [minX, minY, maxX, maxY];
|
||||||
|
};
|
||||||
|
|
||||||
|
const getFreeDrawElementAbsoluteCoords = (
|
||||||
|
element: ExcalidrawFreeDrawElement,
|
||||||
|
): [number, number, number, number] => {
|
||||||
|
const [minX, minY, maxX, maxY] = getBoundsFromPoints(element.points);
|
||||||
|
|
||||||
|
return [
|
||||||
|
minX + element.x,
|
||||||
|
minY + element.y,
|
||||||
|
maxX + element.x,
|
||||||
|
maxY + element.y,
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
const getLinearElementAbsoluteCoords = (
|
const getLinearElementAbsoluteCoords = (
|
||||||
element: ExcalidrawLinearElement,
|
element: ExcalidrawLinearElement,
|
||||||
): [number, number, number, number] => {
|
): [number, number, number, number] => {
|
||||||
|
let coords: [number, number, number, number];
|
||||||
|
|
||||||
if (element.points.length < 2 || !getShapeForElement(element)) {
|
if (element.points.length < 2 || !getShapeForElement(element)) {
|
||||||
// XXX this is just a poor estimate and not very useful
|
// XXX this is just a poor estimate and not very useful
|
||||||
const { minX, minY, maxX, maxY } = element.points.reduce(
|
const { minX, minY, maxX, maxY } = element.points.reduce(
|
||||||
@@ -137,7 +177,21 @@ const getLinearElementAbsoluteCoords = (
|
|||||||
},
|
},
|
||||||
{ minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity },
|
{ minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity },
|
||||||
);
|
);
|
||||||
return [
|
coords = [
|
||||||
|
minX + element.x,
|
||||||
|
minY + element.y,
|
||||||
|
maxX + element.x,
|
||||||
|
maxY + element.y,
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
const shape = getShapeForElement(element) as Drawable[];
|
||||||
|
|
||||||
|
// first element is always the curve
|
||||||
|
const ops = getCurvePathOps(shape[0]);
|
||||||
|
|
||||||
|
const [minX, minY, maxX, maxY] = getMinMaxXYFromCurvePathOps(ops);
|
||||||
|
|
||||||
|
coords = [
|
||||||
minX + element.x,
|
minX + element.x,
|
||||||
minY + element.y,
|
minY + element.y,
|
||||||
maxX + element.x,
|
maxX + element.x,
|
||||||
@@ -145,19 +199,7 @@ const getLinearElementAbsoluteCoords = (
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
const shape = getShapeForElement(element) as Drawable[];
|
return coords;
|
||||||
|
|
||||||
// first element is always the curve
|
|
||||||
const ops = getCurvePathOps(shape[0]);
|
|
||||||
|
|
||||||
const [minX, minY, maxX, maxY] = getMinMaxXYFromCurvePathOps(ops);
|
|
||||||
|
|
||||||
return [
|
|
||||||
minX + element.x,
|
|
||||||
minY + element.y,
|
|
||||||
maxX + element.x,
|
|
||||||
maxY + element.y,
|
|
||||||
];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getArrowheadPoints = (
|
export const getArrowheadPoints = (
|
||||||
@@ -218,20 +260,34 @@ export const getArrowheadPoints = (
|
|||||||
dot: 15,
|
dot: 15,
|
||||||
}[arrowhead]; // pixels (will differ for each arrowhead)
|
}[arrowhead]; // pixels (will differ for each arrowhead)
|
||||||
|
|
||||||
const length = element.points.reduce((total, [cx, cy], idx, points) => {
|
let length = 0;
|
||||||
const [px, py] = idx > 0 ? points[idx - 1] : [0, 0];
|
|
||||||
return total + Math.hypot(cx - px, cy - py);
|
if (arrowhead === "arrow") {
|
||||||
}, 0);
|
// Length for -> arrows is based on the length of the last section
|
||||||
|
const [cx, cy] = element.points[element.points.length - 1];
|
||||||
|
const [px, py] =
|
||||||
|
element.points.length > 1
|
||||||
|
? element.points[element.points.length - 2]
|
||||||
|
: [0, 0];
|
||||||
|
|
||||||
|
length = Math.hypot(cx - px, cy - py);
|
||||||
|
} else {
|
||||||
|
// Length for other arrowhead types is based on the total length of the line
|
||||||
|
for (let i = 0; i < element.points.length; i++) {
|
||||||
|
const [px, py] = element.points[i - 1] || [0, 0];
|
||||||
|
const [cx, cy] = element.points[i];
|
||||||
|
length += Math.hypot(cx - px, cy - py);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Scale down the arrowhead until we hit a certain size so that it doesn't look weird.
|
// Scale down the arrowhead until we hit a certain size so that it doesn't look weird.
|
||||||
// This value is selected by minimizing a minimum size with the whole length of the
|
// This value is selected by minimizing a minimum size with the last segment of the arrowhead
|
||||||
// arrowhead instead of last segment of the arrowhead.
|
|
||||||
const minSize = Math.min(size, length / 2);
|
const minSize = Math.min(size, length / 2);
|
||||||
const xs = x2 - nx * minSize;
|
const xs = x2 - nx * minSize;
|
||||||
const ys = y2 - ny * minSize;
|
const ys = y2 - ny * minSize;
|
||||||
|
|
||||||
if (arrowhead === "dot") {
|
if (arrowhead === "dot") {
|
||||||
const r = Math.hypot(ys - y2, xs - x2);
|
const r = Math.hypot(ys - y2, xs - x2) + element.strokeWidth;
|
||||||
return [x2, y2, r];
|
return [x2, y2, r];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -277,16 +333,31 @@ const getLinearElementRotatedBounds = (
|
|||||||
return getMinMaxXYFromCurvePathOps(ops, transformXY);
|
return getMinMaxXYFromCurvePathOps(ops, transformXY);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// We could cache this stuff
|
||||||
export const getElementBounds = (
|
export const getElementBounds = (
|
||||||
element: ExcalidrawElement,
|
element: ExcalidrawElement,
|
||||||
): [number, number, number, number] => {
|
): [number, number, number, number] => {
|
||||||
|
let bounds: [number, number, number, number];
|
||||||
|
|
||||||
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
|
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
|
||||||
const cx = (x1 + x2) / 2;
|
const cx = (x1 + x2) / 2;
|
||||||
const cy = (y1 + y2) / 2;
|
const cy = (y1 + y2) / 2;
|
||||||
if (isLinearElement(element)) {
|
if (isFreeDrawElement(element)) {
|
||||||
return getLinearElementRotatedBounds(element, cx, cy);
|
const [minX, minY, maxX, maxY] = getBoundsFromPoints(
|
||||||
}
|
element.points.map(([x, y]) =>
|
||||||
if (element.type === "diamond") {
|
rotate(x, y, cx - element.x, cy - element.y, element.angle),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return [
|
||||||
|
minX + element.x,
|
||||||
|
minY + element.y,
|
||||||
|
maxX + element.x,
|
||||||
|
maxY + element.y,
|
||||||
|
];
|
||||||
|
} else if (isLinearElement(element)) {
|
||||||
|
bounds = getLinearElementRotatedBounds(element, cx, cy);
|
||||||
|
} else if (element.type === "diamond") {
|
||||||
const [x11, y11] = rotate(cx, y1, cx, cy, element.angle);
|
const [x11, y11] = rotate(cx, y1, cx, cy, element.angle);
|
||||||
const [x12, y12] = rotate(cx, y2, cx, cy, element.angle);
|
const [x12, y12] = rotate(cx, y2, cx, cy, element.angle);
|
||||||
const [x22, y22] = rotate(x1, cy, cx, cy, element.angle);
|
const [x22, y22] = rotate(x1, cy, cx, cy, element.angle);
|
||||||
@@ -295,26 +366,28 @@ export const getElementBounds = (
|
|||||||
const minY = Math.min(y11, y12, y22, y21);
|
const minY = Math.min(y11, y12, y22, y21);
|
||||||
const maxX = Math.max(x11, x12, x22, x21);
|
const maxX = Math.max(x11, x12, x22, x21);
|
||||||
const maxY = Math.max(y11, y12, y22, y21);
|
const maxY = Math.max(y11, y12, y22, y21);
|
||||||
return [minX, minY, maxX, maxY];
|
bounds = [minX, minY, maxX, maxY];
|
||||||
}
|
} else if (element.type === "ellipse") {
|
||||||
if (element.type === "ellipse") {
|
|
||||||
const w = (x2 - x1) / 2;
|
const w = (x2 - x1) / 2;
|
||||||
const h = (y2 - y1) / 2;
|
const h = (y2 - y1) / 2;
|
||||||
const cos = Math.cos(element.angle);
|
const cos = Math.cos(element.angle);
|
||||||
const sin = Math.sin(element.angle);
|
const sin = Math.sin(element.angle);
|
||||||
const ww = Math.hypot(w * cos, h * sin);
|
const ww = Math.hypot(w * cos, h * sin);
|
||||||
const hh = Math.hypot(h * cos, w * sin);
|
const hh = Math.hypot(h * cos, w * sin);
|
||||||
return [cx - ww, cy - hh, cx + ww, cy + hh];
|
bounds = [cx - ww, cy - hh, cx + ww, cy + hh];
|
||||||
|
} else {
|
||||||
|
const [x11, y11] = rotate(x1, y1, cx, cy, element.angle);
|
||||||
|
const [x12, y12] = rotate(x1, y2, cx, cy, element.angle);
|
||||||
|
const [x22, y22] = rotate(x2, y2, cx, cy, element.angle);
|
||||||
|
const [x21, y21] = rotate(x2, y1, cx, cy, element.angle);
|
||||||
|
const minX = Math.min(x11, x12, x22, x21);
|
||||||
|
const minY = Math.min(y11, y12, y22, y21);
|
||||||
|
const maxX = Math.max(x11, x12, x22, x21);
|
||||||
|
const maxY = Math.max(y11, y12, y22, y21);
|
||||||
|
bounds = [minX, minY, maxX, maxY];
|
||||||
}
|
}
|
||||||
const [x11, y11] = rotate(x1, y1, cx, cy, element.angle);
|
|
||||||
const [x12, y12] = rotate(x1, y2, cx, cy, element.angle);
|
return bounds;
|
||||||
const [x22, y22] = rotate(x2, y2, cx, cy, element.angle);
|
|
||||||
const [x21, y21] = rotate(x2, y1, cx, cy, element.angle);
|
|
||||||
const minX = Math.min(x11, x12, x22, x21);
|
|
||||||
const minY = Math.min(y11, y12, y22, y21);
|
|
||||||
const maxX = Math.max(x11, x12, x22, x21);
|
|
||||||
const maxY = Math.max(y11, y12, y22, y21);
|
|
||||||
return [minX, minY, maxX, maxY];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getCommonBounds = (
|
export const getCommonBounds = (
|
||||||
@@ -345,7 +418,7 @@ export const getResizedElementAbsoluteCoords = (
|
|||||||
nextWidth: number,
|
nextWidth: number,
|
||||||
nextHeight: number,
|
nextHeight: number,
|
||||||
): [number, number, number, number] => {
|
): [number, number, number, number] => {
|
||||||
if (!isLinearElement(element)) {
|
if (!(isLinearElement(element) || isFreeDrawElement(element))) {
|
||||||
return [
|
return [
|
||||||
element.x,
|
element.x,
|
||||||
element.y,
|
element.y,
|
||||||
@@ -360,16 +433,29 @@ export const getResizedElementAbsoluteCoords = (
|
|||||||
rescalePoints(1, nextHeight, element.points),
|
rescalePoints(1, nextHeight, element.points),
|
||||||
);
|
);
|
||||||
|
|
||||||
const gen = rough.generator();
|
let bounds: [number, number, number, number];
|
||||||
const curve =
|
|
||||||
element.strokeSharpness === "sharp"
|
if (isFreeDrawElement(element)) {
|
||||||
? gen.linearPath(
|
// Free Draw
|
||||||
points as [number, number][],
|
bounds = getBoundsFromPoints(points);
|
||||||
generateRoughOptions(element),
|
} else {
|
||||||
)
|
// Line
|
||||||
: gen.curve(points as [number, number][], generateRoughOptions(element));
|
const gen = rough.generator();
|
||||||
const ops = getCurvePathOps(curve);
|
const curve =
|
||||||
const [minX, minY, maxX, maxY] = getMinMaxXYFromCurvePathOps(ops);
|
element.strokeSharpness === "sharp"
|
||||||
|
? gen.linearPath(
|
||||||
|
points as [number, number][],
|
||||||
|
generateRoughOptions(element),
|
||||||
|
)
|
||||||
|
: gen.curve(
|
||||||
|
points as [number, number][],
|
||||||
|
generateRoughOptions(element),
|
||||||
|
);
|
||||||
|
const ops = getCurvePathOps(curve);
|
||||||
|
bounds = getMinMaxXYFromCurvePathOps(ops);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [minX, minY, maxX, maxY] = bounds;
|
||||||
return [
|
return [
|
||||||
minX + element.x,
|
minX + element.x,
|
||||||
minY + element.y,
|
minY + element.y,
|
||||||
|
|||||||
@@ -4,7 +4,13 @@ import * as GADirection from "../gadirections";
|
|||||||
import * as GALine from "../galines";
|
import * as GALine from "../galines";
|
||||||
import * as GATransform from "../gatransforms";
|
import * as GATransform from "../gatransforms";
|
||||||
|
|
||||||
import { isPathALoop, isPointInPolygon, rotate } from "../math";
|
import {
|
||||||
|
distance2d,
|
||||||
|
rotatePoint,
|
||||||
|
isPathALoop,
|
||||||
|
isPointInPolygon,
|
||||||
|
rotate,
|
||||||
|
} from "../math";
|
||||||
import { pointsOnBezierCurves } from "points-on-curve";
|
import { pointsOnBezierCurves } from "points-on-curve";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -16,6 +22,7 @@ import {
|
|||||||
ExcalidrawTextElement,
|
ExcalidrawTextElement,
|
||||||
ExcalidrawEllipseElement,
|
ExcalidrawEllipseElement,
|
||||||
NonDeleted,
|
NonDeleted,
|
||||||
|
ExcalidrawFreeDrawElement,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
import { getElementAbsoluteCoords, getCurvePathOps, Bounds } from "./bounds";
|
import { getElementAbsoluteCoords, getCurvePathOps, Bounds } from "./bounds";
|
||||||
@@ -30,10 +37,17 @@ const isElementDraggableFromInside = (
|
|||||||
if (element.type === "arrow") {
|
if (element.type === "arrow") {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (element.type === "freedraw") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
const isDraggableFromInside = element.backgroundColor !== "transparent";
|
const isDraggableFromInside = element.backgroundColor !== "transparent";
|
||||||
if (element.type === "line" || element.type === "draw") {
|
|
||||||
|
if (element.type === "line") {
|
||||||
return isDraggableFromInside && isPathALoop(element.points);
|
return isDraggableFromInside && isPathALoop(element.points);
|
||||||
}
|
}
|
||||||
|
|
||||||
return isDraggableFromInside;
|
return isDraggableFromInside;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -81,6 +95,7 @@ const isHittingElementNotConsideringBoundingBox = (
|
|||||||
: isElementDraggableFromInside(element)
|
: isElementDraggableFromInside(element)
|
||||||
? isInsideCheck
|
? isInsideCheck
|
||||||
: isNearCheck;
|
: isNearCheck;
|
||||||
|
|
||||||
return hitTestPointAgainstElement({ element, point, threshold, check });
|
return hitTestPointAgainstElement({ element, point, threshold, check });
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -151,9 +166,20 @@ const hitTestPointAgainstElement = (args: HitTestArgs): boolean => {
|
|||||||
case "ellipse":
|
case "ellipse":
|
||||||
const distance = distanceToBindableElement(args.element, args.point);
|
const distance = distanceToBindableElement(args.element, args.point);
|
||||||
return args.check(distance, args.threshold);
|
return args.check(distance, args.threshold);
|
||||||
|
case "freedraw": {
|
||||||
|
if (
|
||||||
|
!args.check(
|
||||||
|
distanceToRectangle(args.element, args.point),
|
||||||
|
args.threshold,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return hitTestFreeDrawElement(args.element, args.point, args.threshold);
|
||||||
|
}
|
||||||
case "arrow":
|
case "arrow":
|
||||||
case "line":
|
case "line":
|
||||||
case "draw":
|
|
||||||
return hitTestLinear(args);
|
return hitTestLinear(args);
|
||||||
case "selection":
|
case "selection":
|
||||||
console.warn(
|
console.warn(
|
||||||
@@ -195,7 +221,10 @@ const isOutsideCheck = (distance: number, threshold: number): boolean => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const distanceToRectangle = (
|
const distanceToRectangle = (
|
||||||
element: ExcalidrawRectangleElement | ExcalidrawTextElement,
|
element:
|
||||||
|
| ExcalidrawRectangleElement
|
||||||
|
| ExcalidrawTextElement
|
||||||
|
| ExcalidrawFreeDrawElement,
|
||||||
point: Point,
|
point: Point,
|
||||||
): number => {
|
): number => {
|
||||||
const [, pointRel, hwidth, hheight] = pointRelativeToElement(element, point);
|
const [, pointRel, hwidth, hheight] = pointRelativeToElement(element, point);
|
||||||
@@ -267,6 +296,71 @@ const ellipseParamsForTest = (
|
|||||||
return [pointRel, tangent];
|
return [pointRel, tangent];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const hitTestFreeDrawElement = (
|
||||||
|
element: ExcalidrawFreeDrawElement,
|
||||||
|
point: Point,
|
||||||
|
threshold: number,
|
||||||
|
): boolean => {
|
||||||
|
// Check point-distance-to-line-segment for every segment in the
|
||||||
|
// element's points (its input points, not its outline points).
|
||||||
|
// This is... okay? It's plenty fast, but the GA library may
|
||||||
|
// have a faster option.
|
||||||
|
|
||||||
|
let x: number;
|
||||||
|
let y: number;
|
||||||
|
|
||||||
|
if (element.angle === 0) {
|
||||||
|
x = point[0] - element.x;
|
||||||
|
y = point[1] - element.y;
|
||||||
|
} else {
|
||||||
|
// Counter-rotate the point around center before testing
|
||||||
|
const [minX, minY, maxX, maxY] = getElementAbsoluteCoords(element);
|
||||||
|
const rotatedPoint = rotatePoint(
|
||||||
|
point,
|
||||||
|
[minX + (maxX - minX) / 2, minY + (maxY - minY) / 2],
|
||||||
|
-element.angle,
|
||||||
|
);
|
||||||
|
x = rotatedPoint[0] - element.x;
|
||||||
|
y = rotatedPoint[1] - element.y;
|
||||||
|
}
|
||||||
|
|
||||||
|
let [A, B] = element.points;
|
||||||
|
let P: readonly [number, number];
|
||||||
|
|
||||||
|
// For freedraw dots
|
||||||
|
if (element.points.length === 2) {
|
||||||
|
return (
|
||||||
|
distance2d(A[0], A[1], x, y) < threshold ||
|
||||||
|
distance2d(B[0], B[1], x, y) < threshold
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// For freedraw lines
|
||||||
|
for (let i = 1; i < element.points.length - 1; i++) {
|
||||||
|
const delta = [B[0] - A[0], B[1] - A[1]];
|
||||||
|
const length = Math.hypot(delta[1], delta[0]);
|
||||||
|
|
||||||
|
const U = [delta[0] / length, delta[1] / length];
|
||||||
|
const C = [x - A[0], y - A[1]];
|
||||||
|
const d = (C[0] * U[0] + C[1] * U[1]) / Math.hypot(U[1], U[0]);
|
||||||
|
P = [A[0] + U[0] * d, A[1] + U[1] * d];
|
||||||
|
|
||||||
|
const da = distance2d(P[0], P[1], A[0], A[1]);
|
||||||
|
const db = distance2d(P[0], P[1], B[0], B[1]);
|
||||||
|
|
||||||
|
P = db < da && da > length ? B : da < db && db > length ? A : P;
|
||||||
|
|
||||||
|
if (Math.hypot(y - P[1], x - P[0]) < threshold) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
A = B;
|
||||||
|
B = element.points[i + 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
const hitTestLinear = (args: HitTestArgs): boolean => {
|
const hitTestLinear = (args: HitTestArgs): boolean => {
|
||||||
const { element, threshold } = args;
|
const { element, threshold } = args;
|
||||||
if (!getShapeForElement(element)) {
|
if (!getShapeForElement(element)) {
|
||||||
|
|||||||
@@ -58,13 +58,6 @@ export {
|
|||||||
} from "./sizeHelpers";
|
} from "./sizeHelpers";
|
||||||
export { showSelectedShapeActions } from "./showSelectedShapeActions";
|
export { showSelectedShapeActions } from "./showSelectedShapeActions";
|
||||||
|
|
||||||
export const getSyncableElements = (
|
|
||||||
elements: readonly ExcalidrawElement[], // There are places in Excalidraw where synthetic invisibly small elements are added and removed.
|
|
||||||
) =>
|
|
||||||
// It's probably best to keep those local otherwise there might be a race condition that
|
|
||||||
// gets the app into an invalid state. I've never seen it happen but I'm worried about it :)
|
|
||||||
elements.filter((el) => el.isDeleted || !isInvisiblySmallElement(el));
|
|
||||||
|
|
||||||
export const getElementMap = (elements: readonly ExcalidrawElement[]) =>
|
export const getElementMap = (elements: readonly ExcalidrawElement[]) =>
|
||||||
elements.reduce(
|
elements.reduce(
|
||||||
(acc: { [key: string]: ExcalidrawElement }, element: ExcalidrawElement) => {
|
(acc: { [key: string]: ExcalidrawElement }, element: ExcalidrawElement) => {
|
||||||
@@ -77,6 +70,11 @@ export const getElementMap = (elements: readonly ExcalidrawElement[]) =>
|
|||||||
export const getSceneVersion = (elements: readonly ExcalidrawElement[]) =>
|
export const getSceneVersion = (elements: readonly ExcalidrawElement[]) =>
|
||||||
elements.reduce((acc, el) => acc + el.version, 0);
|
elements.reduce((acc, el) => acc + el.version, 0);
|
||||||
|
|
||||||
|
export const getVisibleElements = (elements: readonly ExcalidrawElement[]) =>
|
||||||
|
elements.filter(
|
||||||
|
(el) => !el.isDeleted && !isInvisiblySmallElement(el),
|
||||||
|
) as readonly NonDeletedExcalidrawElement[];
|
||||||
|
|
||||||
export const getNonDeletedElements = (elements: readonly ExcalidrawElement[]) =>
|
export const getNonDeletedElements = (elements: readonly ExcalidrawElement[]) =>
|
||||||
elements.filter(
|
elements.filter(
|
||||||
(element) => !element.isDeleted,
|
(element) => !element.isDeleted,
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { getElementAbsoluteCoords } from ".";
|
|||||||
import { getElementPointsCoords } from "./bounds";
|
import { getElementPointsCoords } from "./bounds";
|
||||||
import { Point, AppState } from "../types";
|
import { Point, AppState } from "../types";
|
||||||
import { mutateElement } from "./mutateElement";
|
import { mutateElement } from "./mutateElement";
|
||||||
import { SceneHistory } from "../history";
|
import History from "../history";
|
||||||
|
|
||||||
import Scene from "../scene/Scene";
|
import Scene from "../scene/Scene";
|
||||||
import {
|
import {
|
||||||
@@ -167,7 +167,7 @@ export class LinearElementEditor {
|
|||||||
event: React.PointerEvent<HTMLCanvasElement>,
|
event: React.PointerEvent<HTMLCanvasElement>,
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
setState: React.Component<any, AppState>["setState"],
|
setState: React.Component<any, AppState>["setState"],
|
||||||
history: SceneHistory,
|
history: History,
|
||||||
scenePointer: { x: number; y: number },
|
scenePointer: { x: number; y: number },
|
||||||
): {
|
): {
|
||||||
didAddPoint: boolean;
|
didAddPoint: boolean;
|
||||||
@@ -415,26 +415,31 @@ export class LinearElementEditor {
|
|||||||
return [rotatedX - element.x, rotatedY - element.y];
|
return [rotatedX - element.x, rotatedY - element.y];
|
||||||
}
|
}
|
||||||
|
|
||||||
// element-mutating methods
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Normalizes line points so that the start point is at [0,0]. This is
|
* Normalizes line points so that the start point is at [0,0]. This is
|
||||||
* expected in various parts of the codebase.
|
* expected in various parts of the codebase. Also returns new x/y to account
|
||||||
|
* for the potential normalization.
|
||||||
*/
|
*/
|
||||||
static normalizePoints(element: NonDeleted<ExcalidrawLinearElement>) {
|
static getNormalizedPoints(element: ExcalidrawLinearElement) {
|
||||||
const { points } = element;
|
const { points } = element;
|
||||||
|
|
||||||
const offsetX = points[0][0];
|
const offsetX = points[0][0];
|
||||||
const offsetY = points[0][1];
|
const offsetY = points[0][1];
|
||||||
|
|
||||||
mutateElement(element, {
|
return {
|
||||||
points: points.map((point, _idx) => {
|
points: points.map((point, _idx) => {
|
||||||
return [point[0] - offsetX, point[1] - offsetY] as const;
|
return [point[0] - offsetX, point[1] - offsetY] as const;
|
||||||
}),
|
}),
|
||||||
x: element.x + offsetX,
|
x: element.x + offsetX,
|
||||||
y: element.y + offsetY,
|
y: element.y + offsetY,
|
||||||
});
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// element-mutating methods
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
static normalizePoints(element: NonDeleted<ExcalidrawLinearElement>) {
|
||||||
|
mutateElement(element, LinearElementEditor.getNormalizedPoints(element));
|
||||||
}
|
}
|
||||||
|
|
||||||
static movePointByOffset(
|
static movePointByOffset(
|
||||||
|
|||||||
@@ -114,3 +114,14 @@ export const newElementWith = <TElement extends ExcalidrawElement>(
|
|||||||
versionNonce: randomInteger(),
|
versionNonce: randomInteger(),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mutates element and updates `version` & `versionNonce`.
|
||||||
|
*
|
||||||
|
* NOTE: does not trigger re-render.
|
||||||
|
*/
|
||||||
|
export const bumpVersion = (element: Mutable<ExcalidrawElement>) => {
|
||||||
|
element.version = element.version + 1;
|
||||||
|
element.versionNonce = randomInteger();
|
||||||
|
return element;
|
||||||
|
};
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
GroupId,
|
GroupId,
|
||||||
VerticalAlign,
|
VerticalAlign,
|
||||||
Arrowhead,
|
Arrowhead,
|
||||||
|
ExcalidrawFreeDrawElement,
|
||||||
} 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";
|
||||||
@@ -212,6 +213,22 @@ export const updateTextElement = (
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const newFreeDrawElement = (
|
||||||
|
opts: {
|
||||||
|
type: "freedraw";
|
||||||
|
points?: ExcalidrawFreeDrawElement["points"];
|
||||||
|
simulatePressure: boolean;
|
||||||
|
} & ElementConstructorOpts,
|
||||||
|
): NonDeleted<ExcalidrawFreeDrawElement> => {
|
||||||
|
return {
|
||||||
|
..._newElementBase<ExcalidrawFreeDrawElement>(opts.type, opts),
|
||||||
|
points: opts.points || [],
|
||||||
|
pressures: [],
|
||||||
|
simulatePressure: opts.simulatePressure,
|
||||||
|
lastCommittedPoint: null,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export const newLinearElement = (
|
export const newLinearElement = (
|
||||||
opts: {
|
opts: {
|
||||||
type: ExcalidrawLinearElement["type"];
|
type: ExcalidrawLinearElement["type"];
|
||||||
@@ -290,7 +307,19 @@ export const duplicateElement = <TElement extends Mutable<ExcalidrawElement>>(
|
|||||||
overrides?: Partial<TElement>,
|
overrides?: Partial<TElement>,
|
||||||
): TElement => {
|
): TElement => {
|
||||||
let copy: TElement = deepCopyElement(element);
|
let copy: TElement = deepCopyElement(element);
|
||||||
copy.id = process.env.NODE_ENV === "test" ? `${copy.id}_copy` : randomId();
|
if (process.env.NODE_ENV === "test") {
|
||||||
|
copy.id = `${copy.id}_copy`;
|
||||||
|
// `window.h` may not be defined in some unit tests
|
||||||
|
if (
|
||||||
|
window.h?.app
|
||||||
|
?.getSceneElementsIncludingDeleted()
|
||||||
|
.find((el) => el.id === copy.id)
|
||||||
|
) {
|
||||||
|
copy.id += "_copy";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
copy.id = randomId();
|
||||||
|
}
|
||||||
copy.seed = randomInteger();
|
copy.seed = randomInteger();
|
||||||
copy.groupIds = getNewGroupIdsForDuplication(
|
copy.groupIds = getNewGroupIdsForDuplication(
|
||||||
copy.groupIds,
|
copy.groupIds,
|
||||||
|
|||||||
@@ -18,7 +18,11 @@ import {
|
|||||||
getCommonBounds,
|
getCommonBounds,
|
||||||
getResizedElementAbsoluteCoords,
|
getResizedElementAbsoluteCoords,
|
||||||
} from "./bounds";
|
} from "./bounds";
|
||||||
import { isLinearElement, isTextElement } from "./typeChecks";
|
import {
|
||||||
|
isFreeDrawElement,
|
||||||
|
isLinearElement,
|
||||||
|
isTextElement,
|
||||||
|
} from "./typeChecks";
|
||||||
import { mutateElement } from "./mutateElement";
|
import { mutateElement } from "./mutateElement";
|
||||||
import { getPerfectElementSize } from "./sizeHelpers";
|
import { getPerfectElementSize } from "./sizeHelpers";
|
||||||
import { measureText, getFontString } from "../utils";
|
import { measureText, getFontString } from "../utils";
|
||||||
@@ -244,7 +248,7 @@ const rescalePointsInElement = (
|
|||||||
width: number,
|
width: number,
|
||||||
height: number,
|
height: number,
|
||||||
) =>
|
) =>
|
||||||
isLinearElement(element)
|
isLinearElement(element) || isFreeDrawElement(element)
|
||||||
? {
|
? {
|
||||||
points: rescalePoints(
|
points: rescalePoints(
|
||||||
0,
|
0,
|
||||||
@@ -404,7 +408,7 @@ export const resizeSingleElement = (
|
|||||||
-stateAtResizeStart.angle,
|
-stateAtResizeStart.angle,
|
||||||
);
|
);
|
||||||
|
|
||||||
//Get bounds corners rendered on screen
|
// Get bounds corners rendered on screen
|
||||||
const [esx1, esy1, esx2, esy2] = getResizedElementAbsoluteCoords(
|
const [esx1, esy1, esx2, esy2] = getResizedElementAbsoluteCoords(
|
||||||
element,
|
element,
|
||||||
element.width,
|
element.width,
|
||||||
@@ -644,11 +648,14 @@ const resizeMultipleElements = (
|
|||||||
font = { fontSize: nextFont.size, baseline: nextFont.baseline };
|
font = { fontSize: nextFont.size, baseline: nextFont.baseline };
|
||||||
}
|
}
|
||||||
const origCoords = getElementAbsoluteCoords(element);
|
const origCoords = getElementAbsoluteCoords(element);
|
||||||
|
|
||||||
const rescaledPoints = rescalePointsInElement(element, width, height);
|
const rescaledPoints = rescalePointsInElement(element, width, height);
|
||||||
|
|
||||||
updateBoundElements(element, {
|
updateBoundElements(element, {
|
||||||
newSize: { width, height },
|
newSize: { width, height },
|
||||||
simultaneouslyUpdated: elements,
|
simultaneouslyUpdated: elements,
|
||||||
});
|
});
|
||||||
|
|
||||||
const finalCoords = getResizedElementAbsoluteCoords(
|
const finalCoords = getResizedElementAbsoluteCoords(
|
||||||
{
|
{
|
||||||
...element,
|
...element,
|
||||||
@@ -657,6 +664,7 @@ const resizeMultipleElements = (
|
|||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
);
|
);
|
||||||
|
|
||||||
const { x, y } = getNextXY(element, origCoords, finalCoords);
|
const { x, y } = getNextXY(element, origCoords, finalCoords);
|
||||||
return [...prev, { width, height, x, y, ...rescaledPoints, ...font }];
|
return [...prev, { width, height, x, y, ...rescaledPoints, ...font }];
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { ExcalidrawElement } from "./types";
|
import { ExcalidrawElement } from "./types";
|
||||||
import { mutateElement } from "./mutateElement";
|
import { mutateElement } from "./mutateElement";
|
||||||
import { isLinearElement } from "./typeChecks";
|
import { isFreeDrawElement, isLinearElement } from "./typeChecks";
|
||||||
import { SHIFT_LOCKING_ANGLE } from "../constants";
|
import { SHIFT_LOCKING_ANGLE } from "../constants";
|
||||||
|
|
||||||
export const isInvisiblySmallElement = (
|
export const isInvisiblySmallElement = (
|
||||||
element: ExcalidrawElement,
|
element: ExcalidrawElement,
|
||||||
): boolean => {
|
): boolean => {
|
||||||
if (isLinearElement(element)) {
|
if (isLinearElement(element) || isFreeDrawElement(element)) {
|
||||||
return element.points.length < 2;
|
return element.points.length < 2;
|
||||||
}
|
}
|
||||||
return element.width === 0 && element.height === 0;
|
return element.width === 0 && element.height === 0;
|
||||||
@@ -26,7 +26,7 @@ export const getPerfectElementSize = (
|
|||||||
if (
|
if (
|
||||||
elementType === "line" ||
|
elementType === "line" ||
|
||||||
elementType === "arrow" ||
|
elementType === "arrow" ||
|
||||||
elementType === "draw"
|
elementType === "freedraw"
|
||||||
) {
|
) {
|
||||||
const lockedAngle =
|
const lockedAngle =
|
||||||
Math.round(Math.atan(absHeight / absWidth) / SHIFT_LOCKING_ANGLE) *
|
Math.round(Math.atan(absHeight / absWidth) / SHIFT_LOCKING_ANGLE) *
|
||||||
|
|||||||
@@ -0,0 +1,169 @@
|
|||||||
|
import ReactDOM from "react-dom";
|
||||||
|
import ExcalidrawApp from "../excalidraw-app";
|
||||||
|
import { render } from "../tests/test-utils";
|
||||||
|
import { Pointer, UI } from "../tests/helpers/ui";
|
||||||
|
import { KEYS } from "../keys";
|
||||||
|
|
||||||
|
// Unmount ReactDOM from root
|
||||||
|
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
||||||
|
|
||||||
|
const tab = " ";
|
||||||
|
|
||||||
|
describe("textWysiwyg", () => {
|
||||||
|
let textarea: HTMLTextAreaElement;
|
||||||
|
beforeEach(async () => {
|
||||||
|
await render(<ExcalidrawApp />);
|
||||||
|
|
||||||
|
const element = UI.createElement("text");
|
||||||
|
|
||||||
|
new Pointer("mouse").clickOn(element);
|
||||||
|
textarea = document.querySelector(
|
||||||
|
".excalidraw-textEditorContainer > textarea",
|
||||||
|
)!;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should add a tab at the start of the first line", () => {
|
||||||
|
const event = new KeyboardEvent("keydown", { key: KEYS.TAB });
|
||||||
|
textarea.value = "Line#1\nLine#2";
|
||||||
|
// cursor: "|Line#1\nLine#2"
|
||||||
|
textarea.selectionStart = 0;
|
||||||
|
textarea.selectionEnd = 0;
|
||||||
|
textarea.dispatchEvent(event);
|
||||||
|
|
||||||
|
expect(textarea.value).toEqual(`${tab}Line#1\nLine#2`);
|
||||||
|
// cursor: " |Line#1\nLine#2"
|
||||||
|
expect(textarea.selectionStart).toEqual(4);
|
||||||
|
expect(textarea.selectionEnd).toEqual(4);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should add a tab at the start of the second line", () => {
|
||||||
|
const event = new KeyboardEvent("keydown", { key: KEYS.TAB });
|
||||||
|
textarea.value = "Line#1\nLine#2";
|
||||||
|
// cursor: "Line#1\nLin|e#2"
|
||||||
|
textarea.selectionStart = 10;
|
||||||
|
textarea.selectionEnd = 10;
|
||||||
|
|
||||||
|
textarea.dispatchEvent(event);
|
||||||
|
|
||||||
|
expect(textarea.value).toEqual(`Line#1\n${tab}Line#2`);
|
||||||
|
|
||||||
|
// cursor: "Line#1\n Lin|e#2"
|
||||||
|
expect(textarea.selectionStart).toEqual(14);
|
||||||
|
expect(textarea.selectionEnd).toEqual(14);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should add a tab at the start of the first and second line", () => {
|
||||||
|
const event = new KeyboardEvent("keydown", { key: KEYS.TAB });
|
||||||
|
textarea.value = "Line#1\nLine#2\nLine#3";
|
||||||
|
// cursor: "Li|ne#1\nLi|ne#2\nLine#3"
|
||||||
|
textarea.selectionStart = 2;
|
||||||
|
textarea.selectionEnd = 9;
|
||||||
|
|
||||||
|
textarea.dispatchEvent(event);
|
||||||
|
|
||||||
|
expect(textarea.value).toEqual(`${tab}Line#1\n${tab}Line#2\nLine#3`);
|
||||||
|
|
||||||
|
// cursor: " Li|ne#1\n Li|ne#2\nLine#3"
|
||||||
|
expect(textarea.selectionStart).toEqual(6);
|
||||||
|
expect(textarea.selectionEnd).toEqual(17);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should remove a tab at the start of the first line", () => {
|
||||||
|
const event = new KeyboardEvent("keydown", {
|
||||||
|
key: KEYS.TAB,
|
||||||
|
shiftKey: true,
|
||||||
|
});
|
||||||
|
textarea.value = `${tab}Line#1\nLine#2`;
|
||||||
|
// cursor: "| Line#1\nLine#2"
|
||||||
|
textarea.selectionStart = 0;
|
||||||
|
textarea.selectionEnd = 0;
|
||||||
|
|
||||||
|
textarea.dispatchEvent(event);
|
||||||
|
|
||||||
|
expect(textarea.value).toEqual(`Line#1\nLine#2`);
|
||||||
|
|
||||||
|
// cursor: "|Line#1\nLine#2"
|
||||||
|
expect(textarea.selectionStart).toEqual(0);
|
||||||
|
expect(textarea.selectionEnd).toEqual(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should remove a tab at the start of the second line", () => {
|
||||||
|
const event = new KeyboardEvent("keydown", {
|
||||||
|
key: KEYS.TAB,
|
||||||
|
shiftKey: true,
|
||||||
|
});
|
||||||
|
// cursor: "Line#1\n Lin|e#2"
|
||||||
|
textarea.value = `Line#1\n${tab}Line#2`;
|
||||||
|
textarea.selectionStart = 15;
|
||||||
|
textarea.selectionEnd = 15;
|
||||||
|
|
||||||
|
textarea.dispatchEvent(event);
|
||||||
|
|
||||||
|
expect(textarea.value).toEqual(`Line#1\nLine#2`);
|
||||||
|
// cursor: "Line#1\nLin|e#2"
|
||||||
|
expect(textarea.selectionStart).toEqual(11);
|
||||||
|
expect(textarea.selectionEnd).toEqual(11);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should remove a tab at the start of the first and second line", () => {
|
||||||
|
const event = new KeyboardEvent("keydown", {
|
||||||
|
key: KEYS.TAB,
|
||||||
|
shiftKey: true,
|
||||||
|
});
|
||||||
|
// cursor: " Li|ne#1\n Li|ne#2\nLine#3"
|
||||||
|
textarea.value = `${tab}Line#1\n${tab}Line#2\nLine#3`;
|
||||||
|
textarea.selectionStart = 6;
|
||||||
|
textarea.selectionEnd = 17;
|
||||||
|
|
||||||
|
textarea.dispatchEvent(event);
|
||||||
|
|
||||||
|
expect(textarea.value).toEqual(`Line#1\nLine#2\nLine#3`);
|
||||||
|
// cursor: "Li|ne#1\nLi|ne#2\nLine#3"
|
||||||
|
expect(textarea.selectionStart).toEqual(2);
|
||||||
|
expect(textarea.selectionEnd).toEqual(9);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should remove a tab at the start of the second line and cursor stay on this line", () => {
|
||||||
|
const event = new KeyboardEvent("keydown", {
|
||||||
|
key: KEYS.TAB,
|
||||||
|
shiftKey: true,
|
||||||
|
});
|
||||||
|
// cursor: "Line#1\n | Line#2"
|
||||||
|
textarea.value = `Line#1\n${tab}Line#2`;
|
||||||
|
textarea.selectionStart = 9;
|
||||||
|
textarea.selectionEnd = 9;
|
||||||
|
textarea.dispatchEvent(event);
|
||||||
|
|
||||||
|
// cursor: "Line#1\n|Line#2"
|
||||||
|
expect(textarea.selectionStart).toEqual(7);
|
||||||
|
// expect(textarea.selectionEnd).toEqual(7);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should remove partial tabs", () => {
|
||||||
|
const event = new KeyboardEvent("keydown", {
|
||||||
|
key: KEYS.TAB,
|
||||||
|
shiftKey: true,
|
||||||
|
});
|
||||||
|
// cursor: "Line#1\n Line#|2"
|
||||||
|
textarea.value = `Line#1\n Line#2`;
|
||||||
|
textarea.selectionStart = 15;
|
||||||
|
textarea.selectionEnd = 15;
|
||||||
|
textarea.dispatchEvent(event);
|
||||||
|
|
||||||
|
expect(textarea.value).toEqual(`Line#1\nLine#2`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should remove nothing", () => {
|
||||||
|
const event = new KeyboardEvent("keydown", {
|
||||||
|
key: KEYS.TAB,
|
||||||
|
shiftKey: true,
|
||||||
|
});
|
||||||
|
// cursor: "Line#1\n Li|ne#2"
|
||||||
|
textarea.value = `Line#1\nLine#2`;
|
||||||
|
textarea.selectionStart = 9;
|
||||||
|
textarea.selectionEnd = 9;
|
||||||
|
textarea.dispatchEvent(event);
|
||||||
|
|
||||||
|
expect(textarea.value).toEqual(`Line#1\nLine#2`);
|
||||||
|
});
|
||||||
|
});
|
||||||
+120
-7
@@ -1,4 +1,4 @@
|
|||||||
import { KEYS } from "../keys";
|
import { CODES, KEYS } from "../keys";
|
||||||
import { isWritableElement, getFontString } from "../utils";
|
import { isWritableElement, getFontString } from "../utils";
|
||||||
import Scene from "../scene/Scene";
|
import Scene from "../scene/Scene";
|
||||||
import { isTextElement } from "./typeChecks";
|
import { isTextElement } from "./typeChecks";
|
||||||
@@ -43,6 +43,7 @@ export const textWysiwyg = ({
|
|||||||
getViewportCoords,
|
getViewportCoords,
|
||||||
element,
|
element,
|
||||||
canvas,
|
canvas,
|
||||||
|
excalidrawContainer,
|
||||||
}: {
|
}: {
|
||||||
id: ExcalidrawElement["id"];
|
id: ExcalidrawElement["id"];
|
||||||
appState: AppState;
|
appState: AppState;
|
||||||
@@ -51,6 +52,7 @@ export const textWysiwyg = ({
|
|||||||
getViewportCoords: (x: number, y: number) => [number, number];
|
getViewportCoords: (x: number, y: number) => [number, number];
|
||||||
element: ExcalidrawElement;
|
element: ExcalidrawElement;
|
||||||
canvas: HTMLCanvasElement | null;
|
canvas: HTMLCanvasElement | null;
|
||||||
|
excalidrawContainer: HTMLDivElement | null;
|
||||||
}) => {
|
}) => {
|
||||||
const updateWysiwygStyle = () => {
|
const updateWysiwygStyle = () => {
|
||||||
const updatedElement = Scene.getScene(element)?.getElement(id);
|
const updatedElement = Scene.getScene(element)?.getElement(id);
|
||||||
@@ -71,7 +73,7 @@ export const textWysiwyg = ({
|
|||||||
// margin-right of parent if any
|
// margin-right of parent if any
|
||||||
Number(
|
Number(
|
||||||
getComputedStyle(
|
getComputedStyle(
|
||||||
document.querySelector(".excalidraw")!.parentNode as Element,
|
excalidrawContainer?.parentNode as Element,
|
||||||
).marginRight.slice(0, -2),
|
).marginRight.slice(0, -2),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -134,6 +136,7 @@ export const textWysiwyg = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
editable.onkeydown = (event) => {
|
editable.onkeydown = (event) => {
|
||||||
|
event.stopPropagation();
|
||||||
if (event.key === KEYS.ESCAPE) {
|
if (event.key === KEYS.ESCAPE) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
submittedViaKeyboard = true;
|
submittedViaKeyboard = true;
|
||||||
@@ -145,11 +148,118 @@ export const textWysiwyg = ({
|
|||||||
}
|
}
|
||||||
submittedViaKeyboard = true;
|
submittedViaKeyboard = true;
|
||||||
handleSubmit();
|
handleSubmit();
|
||||||
} else if (event.key === KEYS.ENTER && !event.altKey) {
|
} else if (
|
||||||
event.stopPropagation();
|
event.key === KEYS.TAB ||
|
||||||
|
(event[KEYS.CTRL_OR_CMD] &&
|
||||||
|
(event.code === CODES.BRACKET_LEFT ||
|
||||||
|
event.code === CODES.BRACKET_RIGHT))
|
||||||
|
) {
|
||||||
|
event.preventDefault();
|
||||||
|
if (event.shiftKey || event.code === CODES.BRACKET_LEFT) {
|
||||||
|
outdent();
|
||||||
|
} else {
|
||||||
|
indent();
|
||||||
|
}
|
||||||
|
// We must send an input event to resize the element
|
||||||
|
editable.dispatchEvent(new Event("input"));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const TAB_SIZE = 4;
|
||||||
|
const TAB = " ".repeat(TAB_SIZE);
|
||||||
|
const RE_LEADING_TAB = new RegExp(`^ {1,${TAB_SIZE}}`);
|
||||||
|
const indent = () => {
|
||||||
|
const { selectionStart, selectionEnd } = editable;
|
||||||
|
const linesStartIndices = getSelectedLinesStartIndices();
|
||||||
|
|
||||||
|
let value = editable.value;
|
||||||
|
linesStartIndices.forEach((startIndex) => {
|
||||||
|
const startValue = value.slice(0, startIndex);
|
||||||
|
const endValue = value.slice(startIndex);
|
||||||
|
|
||||||
|
value = `${startValue}${TAB}${endValue}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
editable.value = value;
|
||||||
|
|
||||||
|
editable.selectionStart = selectionStart + TAB_SIZE;
|
||||||
|
editable.selectionEnd = selectionEnd + TAB_SIZE * linesStartIndices.length;
|
||||||
|
};
|
||||||
|
|
||||||
|
const outdent = () => {
|
||||||
|
const { selectionStart, selectionEnd } = editable;
|
||||||
|
const linesStartIndices = getSelectedLinesStartIndices();
|
||||||
|
const removedTabs: number[] = [];
|
||||||
|
|
||||||
|
let value = editable.value;
|
||||||
|
linesStartIndices.forEach((startIndex) => {
|
||||||
|
const tabMatch = value
|
||||||
|
.slice(startIndex, startIndex + TAB_SIZE)
|
||||||
|
.match(RE_LEADING_TAB);
|
||||||
|
|
||||||
|
if (tabMatch) {
|
||||||
|
const startValue = value.slice(0, startIndex);
|
||||||
|
const endValue = value.slice(startIndex + tabMatch[0].length);
|
||||||
|
|
||||||
|
// Delete a tab from the line
|
||||||
|
value = `${startValue}${endValue}`;
|
||||||
|
removedTabs.push(startIndex);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
editable.value = value;
|
||||||
|
|
||||||
|
if (removedTabs.length) {
|
||||||
|
if (selectionStart > removedTabs[removedTabs.length - 1]) {
|
||||||
|
editable.selectionStart = Math.max(
|
||||||
|
selectionStart - TAB_SIZE,
|
||||||
|
removedTabs[removedTabs.length - 1],
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// If the cursor is before the first tab removed, ex:
|
||||||
|
// Line| #1
|
||||||
|
// Line #2
|
||||||
|
// Lin|e #3
|
||||||
|
// we should reset the selectionStart to his initial value.
|
||||||
|
editable.selectionStart = selectionStart;
|
||||||
|
}
|
||||||
|
editable.selectionEnd = Math.max(
|
||||||
|
editable.selectionStart,
|
||||||
|
selectionEnd - TAB_SIZE * removedTabs.length,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns indeces of start positions of selected lines, in reverse order
|
||||||
|
*/
|
||||||
|
const getSelectedLinesStartIndices = () => {
|
||||||
|
let { selectionStart, selectionEnd, value } = editable;
|
||||||
|
|
||||||
|
// chars before selectionStart on the same line
|
||||||
|
const startOffset = value.slice(0, selectionStart).match(/[^\n]*$/)![0]
|
||||||
|
.length;
|
||||||
|
// put caret at the start of the line
|
||||||
|
selectionStart = selectionStart - startOffset;
|
||||||
|
|
||||||
|
const selected = value.slice(selectionStart, selectionEnd);
|
||||||
|
|
||||||
|
return selected
|
||||||
|
.split("\n")
|
||||||
|
.reduce(
|
||||||
|
(startIndices, line, idx, lines) =>
|
||||||
|
startIndices.concat(
|
||||||
|
idx
|
||||||
|
? // curr line index is prev line's start + prev line's length + \n
|
||||||
|
startIndices[idx - 1] + lines[idx - 1].length + 1
|
||||||
|
: // first selected line
|
||||||
|
selectionStart,
|
||||||
|
),
|
||||||
|
[] as number[],
|
||||||
|
)
|
||||||
|
.reverse();
|
||||||
|
};
|
||||||
|
|
||||||
const stopEvent = (event: Event) => {
|
const stopEvent = (event: Event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
@@ -159,11 +269,14 @@ export const textWysiwyg = ({
|
|||||||
// so that we don't need to create separate a callback for event handlers
|
// so that we don't need to create separate a callback for event handlers
|
||||||
let submittedViaKeyboard = false;
|
let submittedViaKeyboard = false;
|
||||||
const handleSubmit = () => {
|
const handleSubmit = () => {
|
||||||
|
// cleanup must be run before onSubmit otherwise when app blurs the wysiwyg
|
||||||
|
// it'd get stuck in an infinite loop of blur→onSubmit after we re-focus the
|
||||||
|
// wysiwyg on update
|
||||||
|
cleanup();
|
||||||
onSubmit({
|
onSubmit({
|
||||||
text: normalizeText(editable.value),
|
text: normalizeText(editable.value),
|
||||||
viaKeyboard: submittedViaKeyboard,
|
viaKeyboard: submittedViaKeyboard,
|
||||||
});
|
});
|
||||||
cleanup();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const cleanup = () => {
|
const cleanup = () => {
|
||||||
@@ -252,7 +365,7 @@ export const textWysiwyg = ({
|
|||||||
passive: false,
|
passive: false,
|
||||||
capture: true,
|
capture: true,
|
||||||
});
|
});
|
||||||
document
|
excalidrawContainer
|
||||||
.querySelector(".excalidraw-textEditorContainer")!
|
?.querySelector(".excalidraw-textEditorContainer")!
|
||||||
.appendChild(editable);
|
.appendChild(editable);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -225,7 +225,7 @@ export const getTransformHandles = (
|
|||||||
if (
|
if (
|
||||||
element.type === "arrow" ||
|
element.type === "arrow" ||
|
||||||
element.type === "line" ||
|
element.type === "line" ||
|
||||||
element.type === "draw"
|
element.type === "freedraw"
|
||||||
) {
|
) {
|
||||||
if (element.points.length === 2) {
|
if (element.points.length === 2) {
|
||||||
// only check the last point because starting point is always (0,0)
|
// only check the last point because starting point is always (0,0)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
ExcalidrawLinearElement,
|
ExcalidrawLinearElement,
|
||||||
ExcalidrawBindableElement,
|
ExcalidrawBindableElement,
|
||||||
ExcalidrawGenericElement,
|
ExcalidrawGenericElement,
|
||||||
|
ExcalidrawFreeDrawElement,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
export const isGenericElement = (
|
export const isGenericElement = (
|
||||||
@@ -24,6 +25,18 @@ export const isTextElement = (
|
|||||||
return element != null && element.type === "text";
|
return element != null && element.type === "text";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const isFreeDrawElement = (
|
||||||
|
element?: ExcalidrawElement | null,
|
||||||
|
): element is ExcalidrawFreeDrawElement => {
|
||||||
|
return element != null && isFreeDrawElementType(element.type);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isFreeDrawElementType = (
|
||||||
|
elementType: ExcalidrawElement["type"],
|
||||||
|
): boolean => {
|
||||||
|
return elementType === "freedraw";
|
||||||
|
};
|
||||||
|
|
||||||
export const isLinearElement = (
|
export const isLinearElement = (
|
||||||
element?: ExcalidrawElement | null,
|
element?: ExcalidrawElement | null,
|
||||||
): element is ExcalidrawLinearElement => {
|
): element is ExcalidrawLinearElement => {
|
||||||
@@ -34,7 +47,7 @@ export const isLinearElementType = (
|
|||||||
elementType: ExcalidrawElement["type"],
|
elementType: ExcalidrawElement["type"],
|
||||||
): boolean => {
|
): boolean => {
|
||||||
return (
|
return (
|
||||||
elementType === "arrow" || elementType === "line" || elementType === "draw"
|
elementType === "arrow" || elementType === "line" // || elementType === "freedraw"
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -69,7 +82,7 @@ export const isExcalidrawElement = (element: any): boolean => {
|
|||||||
element?.type === "rectangle" ||
|
element?.type === "rectangle" ||
|
||||||
element?.type === "ellipse" ||
|
element?.type === "ellipse" ||
|
||||||
element?.type === "arrow" ||
|
element?.type === "arrow" ||
|
||||||
element?.type === "draw" ||
|
element?.type === "freedraw" ||
|
||||||
element?.type === "line"
|
element?.type === "line"
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
+12
-2
@@ -78,7 +78,8 @@ export type ExcalidrawGenericElement =
|
|||||||
export type ExcalidrawElement =
|
export type ExcalidrawElement =
|
||||||
| ExcalidrawGenericElement
|
| ExcalidrawGenericElement
|
||||||
| ExcalidrawTextElement
|
| ExcalidrawTextElement
|
||||||
| ExcalidrawLinearElement;
|
| ExcalidrawLinearElement
|
||||||
|
| ExcalidrawFreeDrawElement;
|
||||||
|
|
||||||
export type NonDeleted<TElement extends ExcalidrawElement> = TElement & {
|
export type NonDeleted<TElement extends ExcalidrawElement> = TElement & {
|
||||||
isDeleted: false;
|
isDeleted: false;
|
||||||
@@ -113,7 +114,7 @@ export type Arrowhead = "arrow" | "bar" | "dot";
|
|||||||
|
|
||||||
export type ExcalidrawLinearElement = _ExcalidrawElementBase &
|
export type ExcalidrawLinearElement = _ExcalidrawElementBase &
|
||||||
Readonly<{
|
Readonly<{
|
||||||
type: "line" | "draw" | "arrow";
|
type: "line" | "arrow";
|
||||||
points: readonly Point[];
|
points: readonly Point[];
|
||||||
lastCommittedPoint: Point | null;
|
lastCommittedPoint: Point | null;
|
||||||
startBinding: PointBinding | null;
|
startBinding: PointBinding | null;
|
||||||
@@ -121,3 +122,12 @@ export type ExcalidrawLinearElement = _ExcalidrawElementBase &
|
|||||||
startArrowhead: Arrowhead | null;
|
startArrowhead: Arrowhead | null;
|
||||||
endArrowhead: Arrowhead | null;
|
endArrowhead: Arrowhead | null;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
export type ExcalidrawFreeDrawElement = _ExcalidrawElementBase &
|
||||||
|
Readonly<{
|
||||||
|
type: "freedraw";
|
||||||
|
points: readonly Point[];
|
||||||
|
pressures: readonly number[];
|
||||||
|
simulatePressure: boolean;
|
||||||
|
lastCommittedPoint: Point | null;
|
||||||
|
}>;
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import { ExcalidrawElement } from "../../element/types";
|
|||||||
import {
|
import {
|
||||||
getElementMap,
|
getElementMap,
|
||||||
getSceneVersion,
|
getSceneVersion,
|
||||||
getSyncableElements,
|
|
||||||
} from "../../packages/excalidraw/index";
|
} from "../../packages/excalidraw/index";
|
||||||
import { Collaborator, Gesture } from "../../types";
|
import { Collaborator, Gesture } from "../../types";
|
||||||
import { resolvablePromise, withBatchedUpdates } from "../../utils";
|
import { resolvablePromise, withBatchedUpdates } from "../../utils";
|
||||||
@@ -41,6 +40,7 @@ import { t } from "../../i18n";
|
|||||||
import { UserIdleState } from "../../types";
|
import { UserIdleState } from "../../types";
|
||||||
import { IDLE_THRESHOLD, ACTIVE_THRESHOLD } from "../../constants";
|
import { IDLE_THRESHOLD, ACTIVE_THRESHOLD } from "../../constants";
|
||||||
import { trackEvent } from "../../analytics";
|
import { trackEvent } from "../../analytics";
|
||||||
|
import { isInvisiblySmallElement } from "../../element";
|
||||||
|
|
||||||
interface CollabState {
|
interface CollabState {
|
||||||
modalIsShown: boolean;
|
modalIsShown: boolean;
|
||||||
@@ -146,7 +146,7 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
private beforeUnload = withBatchedUpdates((event: BeforeUnloadEvent) => {
|
private beforeUnload = withBatchedUpdates((event: BeforeUnloadEvent) => {
|
||||||
const syncableElements = getSyncableElements(
|
const syncableElements = this.getSyncableElements(
|
||||||
this.getSceneElementsIncludingDeleted(),
|
this.getSceneElementsIncludingDeleted(),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -177,7 +177,7 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
saveCollabRoomToFirebase = async (
|
saveCollabRoomToFirebase = async (
|
||||||
syncableElements: ExcalidrawElement[] = getSyncableElements(
|
syncableElements: ExcalidrawElement[] = this.getSyncableElements(
|
||||||
this.excalidrawAPI.getSceneElementsIncludingDeleted(),
|
this.excalidrawAPI.getSceneElementsIncludingDeleted(),
|
||||||
),
|
),
|
||||||
) => {
|
) => {
|
||||||
@@ -565,7 +565,7 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
|
|||||||
) {
|
) {
|
||||||
this.portal.broadcastScene(
|
this.portal.broadcastScene(
|
||||||
SCENE.UPDATE,
|
SCENE.UPDATE,
|
||||||
getSyncableElements(elements),
|
this.getSyncableElements(elements),
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
this.lastBroadcastedOrReceivedSceneVersion = getSceneVersion(elements);
|
this.lastBroadcastedOrReceivedSceneVersion = getSceneVersion(elements);
|
||||||
@@ -576,7 +576,7 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
|
|||||||
queueBroadcastAllElements = throttle(() => {
|
queueBroadcastAllElements = throttle(() => {
|
||||||
this.portal.broadcastScene(
|
this.portal.broadcastScene(
|
||||||
SCENE.UPDATE,
|
SCENE.UPDATE,
|
||||||
getSyncableElements(
|
this.getSyncableElements(
|
||||||
this.excalidrawAPI.getSceneElementsIncludingDeleted(),
|
this.excalidrawAPI.getSceneElementsIncludingDeleted(),
|
||||||
),
|
),
|
||||||
true,
|
true,
|
||||||
@@ -591,8 +591,6 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
|
|||||||
|
|
||||||
handleClose = () => {
|
handleClose = () => {
|
||||||
this.setState({ modalIsShown: false });
|
this.setState({ modalIsShown: false });
|
||||||
const collabIcon = document.querySelector(".CollabButton") as HTMLElement;
|
|
||||||
collabIcon.focus();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
onUsernameChange = (username: string) => {
|
onUsernameChange = (username: string) => {
|
||||||
@@ -606,6 +604,9 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
getSyncableElements = (elements: readonly ExcalidrawElement[]) =>
|
||||||
|
elements.filter((el) => el.isDeleted || !isInvisiblySmallElement(el));
|
||||||
|
|
||||||
/** PRIVATE. Use `this.getContextValue()` instead. */
|
/** PRIVATE. Use `this.getContextValue()` instead. */
|
||||||
private contextValue: CollabAPI | null = null;
|
private contextValue: CollabAPI | null = null;
|
||||||
|
|
||||||
@@ -640,6 +641,7 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
|
|||||||
setErrorMessage={(errorMessage) => {
|
setErrorMessage={(errorMessage) => {
|
||||||
this.setState({ errorMessage });
|
this.setState({ errorMessage });
|
||||||
}}
|
}}
|
||||||
|
theme={this.excalidrawAPI.getAppState().theme}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{errorMessage && (
|
{errorMessage && (
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import {
|
|||||||
|
|
||||||
import CollabWrapper from "./CollabWrapper";
|
import CollabWrapper from "./CollabWrapper";
|
||||||
|
|
||||||
import { getSyncableElements } from "../../packages/excalidraw/index";
|
|
||||||
import { ExcalidrawElement } from "../../element/types";
|
import { ExcalidrawElement } from "../../element/types";
|
||||||
import { BROADCAST, SCENE } from "../app_constants";
|
import { BROADCAST, SCENE } from "../app_constants";
|
||||||
import { UserIdleState } from "../../types";
|
import { UserIdleState } from "../../types";
|
||||||
@@ -39,7 +38,9 @@ class Portal {
|
|||||||
this.socket.on("new-user", async (_socketId: string) => {
|
this.socket.on("new-user", async (_socketId: string) => {
|
||||||
this.broadcastScene(
|
this.broadcastScene(
|
||||||
SCENE.INIT,
|
SCENE.INIT,
|
||||||
getSyncableElements(this.collab.getSceneElementsIncludingDeleted()),
|
this.collab.getSyncableElements(
|
||||||
|
this.collab.getSceneElementsIncludingDeleted(),
|
||||||
|
),
|
||||||
/* syncAll */ true,
|
/* syncAll */ true,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { ToolButton } from "../../components/ToolButton";
|
|||||||
import { t } from "../../i18n";
|
import { t } from "../../i18n";
|
||||||
import "./RoomDialog.scss";
|
import "./RoomDialog.scss";
|
||||||
import Stack from "../../components/Stack";
|
import Stack from "../../components/Stack";
|
||||||
|
import { AppState } from "../../types";
|
||||||
|
|
||||||
const getShareIcon = () => {
|
const getShareIcon = () => {
|
||||||
const navigator = window.navigator as any;
|
const navigator = window.navigator as any;
|
||||||
@@ -36,6 +37,7 @@ const RoomDialog = ({
|
|||||||
onRoomCreate,
|
onRoomCreate,
|
||||||
onRoomDestroy,
|
onRoomDestroy,
|
||||||
setErrorMessage,
|
setErrorMessage,
|
||||||
|
theme,
|
||||||
}: {
|
}: {
|
||||||
handleClose: () => void;
|
handleClose: () => void;
|
||||||
activeRoomLink: string;
|
activeRoomLink: string;
|
||||||
@@ -44,6 +46,7 @@ const RoomDialog = ({
|
|||||||
onRoomCreate: () => void;
|
onRoomCreate: () => void;
|
||||||
onRoomDestroy: () => void;
|
onRoomDestroy: () => void;
|
||||||
setErrorMessage: (message: string) => void;
|
setErrorMessage: (message: string) => void;
|
||||||
|
theme: AppState["theme"];
|
||||||
}) => {
|
}) => {
|
||||||
const roomLinkInput = useRef<HTMLInputElement>(null);
|
const roomLinkInput = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
@@ -168,6 +171,7 @@ const RoomDialog = ({
|
|||||||
small
|
small
|
||||||
onCloseRequest={handleClose}
|
onCloseRequest={handleClose}
|
||||||
title={t("labels.liveCollaboration")}
|
title={t("labels.liveCollaboration")}
|
||||||
|
theme={theme}
|
||||||
>
|
>
|
||||||
{renderRoomDialog()}
|
{renderRoomDialog()}
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@@ -3,13 +3,19 @@ import React from "react";
|
|||||||
|
|
||||||
// https://github.com/tholman/github-corners
|
// https://github.com/tholman/github-corners
|
||||||
export const GitHubCorner = React.memo(
|
export const GitHubCorner = React.memo(
|
||||||
({ theme }: { theme: "light" | "dark" }) => (
|
({ theme, dir }: { theme: "light" | "dark"; dir: string }) => (
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
width="40"
|
width="40"
|
||||||
height="40"
|
height="40"
|
||||||
viewBox="0 0 250 250"
|
viewBox="0 0 250 250"
|
||||||
className="github-corner rtl-mirror"
|
className="rtl-mirror"
|
||||||
|
style={{
|
||||||
|
marginTop: "calc(var(--space-factor) * -1)",
|
||||||
|
[dir === "rtl"
|
||||||
|
? "marginLeft"
|
||||||
|
: "marginRight"]: "calc(var(--space-factor) * -1)",
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
href="https://github.com/excalidraw/excalidraw"
|
href="https://github.com/excalidraw/excalidraw"
|
||||||
@@ -19,18 +25,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[8]}
|
fill={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 : oc.black}
|
fill={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 : oc.black}
|
fill={theme === "light" ? oc.white : "var(--default-bg-color)"}
|
||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
</svg>
|
</svg>
|
||||||
@@ -245,10 +245,10 @@ const importFromBackend = async (
|
|||||||
export const loadScene = async (
|
export const loadScene = async (
|
||||||
id: string | null,
|
id: string | null,
|
||||||
privateKey: string | null,
|
privateKey: string | null,
|
||||||
// Supply initialData even if importing from backend to ensure we restore
|
// Supply local state even if importing from backend to ensure we restore
|
||||||
// localStorage user settings which we do not persist on server.
|
// localStorage user settings which we do not persist on server.
|
||||||
// Non-optional so we don't forget to pass it even if `undefined`.
|
// Non-optional so we don't forget to pass it even if `undefined`.
|
||||||
initialData: ImportedDataState | undefined | null,
|
localDataState: ImportedDataState | undefined | null,
|
||||||
) => {
|
) => {
|
||||||
let data;
|
let data;
|
||||||
if (id != null) {
|
if (id != null) {
|
||||||
@@ -256,10 +256,10 @@ export const loadScene = async (
|
|||||||
// extra care not to leak it
|
// extra care not to leak it
|
||||||
data = restore(
|
data = restore(
|
||||||
await importFromBackend(id, privateKey),
|
await importFromBackend(id, privateKey),
|
||||||
initialData?.appState,
|
localDataState?.appState,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
data = restore(initialData || null, null);
|
data = restore(localDataState || null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
.excalidraw {
|
||||||
|
.layer-ui__wrapper__footer-center {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.encrypted-icon {
|
||||||
|
border-radius: var(--space-factor);
|
||||||
|
color: var(--icon-green-fill-color);
|
||||||
|
margin-top: 13px;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 1.2rem;
|
||||||
|
height: 1.2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+130
-17
@@ -14,12 +14,13 @@ import { TopErrorBoundary } from "../components/TopErrorBoundary";
|
|||||||
import {
|
import {
|
||||||
APP_NAME,
|
APP_NAME,
|
||||||
EVENT,
|
EVENT,
|
||||||
|
STORAGE_KEYS,
|
||||||
TITLE_TIMEOUT,
|
TITLE_TIMEOUT,
|
||||||
URL_HASH_KEYS,
|
URL_HASH_KEYS,
|
||||||
VERSION_TIMEOUT,
|
VERSION_TIMEOUT,
|
||||||
} from "../constants";
|
} from "../constants";
|
||||||
import { loadFromBlob } from "../data/blob";
|
import { loadFromBlob } from "../data/blob";
|
||||||
import { DataState, ImportedDataState } from "../data/types";
|
import { ImportedDataState } from "../data/types";
|
||||||
import {
|
import {
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
NonDeletedExcalidrawElement,
|
NonDeletedExcalidrawElement,
|
||||||
@@ -30,7 +31,7 @@ import Excalidraw, {
|
|||||||
defaultLang,
|
defaultLang,
|
||||||
languages,
|
languages,
|
||||||
} from "../packages/excalidraw/index";
|
} from "../packages/excalidraw/index";
|
||||||
import { AppState } from "../types";
|
import { AppState, LibraryItems } from "../types";
|
||||||
import {
|
import {
|
||||||
debounce,
|
debounce,
|
||||||
getVersion,
|
getVersion,
|
||||||
@@ -50,6 +51,11 @@ import {
|
|||||||
saveToLocalStorage,
|
saveToLocalStorage,
|
||||||
} from "./data/localStorage";
|
} from "./data/localStorage";
|
||||||
import CustomStats from "./CustomStats";
|
import CustomStats from "./CustomStats";
|
||||||
|
import { restoreAppState, RestoredDataState } from "../data/restore";
|
||||||
|
import { Tooltip } from "../components/Tooltip";
|
||||||
|
import { shield } from "../components/icons";
|
||||||
|
|
||||||
|
import "./index.scss";
|
||||||
|
|
||||||
const languageDetector = new LanguageDetector();
|
const languageDetector = new LanguageDetector();
|
||||||
languageDetector.init({
|
languageDetector.init({
|
||||||
@@ -81,13 +87,11 @@ const initializeScene = async (opts: {
|
|||||||
);
|
);
|
||||||
const externalUrlMatch = window.location.hash.match(/^#url=(.*)$/);
|
const externalUrlMatch = window.location.hash.match(/^#url=(.*)$/);
|
||||||
|
|
||||||
const initialData = importFromLocalStorage();
|
const localDataState = importFromLocalStorage();
|
||||||
|
|
||||||
let scene: DataState & { scrollToContent?: boolean } = await loadScene(
|
let scene: RestoredDataState & {
|
||||||
null,
|
scrollToContent?: boolean;
|
||||||
null,
|
} = await loadScene(null, null, localDataState);
|
||||||
initialData,
|
|
||||||
);
|
|
||||||
|
|
||||||
let roomLinkData = getCollaborationLinkData(window.location.href);
|
let roomLinkData = getCollaborationLinkData(window.location.href);
|
||||||
const isExternalScene = !!(id || jsonBackendMatch || roomLinkData);
|
const isExternalScene = !!(id || jsonBackendMatch || roomLinkData);
|
||||||
@@ -102,12 +106,12 @@ const initializeScene = async (opts: {
|
|||||||
) {
|
) {
|
||||||
// Backwards compatibility with legacy url format
|
// Backwards compatibility with legacy url format
|
||||||
if (id) {
|
if (id) {
|
||||||
scene = await loadScene(id, null, initialData);
|
scene = await loadScene(id, null, localDataState);
|
||||||
} else if (jsonBackendMatch) {
|
} else if (jsonBackendMatch) {
|
||||||
scene = await loadScene(
|
scene = await loadScene(
|
||||||
jsonBackendMatch[1],
|
jsonBackendMatch[1],
|
||||||
jsonBackendMatch[2],
|
jsonBackendMatch[2],
|
||||||
initialData,
|
localDataState,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
scene.scrollToContent = true;
|
scene.scrollToContent = true;
|
||||||
@@ -161,6 +165,20 @@ const initializeScene = async (opts: {
|
|||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const PlusLinkJSX = (
|
||||||
|
<p style={{ direction: "ltr", unicodeBidi: "embed" }}>
|
||||||
|
Introducing Excalidraw+
|
||||||
|
<br />
|
||||||
|
<a
|
||||||
|
href="https://plus.excalidraw.com/?utm_source=excalidraw&utm_medium=banner&utm_campaign=launch"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
Try out now!
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
|
||||||
const ExcalidrawWrapper = () => {
|
const ExcalidrawWrapper = () => {
|
||||||
const [errorMessage, setErrorMessage] = useState("");
|
const [errorMessage, setErrorMessage] = useState("");
|
||||||
const currentLangCode = languageDetector.detect() || defaultLang.code;
|
const currentLangCode = languageDetector.detect() || defaultLang.code;
|
||||||
@@ -196,6 +214,18 @@ const ExcalidrawWrapper = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
initializeScene({ collabAPI }).then((scene) => {
|
initializeScene({ collabAPI }).then((scene) => {
|
||||||
|
if (scene) {
|
||||||
|
try {
|
||||||
|
scene.libraryItems =
|
||||||
|
JSON.parse(
|
||||||
|
localStorage.getItem(
|
||||||
|
STORAGE_KEYS.LOCAL_STORAGE_LIBRARY,
|
||||||
|
) as string,
|
||||||
|
) || [];
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
initialStatePromiseRef.current.promise.resolve(scene);
|
initialStatePromiseRef.current.promise.resolve(scene);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -213,7 +243,10 @@ const ExcalidrawWrapper = () => {
|
|||||||
} else {
|
} else {
|
||||||
initializeScene({ collabAPI }).then((scene) => {
|
initializeScene({ collabAPI }).then((scene) => {
|
||||||
if (scene) {
|
if (scene) {
|
||||||
excalidrawAPI.updateScene(scene);
|
excalidrawAPI.updateScene({
|
||||||
|
...scene,
|
||||||
|
appState: restoreAppState(scene.appState, null),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -278,8 +311,41 @@ const ExcalidrawWrapper = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const renderTopRightUI = useCallback(
|
||||||
|
(isMobile: boolean, appState: AppState) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: "24ch",
|
||||||
|
fontSize: "0.7em",
|
||||||
|
textAlign: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* <GitHubCorner theme={appState.theme} dir={document.dir} /> */}
|
||||||
|
{/* FIXME remove after 2021-05-20 */}
|
||||||
|
{PlusLinkJSX}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
const renderFooter = useCallback(
|
const renderFooter = useCallback(
|
||||||
(isMobile: boolean) => {
|
(isMobile: boolean) => {
|
||||||
|
const renderEncryptedIcon = () => (
|
||||||
|
<a
|
||||||
|
className="encrypted-icon tooltip"
|
||||||
|
href="https://blog.excalidraw.com/end-to-end-encryption/"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
aria-label={t("encrypted.link")}
|
||||||
|
>
|
||||||
|
<Tooltip label={t("encrypted.tooltip")} long={true}>
|
||||||
|
{shield}
|
||||||
|
</Tooltip>
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
|
||||||
const renderLanguageList = () => (
|
const renderLanguageList = () => (
|
||||||
<LanguageList
|
<LanguageList
|
||||||
onChange={(langCode) => {
|
onChange={(langCode) => {
|
||||||
@@ -291,14 +357,43 @@ const ExcalidrawWrapper = () => {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
if (isMobile) {
|
if (isMobile) {
|
||||||
|
const isTinyDevice = window.innerWidth < 362;
|
||||||
return (
|
return (
|
||||||
<fieldset>
|
<div
|
||||||
<legend>{t("labels.language")}</legend>
|
style={{
|
||||||
{renderLanguageList()}
|
display: "flex",
|
||||||
</fieldset>
|
flexDirection: isTinyDevice ? "column" : "row",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<fieldset>
|
||||||
|
<legend>{t("labels.language")}</legend>
|
||||||
|
{renderLanguageList()}
|
||||||
|
</fieldset>
|
||||||
|
{/* FIXME remove after 2021-05-20 */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: "24ch",
|
||||||
|
fontSize: "0.7em",
|
||||||
|
textAlign: "center",
|
||||||
|
marginTop: isTinyDevice ? 16 : undefined,
|
||||||
|
marginLeft: "auto",
|
||||||
|
marginRight: isTinyDevice ? "auto" : undefined,
|
||||||
|
padding: "4px 2px",
|
||||||
|
border: "1px dashed #aaa",
|
||||||
|
borderRadius: 12,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{PlusLinkJSX}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return renderLanguageList();
|
return (
|
||||||
|
<>
|
||||||
|
{renderEncryptedIcon()}
|
||||||
|
{renderLanguageList()}
|
||||||
|
</>
|
||||||
|
);
|
||||||
},
|
},
|
||||||
[langCode],
|
[langCode],
|
||||||
);
|
);
|
||||||
@@ -311,6 +406,15 @@ const ExcalidrawWrapper = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onLibraryChange = async (items: LibraryItems) => {
|
||||||
|
if (!items.length) {
|
||||||
|
localStorage.removeItem(STORAGE_KEYS.LOCAL_STORAGE_LIBRARY);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const serializedItems = JSON.stringify(items);
|
||||||
|
localStorage.setItem(STORAGE_KEYS.LOCAL_STORAGE_LIBRARY, serializedItems);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Excalidraw
|
<Excalidraw
|
||||||
@@ -320,11 +424,20 @@ const ExcalidrawWrapper = () => {
|
|||||||
onCollabButtonClick={collabAPI?.onCollabButtonClick}
|
onCollabButtonClick={collabAPI?.onCollabButtonClick}
|
||||||
isCollaborating={collabAPI?.isCollaborating()}
|
isCollaborating={collabAPI?.isCollaborating()}
|
||||||
onPointerUpdate={collabAPI?.onPointerUpdate}
|
onPointerUpdate={collabAPI?.onPointerUpdate}
|
||||||
onExportToBackend={onExportToBackend}
|
UIOptions={{
|
||||||
|
canvasActions: {
|
||||||
|
export: {
|
||||||
|
onExportToBackend,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
renderTopRightUI={renderTopRightUI}
|
||||||
renderFooter={renderFooter}
|
renderFooter={renderFooter}
|
||||||
langCode={langCode}
|
langCode={langCode}
|
||||||
renderCustomStats={renderCustomStats}
|
renderCustomStats={renderCustomStats}
|
||||||
detectScroll={false}
|
detectScroll={false}
|
||||||
|
handleKeyboardGlobally={true}
|
||||||
|
onLibraryChange={onLibraryChange}
|
||||||
/>
|
/>
|
||||||
{excalidrawAPI && <CollabWrapper excalidrawAPI={excalidrawAPI} />}
|
{excalidrawAPI && <CollabWrapper excalidrawAPI={excalidrawAPI} />}
|
||||||
{errorMessage && (
|
{errorMessage && (
|
||||||
|
|||||||
+5
-1
@@ -67,10 +67,14 @@ export const selectGroupsForSelectedElements = (
|
|||||||
appState: AppState,
|
appState: AppState,
|
||||||
elements: readonly NonDeleted<ExcalidrawElement>[],
|
elements: readonly NonDeleted<ExcalidrawElement>[],
|
||||||
): AppState => {
|
): AppState => {
|
||||||
let nextAppState = { ...appState };
|
let nextAppState: AppState = { ...appState, selectedGroupIds: {} };
|
||||||
|
|
||||||
const selectedElements = getSelectedElements(elements, appState);
|
const selectedElements = getSelectedElements(elements, appState);
|
||||||
|
|
||||||
|
if (!selectedElements.length) {
|
||||||
|
return { ...nextAppState, editingGroupId: null };
|
||||||
|
}
|
||||||
|
|
||||||
for (const selectedElement of selectedElements) {
|
for (const selectedElement of selectedElements) {
|
||||||
let groupIds = selectedElement.groupIds;
|
let groupIds = selectedElement.groupIds;
|
||||||
if (appState.editingGroupId) {
|
if (appState.editingGroupId) {
|
||||||
|
|||||||
+4
-6
@@ -21,6 +21,7 @@ interface DehydratedHistoryEntry {
|
|||||||
const clearAppStatePropertiesForHistory = (appState: AppState) => {
|
const clearAppStatePropertiesForHistory = (appState: AppState) => {
|
||||||
return {
|
return {
|
||||||
selectedElementIds: appState.selectedElementIds,
|
selectedElementIds: appState.selectedElementIds,
|
||||||
|
selectedGroupIds: appState.selectedGroupIds,
|
||||||
viewBackgroundColor: appState.viewBackgroundColor,
|
viewBackgroundColor: appState.viewBackgroundColor,
|
||||||
editingLinearElement: appState.editingLinearElement,
|
editingLinearElement: appState.editingLinearElement,
|
||||||
editingGroupId: appState.editingGroupId,
|
editingGroupId: appState.editingGroupId,
|
||||||
@@ -28,7 +29,7 @@ const clearAppStatePropertiesForHistory = (appState: AppState) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export class SceneHistory {
|
class History {
|
||||||
private elementCache = new Map<string, Map<number, ExcalidrawElement>>();
|
private elementCache = new Map<string, Map<number, ExcalidrawElement>>();
|
||||||
private recording: boolean = true;
|
private recording: boolean = true;
|
||||||
private stateHistory: DehydratedHistoryEntry[] = [];
|
private stateHistory: DehydratedHistoryEntry[] = [];
|
||||||
@@ -169,7 +170,7 @@ export class SceneHistory {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (key === "selectedElementIds") {
|
if (key === "selectedElementIds" || key === "selectedGroupIds") {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (nextEntry.appState[key] !== lastEntry.appState[key]) {
|
if (nextEntry.appState[key] !== lastEntry.appState[key]) {
|
||||||
@@ -260,7 +261,4 @@ export class SceneHistory {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createHistory: () => { history: SceneHistory } = () => {
|
export default History;
|
||||||
const history = new SceneHistory();
|
|
||||||
return { history };
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -47,6 +47,8 @@ const allLanguages: Language[] = [
|
|||||||
{ code: "uk-UA", label: "Українська" },
|
{ code: "uk-UA", label: "Українська" },
|
||||||
{ code: "zh-CN", label: "简体中文" },
|
{ code: "zh-CN", label: "简体中文" },
|
||||||
{ code: "zh-TW", label: "繁體中文" },
|
{ code: "zh-TW", label: "繁體中文" },
|
||||||
|
{ code: "lv-LV", label: "Latviešu" },
|
||||||
|
{ code: "cs-CZ", label: "Česky" },
|
||||||
].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",
|
||||||
shouldAddWatermark: false,
|
|
||||||
scale: 1,
|
scale: 1,
|
||||||
},
|
},
|
||||||
createCanvas,
|
createCanvas,
|
||||||
|
|||||||
@@ -14,7 +14,9 @@ export const CODES = {
|
|||||||
NINE: "Digit9",
|
NINE: "Digit9",
|
||||||
QUOTE: "Quote",
|
QUOTE: "Quote",
|
||||||
ZERO: "Digit0",
|
ZERO: "Digit0",
|
||||||
|
SLASH: "Slash",
|
||||||
C: "KeyC",
|
C: "KeyC",
|
||||||
|
D: "KeyD",
|
||||||
G: "KeyG",
|
G: "KeyG",
|
||||||
F: "KeyF",
|
F: "KeyF",
|
||||||
H: "KeyH",
|
H: "KeyH",
|
||||||
@@ -42,6 +44,7 @@ export const KEYS = {
|
|||||||
A: "a",
|
A: "a",
|
||||||
D: "d",
|
D: "d",
|
||||||
E: "e",
|
E: "e",
|
||||||
|
G: "g",
|
||||||
L: "l",
|
L: "l",
|
||||||
O: "o",
|
O: "o",
|
||||||
P: "p",
|
P: "p",
|
||||||
|
|||||||
+52
-35
@@ -20,6 +20,10 @@
|
|||||||
"background": "الخلفية",
|
"background": "الخلفية",
|
||||||
"fill": "التعبئة",
|
"fill": "التعبئة",
|
||||||
"strokeWidth": "حجم الحدود",
|
"strokeWidth": "حجم الحدود",
|
||||||
|
"strokeShape": "",
|
||||||
|
"strokeShape_gel": "",
|
||||||
|
"strokeShape_fountain": "",
|
||||||
|
"strokeShape_brush": "",
|
||||||
"strokeStyle": "نمط الحدود",
|
"strokeStyle": "نمط الحدود",
|
||||||
"strokeStyle_solid": "صلبة",
|
"strokeStyle_solid": "صلبة",
|
||||||
"strokeStyle_dashed": "متقطع",
|
"strokeStyle_dashed": "متقطع",
|
||||||
@@ -38,8 +42,8 @@
|
|||||||
"fontSize": "حجم الخط",
|
"fontSize": "حجم الخط",
|
||||||
"fontFamily": "نوع الخط",
|
"fontFamily": "نوع الخط",
|
||||||
"onlySelected": "المحدد فقط",
|
"onlySelected": "المحدد فقط",
|
||||||
"withBackground": "مع الخلفية",
|
"withBackground": "الخلفية",
|
||||||
"exportEmbedScene": "تضمين المشهد في ملف التصدير",
|
"exportEmbedScene": "",
|
||||||
"exportEmbedScene_details": "سيتم حفظ بيانات المشهد في ملف PNG/SVG المصدّر بحيث يمكن استعادة المشهد منه.\nسيزيد حجم الملف المصدر.",
|
"exportEmbedScene_details": "سيتم حفظ بيانات المشهد في ملف PNG/SVG المصدّر بحيث يمكن استعادة المشهد منه.\nسيزيد حجم الملف المصدر.",
|
||||||
"addWatermark": "إضافة \"مصنوعة بواسطة Excalidraw\"",
|
"addWatermark": "إضافة \"مصنوعة بواسطة Excalidraw\"",
|
||||||
"handDrawn": "رسم باليد",
|
"handDrawn": "رسم باليد",
|
||||||
@@ -61,7 +65,7 @@
|
|||||||
"architect": "معماري",
|
"architect": "معماري",
|
||||||
"artist": "رسام",
|
"artist": "رسام",
|
||||||
"cartoonist": "كرتوني",
|
"cartoonist": "كرتوني",
|
||||||
"fileTitle": "",
|
"fileTitle": "إسم الملف",
|
||||||
"colorPicker": "اختيار الألوان",
|
"colorPicker": "اختيار الألوان",
|
||||||
"canvasBackground": "خلفية اللوحة",
|
"canvasBackground": "خلفية اللوحة",
|
||||||
"drawingCanvas": "لوحة الرسم",
|
"drawingCanvas": "لوحة الرسم",
|
||||||
@@ -92,21 +96,24 @@
|
|||||||
"centerHorizontally": "توسيط أفقي",
|
"centerHorizontally": "توسيط أفقي",
|
||||||
"distributeHorizontally": "التوزيع الأفقي",
|
"distributeHorizontally": "التوزيع الأفقي",
|
||||||
"distributeVertically": "التوزيع عمودياً",
|
"distributeVertically": "التوزيع عمودياً",
|
||||||
"flipHorizontal": "",
|
"flipHorizontal": "قلب عامودي",
|
||||||
"flipVertical": "",
|
"flipVertical": "قلب أفقي",
|
||||||
"viewMode": "نمط العرض",
|
"viewMode": "نمط العرض",
|
||||||
"toggleExportColorScheme": "",
|
"toggleExportColorScheme": "",
|
||||||
"share": "مشاركة"
|
"share": "مشاركة",
|
||||||
|
"toggleTheme": "غير النمط"
|
||||||
},
|
},
|
||||||
"buttons": {
|
"buttons": {
|
||||||
"clearReset": "إعادة تعيين اللوحة",
|
"clearReset": "إعادة تعيين اللوحة",
|
||||||
|
"exportJSON": "صدر الملف",
|
||||||
|
"exportImage": "إحفظ كصورة",
|
||||||
"export": "تصدير",
|
"export": "تصدير",
|
||||||
"exportToPng": "تصدير بصيغة PNG",
|
"exportToPng": "تصدير بصيغة PNG",
|
||||||
"exportToSvg": "تصدير بصيغة SVG",
|
"exportToSvg": "تصدير بصيغة SVG",
|
||||||
"copyToClipboard": "نسخ إلى الحافظة",
|
"copyToClipboard": "نسخ إلى الحافظة",
|
||||||
"copyPngToClipboard": "نسخ الـ PNG إلى الحافظة",
|
"copyPngToClipboard": "نسخ الـ PNG إلى الحافظة",
|
||||||
"scale": "مقاس",
|
"scale": "مقاس",
|
||||||
"save": "حفظ",
|
"save": "احفظ للملف الحالي",
|
||||||
"saveAs": "حفظ كـ",
|
"saveAs": "حفظ كـ",
|
||||||
"load": "تحميل",
|
"load": "تحميل",
|
||||||
"getShareableLink": "احصل على رابط المشاركة",
|
"getShareableLink": "احصل على رابط المشاركة",
|
||||||
@@ -142,6 +149,8 @@
|
|||||||
"loadSceneOverridePrompt": "تحميل الرسم الخارجي سيحل محل المحتوى الموجود لديك. هل ترغب في المتابعة؟",
|
"loadSceneOverridePrompt": "تحميل الرسم الخارجي سيحل محل المحتوى الموجود لديك. هل ترغب في المتابعة؟",
|
||||||
"collabStopOverridePrompt": "إيقاف الجلسة سيؤدي إلى الكتابة فوق رسومك السابقة المخزنة داخليا. هل أنت متأكد؟\n\n(إذا كنت ترغب في الاحتفاظ برسمك المخزن داخليا، ببساطة أغلق علامة تبويب المتصفح بدلاً من ذلك.)",
|
"collabStopOverridePrompt": "إيقاف الجلسة سيؤدي إلى الكتابة فوق رسومك السابقة المخزنة داخليا. هل أنت متأكد؟\n\n(إذا كنت ترغب في الاحتفاظ برسمك المخزن داخليا، ببساطة أغلق علامة تبويب المتصفح بدلاً من ذلك.)",
|
||||||
"errorLoadingLibrary": "حصل خطأ أثناء تحميل مكتبة الطرف الثالث.",
|
"errorLoadingLibrary": "حصل خطأ أثناء تحميل مكتبة الطرف الثالث.",
|
||||||
|
"errorAddingToLibrary": "",
|
||||||
|
"errorRemovingFromLibrary": "",
|
||||||
"confirmAddLibrary": "هذا سيضيف {{numShapes}} شكل إلى مكتبتك. هل أنت متأكد؟",
|
"confirmAddLibrary": "هذا سيضيف {{numShapes}} شكل إلى مكتبتك. هل أنت متأكد؟",
|
||||||
"imageDoesNotContainScene": "استيراد الصور غير مدعوم في الوقت الراهن.\n\nهل تريد استيراد مشهد؟ لا يبدو أن هذه الصورة تحتوي على أي بيانات مشهد. هل قمت بسماح هذا أثناء التصدير؟",
|
"imageDoesNotContainScene": "استيراد الصور غير مدعوم في الوقت الراهن.\n\nهل تريد استيراد مشهد؟ لا يبدو أن هذه الصورة تحتوي على أي بيانات مشهد. هل قمت بسماح هذا أثناء التصدير؟",
|
||||||
"cannotRestoreFromImage": "تعذر استعادة المشهد من ملف الصورة",
|
"cannotRestoreFromImage": "تعذر استعادة المشهد من ملف الصورة",
|
||||||
@@ -150,12 +159,12 @@
|
|||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "تحديد",
|
"selection": "تحديد",
|
||||||
"draw": "الكتابة الحرة",
|
|
||||||
"rectangle": "مستطيل",
|
"rectangle": "مستطيل",
|
||||||
"diamond": "مضلع",
|
"diamond": "مضلع",
|
||||||
"ellipse": "دائرة",
|
"ellipse": "دائرة",
|
||||||
"arrow": "سهم",
|
"arrow": "سهم",
|
||||||
"line": "خط",
|
"line": "خط",
|
||||||
|
"freedraw": "",
|
||||||
"text": "نص",
|
"text": "نص",
|
||||||
"library": "مكتبة",
|
"library": "مكتبة",
|
||||||
"lock": "الحفاظ على أداة التحديد نشطة بعد الرسم"
|
"lock": "الحفاظ على أداة التحديد نشطة بعد الرسم"
|
||||||
@@ -208,30 +217,38 @@
|
|||||||
"errorDialog": {
|
"errorDialog": {
|
||||||
"title": "خطأ"
|
"title": "خطأ"
|
||||||
},
|
},
|
||||||
|
"exportDialog": {
|
||||||
|
"disk_title": "حفظ الملف للجهاز",
|
||||||
|
"disk_details": "",
|
||||||
|
"disk_button": "إحفظ لملف",
|
||||||
|
"link_title": "رابط قابل للمشاركة",
|
||||||
|
"link_details": "صدر الملف للمشاهدة فقط.",
|
||||||
|
"link_button": "التصدير كرابط"
|
||||||
|
},
|
||||||
"helpDialog": {
|
"helpDialog": {
|
||||||
"blog": "",
|
"blog": "اقرأ مدونتنا",
|
||||||
"click": "",
|
"click": "انقر",
|
||||||
"curvedArrow": "",
|
"curvedArrow": "سهم مائل",
|
||||||
"curvedLine": "",
|
"curvedLine": "خط مائل",
|
||||||
"documentation": "",
|
"documentation": "دليل الاستخدام",
|
||||||
"drag": "اسحب",
|
"drag": "اسحب",
|
||||||
"editor": "المحرر",
|
"editor": "المحرر",
|
||||||
"github": "عثرت على مشكلة؟ إرسال",
|
"github": "عثرت على مشكلة؟ إرسال",
|
||||||
"howto": "",
|
"howto": "اتبع التعليمات",
|
||||||
"or": "",
|
"or": "أو",
|
||||||
"preventBinding": "",
|
"preventBinding": "منع ارتبط السهم",
|
||||||
"shapes": "",
|
"shapes": "أشكال",
|
||||||
"shortcuts": "",
|
"shortcuts": "اختصارات لوحة المفاتيح",
|
||||||
"textFinish": "",
|
"textFinish": "الانتهاء من التحرير (نص)",
|
||||||
"textNewLine": "",
|
"textNewLine": "اضف سطر جديد (نص)",
|
||||||
"title": "",
|
"title": "المساعدة",
|
||||||
"view": "",
|
"view": "عرض",
|
||||||
"zoomToFit": "",
|
"zoomToFit": "تكبير للملائمة",
|
||||||
"zoomToSelection": ""
|
"zoomToSelection": "تكبير للعنصر المحدد"
|
||||||
},
|
},
|
||||||
"encrypted": {
|
"encrypted": {
|
||||||
"tooltip": "رسوماتك مشفرة من النهاية إلى النهاية حتى أن خوادم Excalidraw لن تراها أبدا.",
|
"tooltip": "رسوماتك مشفرة من النهاية إلى النهاية حتى أن خوادم Excalidraw لن تراها أبدا.",
|
||||||
"link": ""
|
"link": "مشاركة المدونة في التشفير من النهاية إلى النهاية في Excalidraw"
|
||||||
},
|
},
|
||||||
"stats": {
|
"stats": {
|
||||||
"angle": "الزاوية",
|
"angle": "الزاوية",
|
||||||
@@ -243,18 +260,18 @@
|
|||||||
"storage": "التخزين",
|
"storage": "التخزين",
|
||||||
"title": "إحصائيات للمهووسين",
|
"title": "إحصائيات للمهووسين",
|
||||||
"total": "المجموع",
|
"total": "المجموع",
|
||||||
"version": "",
|
"version": "الإصدار",
|
||||||
"versionCopy": "",
|
"versionCopy": "انقر للنسخ",
|
||||||
"versionNotAvailable": "",
|
"versionNotAvailable": "الإصدار غير متوفر",
|
||||||
"width": "العرض"
|
"width": "العرض"
|
||||||
},
|
},
|
||||||
"toast": {
|
"toast": {
|
||||||
"copyStyles": "",
|
"copyStyles": "نسخ النمط.",
|
||||||
"copyToClipboard": "",
|
"copyToClipboard": "نسخ إلى الحافظة.",
|
||||||
"copyToClipboardAsPng": "",
|
"copyToClipboardAsPng": "تم نسخ {{exportSelection}} إلى الحافظة بصيغةPNG\n({{exportColorScheme}})",
|
||||||
"fileSaved": "",
|
"fileSaved": "تم حفظ الملف.",
|
||||||
"fileSavedToFilename": "",
|
"fileSavedToFilename": "حفظ باسم {filename}",
|
||||||
"canvas": "",
|
"canvas": "لوحة الرسم",
|
||||||
"selection": ""
|
"selection": "العنصر المحدد"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+22
-5
@@ -20,6 +20,10 @@
|
|||||||
"background": "Фон",
|
"background": "Фон",
|
||||||
"fill": "Наситеност",
|
"fill": "Наситеност",
|
||||||
"strokeWidth": "Ширина на щриха",
|
"strokeWidth": "Ширина на щриха",
|
||||||
|
"strokeShape": "",
|
||||||
|
"strokeShape_gel": "",
|
||||||
|
"strokeShape_fountain": "",
|
||||||
|
"strokeShape_brush": "",
|
||||||
"strokeStyle": "Стил на линия",
|
"strokeStyle": "Стил на линия",
|
||||||
"strokeStyle_solid": "Плътен",
|
"strokeStyle_solid": "Плътен",
|
||||||
"strokeStyle_dashed": "Пунктир",
|
"strokeStyle_dashed": "Пунктир",
|
||||||
@@ -38,8 +42,8 @@
|
|||||||
"fontSize": "Размер на шрифта",
|
"fontSize": "Размер на шрифта",
|
||||||
"fontFamily": "Семейство шрифтове",
|
"fontFamily": "Семейство шрифтове",
|
||||||
"onlySelected": "Само избраното",
|
"onlySelected": "Само избраното",
|
||||||
"withBackground": "С фон",
|
"withBackground": "",
|
||||||
"exportEmbedScene": "Вгради сцената във файл",
|
"exportEmbedScene": "",
|
||||||
"exportEmbedScene_details": "Данните от сцената ще бъдат екпортирани в PNG/SVG файл, за да може сцената да бъде възстановена от него.\nТова ще увеличи размера на файла.",
|
"exportEmbedScene_details": "Данните от сцената ще бъдат екпортирани в PNG/SVG файл, за да може сцената да бъде възстановена от него.\nТова ще увеличи размера на файла.",
|
||||||
"addWatermark": "Добави \"Направено с Excalidraw\"",
|
"addWatermark": "Добави \"Направено с Excalidraw\"",
|
||||||
"handDrawn": "Нарисувано на ръка",
|
"handDrawn": "Нарисувано на ръка",
|
||||||
@@ -96,17 +100,20 @@
|
|||||||
"flipVertical": "",
|
"flipVertical": "",
|
||||||
"viewMode": "Изглед",
|
"viewMode": "Изглед",
|
||||||
"toggleExportColorScheme": "",
|
"toggleExportColorScheme": "",
|
||||||
"share": ""
|
"share": "",
|
||||||
|
"toggleTheme": ""
|
||||||
},
|
},
|
||||||
"buttons": {
|
"buttons": {
|
||||||
"clearReset": "Нулиране на платно",
|
"clearReset": "Нулиране на платно",
|
||||||
|
"exportJSON": "",
|
||||||
|
"exportImage": "",
|
||||||
"export": "Експортиране",
|
"export": "Експортиране",
|
||||||
"exportToPng": "Изнасяне в PNG",
|
"exportToPng": "Изнасяне в PNG",
|
||||||
"exportToSvg": "Изнасяне в SVG",
|
"exportToSvg": "Изнасяне в SVG",
|
||||||
"copyToClipboard": "Копиране в клипборда",
|
"copyToClipboard": "Копиране в клипборда",
|
||||||
"copyPngToClipboard": "Копирай PNG в клипборда",
|
"copyPngToClipboard": "Копирай PNG в клипборда",
|
||||||
"scale": "Мащаб",
|
"scale": "Мащаб",
|
||||||
"save": "Запази",
|
"save": "",
|
||||||
"saveAs": "Запиши като",
|
"saveAs": "Запиши като",
|
||||||
"load": "Зареждане",
|
"load": "Зареждане",
|
||||||
"getShareableLink": "Получаване на връзка за споделяне",
|
"getShareableLink": "Получаване на връзка за споделяне",
|
||||||
@@ -142,6 +149,8 @@
|
|||||||
"loadSceneOverridePrompt": "Зареждането на външна рисунка ще презапише настоящото ви съдържание. Желаете ли да продължите?",
|
"loadSceneOverridePrompt": "Зареждането на външна рисунка ще презапише настоящото ви съдържание. Желаете ли да продължите?",
|
||||||
"collabStopOverridePrompt": "Прекратяването на сесията ще презапише предишната, локално запазена, рисунка. Сигурни ли сте?\n\n(Ако искате да продължите с локалната рисунка, просто затворете таба на браузъра.)",
|
"collabStopOverridePrompt": "Прекратяването на сесията ще презапише предишната, локално запазена, рисунка. Сигурни ли сте?\n\n(Ако искате да продължите с локалната рисунка, просто затворете таба на браузъра.)",
|
||||||
"errorLoadingLibrary": "Възникна грешка при зареждането на външна библиотека.",
|
"errorLoadingLibrary": "Възникна грешка при зареждането на външна библиотека.",
|
||||||
|
"errorAddingToLibrary": "",
|
||||||
|
"errorRemovingFromLibrary": "",
|
||||||
"confirmAddLibrary": "Ще се добавят {{numShapes}} фигура(и) във вашата библиотека. Сигурни ли сте?",
|
"confirmAddLibrary": "Ще се добавят {{numShapes}} фигура(и) във вашата библиотека. Сигурни ли сте?",
|
||||||
"imageDoesNotContainScene": "Импортирането на картинки не се поддържва в момента.\n\nИскате да импортнете сцена? Тази картинка не съдържа данни от сцена. Разрешили ли сте последното при експортирането?",
|
"imageDoesNotContainScene": "Импортирането на картинки не се поддържва в момента.\n\nИскате да импортнете сцена? Тази картинка не съдържа данни от сцена. Разрешили ли сте последното при експортирането?",
|
||||||
"cannotRestoreFromImage": "Не може да бъде възстановена сцена от този файл",
|
"cannotRestoreFromImage": "Не може да бъде възстановена сцена от този файл",
|
||||||
@@ -150,12 +159,12 @@
|
|||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "Селекция",
|
"selection": "Селекция",
|
||||||
"draw": "Рисуване",
|
|
||||||
"rectangle": "Правоъгълник",
|
"rectangle": "Правоъгълник",
|
||||||
"diamond": "Диамант",
|
"diamond": "Диамант",
|
||||||
"ellipse": "Елипс",
|
"ellipse": "Елипс",
|
||||||
"arrow": "Стрелка",
|
"arrow": "Стрелка",
|
||||||
"line": "Линия",
|
"line": "Линия",
|
||||||
|
"freedraw": "",
|
||||||
"text": "Текст",
|
"text": "Текст",
|
||||||
"library": "Библиотека",
|
"library": "Библиотека",
|
||||||
"lock": "Поддържайте избрания инструмент активен след рисуване"
|
"lock": "Поддържайте избрания инструмент активен след рисуване"
|
||||||
@@ -208,6 +217,14 @@
|
|||||||
"errorDialog": {
|
"errorDialog": {
|
||||||
"title": "Грешка"
|
"title": "Грешка"
|
||||||
},
|
},
|
||||||
|
"exportDialog": {
|
||||||
|
"disk_title": "",
|
||||||
|
"disk_details": "",
|
||||||
|
"disk_button": "",
|
||||||
|
"link_title": "",
|
||||||
|
"link_details": "",
|
||||||
|
"link_button": ""
|
||||||
|
},
|
||||||
"helpDialog": {
|
"helpDialog": {
|
||||||
"blog": "Прочетете нашия блог",
|
"blog": "Прочетете нашия блог",
|
||||||
"click": "клик",
|
"click": "клик",
|
||||||
|
|||||||
+26
-9
@@ -20,6 +20,10 @@
|
|||||||
"background": "Color del fons",
|
"background": "Color del fons",
|
||||||
"fill": "Estil del fons",
|
"fill": "Estil del fons",
|
||||||
"strokeWidth": "Amplada del traç",
|
"strokeWidth": "Amplada del traç",
|
||||||
|
"strokeShape": "",
|
||||||
|
"strokeShape_gel": "",
|
||||||
|
"strokeShape_fountain": "",
|
||||||
|
"strokeShape_brush": "",
|
||||||
"strokeStyle": "Estil del traç",
|
"strokeStyle": "Estil del traç",
|
||||||
"strokeStyle_solid": "Sòlid",
|
"strokeStyle_solid": "Sòlid",
|
||||||
"strokeStyle_dashed": "Guions",
|
"strokeStyle_dashed": "Guions",
|
||||||
@@ -38,8 +42,8 @@
|
|||||||
"fontSize": "Mida de lletra",
|
"fontSize": "Mida de lletra",
|
||||||
"fontFamily": "Tipus de lletra",
|
"fontFamily": "Tipus de lletra",
|
||||||
"onlySelected": "Només seleccionats",
|
"onlySelected": "Només seleccionats",
|
||||||
"withBackground": "Amb fons",
|
"withBackground": "",
|
||||||
"exportEmbedScene": "Incrustar escena al fitxer exportat",
|
"exportEmbedScene": "",
|
||||||
"exportEmbedScene_details": "Les dades de l’escena es desaran al fitxer PNG/SVG de manera que es pugui restaurar l’escena.\nAugmentarà la mida del fitxer exportat.",
|
"exportEmbedScene_details": "Les dades de l’escena es desaran al fitxer PNG/SVG de manera que es pugui restaurar l’escena.\nAugmentarà la mida del fitxer exportat.",
|
||||||
"addWatermark": "Afegir \"Fet amb Excalidraw\"",
|
"addWatermark": "Afegir \"Fet amb Excalidraw\"",
|
||||||
"handDrawn": "Dibuixat a mà",
|
"handDrawn": "Dibuixat a mà",
|
||||||
@@ -61,7 +65,7 @@
|
|||||||
"architect": "Arquitecte",
|
"architect": "Arquitecte",
|
||||||
"artist": "Artista",
|
"artist": "Artista",
|
||||||
"cartoonist": "Dibuixant",
|
"cartoonist": "Dibuixant",
|
||||||
"fileTitle": "",
|
"fileTitle": "Nom del fitxer",
|
||||||
"colorPicker": "Selector de colors",
|
"colorPicker": "Selector de colors",
|
||||||
"canvasBackground": "Fons del llenç",
|
"canvasBackground": "Fons del llenç",
|
||||||
"drawingCanvas": "Llenç de dibuix",
|
"drawingCanvas": "Llenç de dibuix",
|
||||||
@@ -92,21 +96,24 @@
|
|||||||
"centerHorizontally": "Centrar horitzontalment",
|
"centerHorizontally": "Centrar horitzontalment",
|
||||||
"distributeHorizontally": "Distribuir horitzontalment",
|
"distributeHorizontally": "Distribuir horitzontalment",
|
||||||
"distributeVertically": "Distribuir verticalment",
|
"distributeVertically": "Distribuir verticalment",
|
||||||
"flipHorizontal": "",
|
"flipHorizontal": "Capgira horitzontalment",
|
||||||
"flipVertical": "",
|
"flipVertical": "Capgira verticalment",
|
||||||
"viewMode": "Mode de visualització",
|
"viewMode": "Mode de visualització",
|
||||||
"toggleExportColorScheme": "Canvia l'esquema de colors de l'exportació",
|
"toggleExportColorScheme": "Canvia l'esquema de colors de l'exportació",
|
||||||
"share": "Compartir"
|
"share": "Compartir",
|
||||||
|
"toggleTheme": ""
|
||||||
},
|
},
|
||||||
"buttons": {
|
"buttons": {
|
||||||
"clearReset": "Netejar el llenç",
|
"clearReset": "Netejar el llenç",
|
||||||
|
"exportJSON": "",
|
||||||
|
"exportImage": "",
|
||||||
"export": "Exportar",
|
"export": "Exportar",
|
||||||
"exportToPng": "Exportar a PNG",
|
"exportToPng": "Exportar a PNG",
|
||||||
"exportToSvg": "Exportar a SNG",
|
"exportToSvg": "Exportar a SNG",
|
||||||
"copyToClipboard": "Copiar al porta-retalls",
|
"copyToClipboard": "Copiar al porta-retalls",
|
||||||
"copyPngToClipboard": "Copiar PNG al porta-retalls",
|
"copyPngToClipboard": "Copiar PNG al porta-retalls",
|
||||||
"scale": "Escala",
|
"scale": "Escala",
|
||||||
"save": "Desar",
|
"save": "",
|
||||||
"saveAs": "Desar com",
|
"saveAs": "Desar com",
|
||||||
"load": "Carregar",
|
"load": "Carregar",
|
||||||
"getShareableLink": "Obtenir enllaç per compartir",
|
"getShareableLink": "Obtenir enllaç per compartir",
|
||||||
@@ -142,6 +149,8 @@
|
|||||||
"loadSceneOverridePrompt": "Si carregas aquest dibuix extern, substituirá el que tens. Vols continuar?",
|
"loadSceneOverridePrompt": "Si carregas aquest dibuix extern, substituirá el que tens. Vols continuar?",
|
||||||
"collabStopOverridePrompt": "Aturar la sessió provocarà la sobreescriptura del dibuix previ, que hi ha desat en l'emmagatzematge local. N'esteu segur?\n\n(Si voleu conservar el dibuix local, tanqueu la pentanya del navegador en comptes d'aturar la sessió).",
|
"collabStopOverridePrompt": "Aturar la sessió provocarà la sobreescriptura del dibuix previ, que hi ha desat en l'emmagatzematge local. N'esteu segur?\n\n(Si voleu conservar el dibuix local, tanqueu la pentanya del navegador en comptes d'aturar la sessió).",
|
||||||
"errorLoadingLibrary": "S'ha produït un error en carregar la biblioteca de tercers.",
|
"errorLoadingLibrary": "S'ha produït un error en carregar la biblioteca de tercers.",
|
||||||
|
"errorAddingToLibrary": "",
|
||||||
|
"errorRemovingFromLibrary": "",
|
||||||
"confirmAddLibrary": "Això afegirà {{numShapes}} forma(es) a la vostra biblioteca. Estàs segur?",
|
"confirmAddLibrary": "Això afegirà {{numShapes}} forma(es) a la vostra biblioteca. Estàs segur?",
|
||||||
"imageDoesNotContainScene": "En aquest moment no s’admet la importació d’imatges.\n\nVolies importar una escena? Sembla que aquesta imatge no conté cap dada d’escena. Ho has activat durant l'exportació?",
|
"imageDoesNotContainScene": "En aquest moment no s’admet la importació d’imatges.\n\nVolies importar una escena? Sembla que aquesta imatge no conté cap dada d’escena. Ho has activat durant l'exportació?",
|
||||||
"cannotRestoreFromImage": "L’escena no s’ha pogut restaurar des d’aquest fitxer d’imatge",
|
"cannotRestoreFromImage": "L’escena no s’ha pogut restaurar des d’aquest fitxer d’imatge",
|
||||||
@@ -150,12 +159,12 @@
|
|||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "Selecció",
|
"selection": "Selecció",
|
||||||
"draw": "Dibuix lliure",
|
|
||||||
"rectangle": "Rectangle",
|
"rectangle": "Rectangle",
|
||||||
"diamond": "Rombe",
|
"diamond": "Rombe",
|
||||||
"ellipse": "El·lipse",
|
"ellipse": "El·lipse",
|
||||||
"arrow": "Fletxa",
|
"arrow": "Fletxa",
|
||||||
"line": "Línia",
|
"line": "Línia",
|
||||||
|
"freedraw": "Dibuix",
|
||||||
"text": "Text",
|
"text": "Text",
|
||||||
"library": "Biblioteca",
|
"library": "Biblioteca",
|
||||||
"lock": "Mantenir activa l'eina seleccionada desprès de dibuixar"
|
"lock": "Mantenir activa l'eina seleccionada desprès de dibuixar"
|
||||||
@@ -208,6 +217,14 @@
|
|||||||
"errorDialog": {
|
"errorDialog": {
|
||||||
"title": "Error"
|
"title": "Error"
|
||||||
},
|
},
|
||||||
|
"exportDialog": {
|
||||||
|
"disk_title": "",
|
||||||
|
"disk_details": "",
|
||||||
|
"disk_button": "",
|
||||||
|
"link_title": "",
|
||||||
|
"link_details": "",
|
||||||
|
"link_button": ""
|
||||||
|
},
|
||||||
"helpDialog": {
|
"helpDialog": {
|
||||||
"blog": "Llegiu el nostre blog",
|
"blog": "Llegiu el nostre blog",
|
||||||
"click": "clic",
|
"click": "clic",
|
||||||
@@ -231,7 +248,7 @@
|
|||||||
},
|
},
|
||||||
"encrypted": {
|
"encrypted": {
|
||||||
"tooltip": "Els vostres dibuixos estan xifrats de punta a punta de manera que els servidors d’Excalidraw no els veuran mai.",
|
"tooltip": "Els vostres dibuixos estan xifrats de punta a punta de manera que els servidors d’Excalidraw no els veuran mai.",
|
||||||
"link": ""
|
"link": "Article del blog sobre encriptació d'extrem a extrem a Excalidraw"
|
||||||
},
|
},
|
||||||
"stats": {
|
"stats": {
|
||||||
"angle": "Angle",
|
"angle": "Angle",
|
||||||
|
|||||||
@@ -0,0 +1,277 @@
|
|||||||
|
{
|
||||||
|
"labels": {
|
||||||
|
"paste": "Vložit",
|
||||||
|
"pasteCharts": "Vložit grafy",
|
||||||
|
"selectAll": "Vybrat vše",
|
||||||
|
"multiSelect": "Přidat prvek do výběru",
|
||||||
|
"moveCanvas": "Posunout plátno",
|
||||||
|
"cut": "Vyjmout",
|
||||||
|
"copy": "Kopírovat",
|
||||||
|
"copyAsPng": "Zkopírovat do schránky jako PNG",
|
||||||
|
"copyAsSvg": "Zkopírovat do schránky jako SVG",
|
||||||
|
"bringForward": "Přenést blíž",
|
||||||
|
"sendToBack": "Přenést do pozadí",
|
||||||
|
"bringToFront": "Přenést do popředí",
|
||||||
|
"sendBackward": "Přenést dál",
|
||||||
|
"delete": "Smazat",
|
||||||
|
"copyStyles": "Kopírovat styly",
|
||||||
|
"pasteStyles": "Vložit styly",
|
||||||
|
"stroke": "Obrys",
|
||||||
|
"background": "Pozadí",
|
||||||
|
"fill": "Výplň",
|
||||||
|
"strokeWidth": "Šířka obrysu",
|
||||||
|
"strokeShape": "Tvar tahu",
|
||||||
|
"strokeShape_gel": "Gelové pero",
|
||||||
|
"strokeShape_fountain": "Plnicí pero",
|
||||||
|
"strokeShape_brush": "Fixa",
|
||||||
|
"strokeStyle": "Styl tahu",
|
||||||
|
"strokeStyle_solid": "Plný",
|
||||||
|
"strokeStyle_dashed": "Čárkovaný",
|
||||||
|
"strokeStyle_dotted": "Tečkovaný",
|
||||||
|
"sloppiness": "Stylizace",
|
||||||
|
"opacity": "Průhlednost",
|
||||||
|
"textAlign": "Zarovnání textu",
|
||||||
|
"edges": "Hrany",
|
||||||
|
"sharp": "Ostré",
|
||||||
|
"round": "Zaoblené",
|
||||||
|
"arrowheads": "Styl šipky",
|
||||||
|
"arrowhead_none": "Žádný",
|
||||||
|
"arrowhead_arrow": "Šipka",
|
||||||
|
"arrowhead_bar": "Kóta",
|
||||||
|
"arrowhead_dot": "Tečka",
|
||||||
|
"fontSize": "Velikost písma",
|
||||||
|
"fontFamily": "Písmo",
|
||||||
|
"onlySelected": "Pouze vybrané",
|
||||||
|
"withBackground": "",
|
||||||
|
"exportEmbedScene": "",
|
||||||
|
"exportEmbedScene_details": "",
|
||||||
|
"addWatermark": "",
|
||||||
|
"handDrawn": "Od ruky",
|
||||||
|
"normal": "Normální",
|
||||||
|
"code": "Kód",
|
||||||
|
"small": "Malé",
|
||||||
|
"medium": "Střední",
|
||||||
|
"large": "Velké",
|
||||||
|
"veryLarge": "Velmi velké",
|
||||||
|
"solid": "",
|
||||||
|
"hachure": "",
|
||||||
|
"crossHatch": "",
|
||||||
|
"thin": "",
|
||||||
|
"bold": "",
|
||||||
|
"left": "",
|
||||||
|
"center": "",
|
||||||
|
"right": "",
|
||||||
|
"extraBold": "",
|
||||||
|
"architect": "",
|
||||||
|
"artist": "",
|
||||||
|
"cartoonist": "",
|
||||||
|
"fileTitle": "",
|
||||||
|
"colorPicker": "",
|
||||||
|
"canvasBackground": "Pozadí plátna",
|
||||||
|
"drawingCanvas": "",
|
||||||
|
"layers": "",
|
||||||
|
"actions": "",
|
||||||
|
"language": "",
|
||||||
|
"liveCollaboration": "",
|
||||||
|
"duplicateSelection": "",
|
||||||
|
"untitled": "",
|
||||||
|
"name": "",
|
||||||
|
"yourName": "",
|
||||||
|
"madeWithExcalidraw": "",
|
||||||
|
"group": "",
|
||||||
|
"ungroup": "",
|
||||||
|
"collaborators": "",
|
||||||
|
"showGrid": "",
|
||||||
|
"addToLibrary": "",
|
||||||
|
"removeFromLibrary": "",
|
||||||
|
"libraryLoadingMessage": "",
|
||||||
|
"libraries": "",
|
||||||
|
"loadingScene": "",
|
||||||
|
"align": "",
|
||||||
|
"alignTop": "",
|
||||||
|
"alignBottom": "",
|
||||||
|
"alignLeft": "",
|
||||||
|
"alignRight": "",
|
||||||
|
"centerVertically": "",
|
||||||
|
"centerHorizontally": "",
|
||||||
|
"distributeHorizontally": "",
|
||||||
|
"distributeVertically": "",
|
||||||
|
"flipHorizontal": "",
|
||||||
|
"flipVertical": "",
|
||||||
|
"viewMode": "Náhled",
|
||||||
|
"toggleExportColorScheme": "",
|
||||||
|
"share": "Sdílet",
|
||||||
|
"toggleTheme": "Přepnout tmavý řežim"
|
||||||
|
},
|
||||||
|
"buttons": {
|
||||||
|
"clearReset": "",
|
||||||
|
"exportJSON": "",
|
||||||
|
"exportImage": "",
|
||||||
|
"export": "Exportovat",
|
||||||
|
"exportToPng": "Exportovat do PNG",
|
||||||
|
"exportToSvg": "Exportovat do SVG",
|
||||||
|
"copyToClipboard": "Kopírovat do schránky",
|
||||||
|
"copyPngToClipboard": "Kopírovat PNG do schránky",
|
||||||
|
"scale": "Měřítko",
|
||||||
|
"save": "",
|
||||||
|
"saveAs": "Uložit jako",
|
||||||
|
"load": "Nahrát",
|
||||||
|
"getShareableLink": "Získat odkaz pro sdílení",
|
||||||
|
"close": "Zavřít",
|
||||||
|
"selectLanguage": "Zvolit jazyk",
|
||||||
|
"scrollBackToContent": "",
|
||||||
|
"zoomIn": "Přiblížit",
|
||||||
|
"zoomOut": "Oddálit",
|
||||||
|
"resetZoom": "Resetovat přiblížení",
|
||||||
|
"menu": "Menu",
|
||||||
|
"done": "Hotovo",
|
||||||
|
"edit": "Upravit",
|
||||||
|
"undo": "Zpět",
|
||||||
|
"redo": "Znovu",
|
||||||
|
"resetLibrary": "",
|
||||||
|
"createNewRoom": "Vytvořit novou místnost",
|
||||||
|
"fullScreen": "Celá obrazovka",
|
||||||
|
"darkMode": "Tmavý režim",
|
||||||
|
"lightMode": "Světlý režim",
|
||||||
|
"zenMode": "Zen mód",
|
||||||
|
"exitZenMode": "Opustit zen mód"
|
||||||
|
},
|
||||||
|
"alerts": {
|
||||||
|
"clearReset": "",
|
||||||
|
"couldNotCreateShareableLink": "",
|
||||||
|
"couldNotCreateShareableLinkTooBig": "",
|
||||||
|
"couldNotLoadInvalidFile": "",
|
||||||
|
"importBackendFailed": "",
|
||||||
|
"cannotExportEmptyCanvas": "",
|
||||||
|
"couldNotCopyToClipboard": "",
|
||||||
|
"decryptFailed": "",
|
||||||
|
"uploadedSecurly": "",
|
||||||
|
"loadSceneOverridePrompt": "",
|
||||||
|
"collabStopOverridePrompt": "",
|
||||||
|
"errorLoadingLibrary": "",
|
||||||
|
"errorAddingToLibrary": "",
|
||||||
|
"errorRemovingFromLibrary": "",
|
||||||
|
"confirmAddLibrary": "",
|
||||||
|
"imageDoesNotContainScene": "",
|
||||||
|
"cannotRestoreFromImage": "",
|
||||||
|
"invalidSceneUrl": "",
|
||||||
|
"resetLibrary": ""
|
||||||
|
},
|
||||||
|
"toolBar": {
|
||||||
|
"selection": "Výběr",
|
||||||
|
"rectangle": "Obdélník",
|
||||||
|
"diamond": "Diamant",
|
||||||
|
"ellipse": "Elipsa",
|
||||||
|
"arrow": "Šipka",
|
||||||
|
"line": "Čára",
|
||||||
|
"freedraw": "Kreslení",
|
||||||
|
"text": "Text",
|
||||||
|
"library": "",
|
||||||
|
"lock": ""
|
||||||
|
},
|
||||||
|
"headings": {
|
||||||
|
"canvasActions": "",
|
||||||
|
"selectedShapeActions": "",
|
||||||
|
"shapes": "Tvary"
|
||||||
|
},
|
||||||
|
"hints": {
|
||||||
|
"linearElement": "",
|
||||||
|
"freeDraw": "",
|
||||||
|
"text": "",
|
||||||
|
"linearElementMulti": "",
|
||||||
|
"lockAngle": "",
|
||||||
|
"resize": "",
|
||||||
|
"rotate": "",
|
||||||
|
"lineEditor_info": "",
|
||||||
|
"lineEditor_pointSelected": "",
|
||||||
|
"lineEditor_nothingSelected": ""
|
||||||
|
},
|
||||||
|
"canvasError": {
|
||||||
|
"cannotShowPreview": "",
|
||||||
|
"canvasTooBig": "",
|
||||||
|
"canvasTooBigTip": ""
|
||||||
|
},
|
||||||
|
"errorSplash": {
|
||||||
|
"headingMain_pre": "",
|
||||||
|
"headingMain_button": "",
|
||||||
|
"clearCanvasMessage": "",
|
||||||
|
"clearCanvasMessage_button": "",
|
||||||
|
"clearCanvasCaveat": "",
|
||||||
|
"trackedToSentry_pre": "",
|
||||||
|
"trackedToSentry_post": "",
|
||||||
|
"openIssueMessage_pre": "",
|
||||||
|
"openIssueMessage_button": "",
|
||||||
|
"openIssueMessage_post": "",
|
||||||
|
"sceneContent": ""
|
||||||
|
},
|
||||||
|
"roomDialog": {
|
||||||
|
"desc_intro": "",
|
||||||
|
"desc_privacy": "",
|
||||||
|
"button_startSession": "",
|
||||||
|
"button_stopSession": "",
|
||||||
|
"desc_inProgressIntro": "",
|
||||||
|
"desc_shareLink": "",
|
||||||
|
"desc_exitSession": "",
|
||||||
|
"shareTitle": ""
|
||||||
|
},
|
||||||
|
"errorDialog": {
|
||||||
|
"title": ""
|
||||||
|
},
|
||||||
|
"exportDialog": {
|
||||||
|
"disk_title": "",
|
||||||
|
"disk_details": "",
|
||||||
|
"disk_button": "",
|
||||||
|
"link_title": "",
|
||||||
|
"link_details": "",
|
||||||
|
"link_button": ""
|
||||||
|
},
|
||||||
|
"helpDialog": {
|
||||||
|
"blog": "",
|
||||||
|
"click": "kliknutí",
|
||||||
|
"curvedArrow": "",
|
||||||
|
"curvedLine": "",
|
||||||
|
"documentation": "",
|
||||||
|
"drag": "tažení",
|
||||||
|
"editor": "",
|
||||||
|
"github": "",
|
||||||
|
"howto": "",
|
||||||
|
"or": "nebo",
|
||||||
|
"preventBinding": "",
|
||||||
|
"shapes": "",
|
||||||
|
"shortcuts": "",
|
||||||
|
"textFinish": "",
|
||||||
|
"textNewLine": "",
|
||||||
|
"title": "",
|
||||||
|
"view": "",
|
||||||
|
"zoomToFit": "",
|
||||||
|
"zoomToSelection": ""
|
||||||
|
},
|
||||||
|
"encrypted": {
|
||||||
|
"tooltip": "",
|
||||||
|
"link": ""
|
||||||
|
},
|
||||||
|
"stats": {
|
||||||
|
"angle": "",
|
||||||
|
"element": "",
|
||||||
|
"elements": "",
|
||||||
|
"height": "",
|
||||||
|
"scene": "",
|
||||||
|
"selected": "",
|
||||||
|
"storage": "",
|
||||||
|
"title": "",
|
||||||
|
"total": "",
|
||||||
|
"version": "",
|
||||||
|
"versionCopy": "",
|
||||||
|
"versionNotAvailable": "",
|
||||||
|
"width": ""
|
||||||
|
},
|
||||||
|
"toast": {
|
||||||
|
"copyStyles": "",
|
||||||
|
"copyToClipboard": "",
|
||||||
|
"copyToClipboardAsPng": "",
|
||||||
|
"fileSaved": "",
|
||||||
|
"fileSavedToFilename": "",
|
||||||
|
"canvas": "plátno",
|
||||||
|
"selection": "výběr"
|
||||||
|
}
|
||||||
|
}
|
||||||
+33
-16
@@ -20,6 +20,10 @@
|
|||||||
"background": "Hintergrund",
|
"background": "Hintergrund",
|
||||||
"fill": "Füllung",
|
"fill": "Füllung",
|
||||||
"strokeWidth": "Strichstärke",
|
"strokeWidth": "Strichstärke",
|
||||||
|
"strokeShape": "Strichform",
|
||||||
|
"strokeShape_gel": "Gelschreiber",
|
||||||
|
"strokeShape_fountain": "Füllfederhalter",
|
||||||
|
"strokeShape_brush": "Pinselstift",
|
||||||
"strokeStyle": "Konturstil",
|
"strokeStyle": "Konturstil",
|
||||||
"strokeStyle_solid": "Durchgezogen",
|
"strokeStyle_solid": "Durchgezogen",
|
||||||
"strokeStyle_dashed": "Gestrichelt",
|
"strokeStyle_dashed": "Gestrichelt",
|
||||||
@@ -28,7 +32,7 @@
|
|||||||
"opacity": "Deckkraft",
|
"opacity": "Deckkraft",
|
||||||
"textAlign": "Textausrichtung",
|
"textAlign": "Textausrichtung",
|
||||||
"edges": "Kanten",
|
"edges": "Kanten",
|
||||||
"sharp": "Eckig",
|
"sharp": "Scharf",
|
||||||
"round": "Rund",
|
"round": "Rund",
|
||||||
"arrowheads": "Pfeilspitzen",
|
"arrowheads": "Pfeilspitzen",
|
||||||
"arrowhead_none": "Keine",
|
"arrowhead_none": "Keine",
|
||||||
@@ -38,8 +42,8 @@
|
|||||||
"fontSize": "Schriftgröße",
|
"fontSize": "Schriftgröße",
|
||||||
"fontFamily": "Schriftfamilie",
|
"fontFamily": "Schriftfamilie",
|
||||||
"onlySelected": "Nur ausgewählte",
|
"onlySelected": "Nur ausgewählte",
|
||||||
"withBackground": "Mit Hintergrund",
|
"withBackground": "Hintergrund",
|
||||||
"exportEmbedScene": "Zeichnung in exportierte Datei einbetten",
|
"exportEmbedScene": "Szene einbetten",
|
||||||
"exportEmbedScene_details": "Die Zeichnungsdaten werden in der exportierten PNG/SVG-Datei gespeichert, sodass das Dokument später weiter bearbeitet werden kann. \nDieses wird die exportierte Datei vergrößern.",
|
"exportEmbedScene_details": "Die Zeichnungsdaten werden in der exportierten PNG/SVG-Datei gespeichert, sodass das Dokument später weiter bearbeitet werden kann. \nDieses wird die exportierte Datei vergrößern.",
|
||||||
"addWatermark": "\"Made with Excalidraw\" hinzufügen",
|
"addWatermark": "\"Made with Excalidraw\" hinzufügen",
|
||||||
"handDrawn": "Handgezeichnet",
|
"handDrawn": "Handgezeichnet",
|
||||||
@@ -76,7 +80,7 @@
|
|||||||
"madeWithExcalidraw": "Made with Excalidraw",
|
"madeWithExcalidraw": "Made with Excalidraw",
|
||||||
"group": "Auswahl gruppieren",
|
"group": "Auswahl gruppieren",
|
||||||
"ungroup": "Gruppierung aufheben",
|
"ungroup": "Gruppierung aufheben",
|
||||||
"collaborators": "Kollaboratoren",
|
"collaborators": "Mitarbeitende",
|
||||||
"showGrid": "Raster anzeigen",
|
"showGrid": "Raster anzeigen",
|
||||||
"addToLibrary": "Zur Bibliothek hinzufügen",
|
"addToLibrary": "Zur Bibliothek hinzufügen",
|
||||||
"removeFromLibrary": "Aus Bibliothek entfernen",
|
"removeFromLibrary": "Aus Bibliothek entfernen",
|
||||||
@@ -95,18 +99,21 @@
|
|||||||
"flipHorizontal": "Horizontal spiegeln",
|
"flipHorizontal": "Horizontal spiegeln",
|
||||||
"flipVertical": "Vertikal spiegeln",
|
"flipVertical": "Vertikal spiegeln",
|
||||||
"viewMode": "Ansichtsmodus",
|
"viewMode": "Ansichtsmodus",
|
||||||
"toggleExportColorScheme": "Farbschema für Export umschalten",
|
"toggleExportColorScheme": "Exportfarbschema umschalten",
|
||||||
"share": "Teilen"
|
"share": "Teilen",
|
||||||
|
"toggleTheme": "Design umschalten"
|
||||||
},
|
},
|
||||||
"buttons": {
|
"buttons": {
|
||||||
"clearReset": "Zeichenfläche löschen & Hintergrundfarbe zurücksetzen",
|
"clearReset": "Zeichenfläche löschen & Hintergrundfarbe zurücksetzen",
|
||||||
|
"exportJSON": "In Datei exportieren",
|
||||||
|
"exportImage": "Als Bild speichern",
|
||||||
"export": "Exportieren",
|
"export": "Exportieren",
|
||||||
"exportToPng": "Als PNG exportieren",
|
"exportToPng": "Als PNG exportieren",
|
||||||
"exportToSvg": "Als SVG exportieren",
|
"exportToSvg": "Als SVG exportieren",
|
||||||
"copyToClipboard": "In Zwischenablage kopieren",
|
"copyToClipboard": "In Zwischenablage kopieren",
|
||||||
"copyPngToClipboard": "PNG in die Zwischenablage kopieren",
|
"copyPngToClipboard": "PNG in die Zwischenablage kopieren",
|
||||||
"scale": "Skalierung",
|
"scale": "Skalierung",
|
||||||
"save": "Speichern",
|
"save": "In aktueller Datei speichern",
|
||||||
"saveAs": "Speichern unter",
|
"saveAs": "Speichern unter",
|
||||||
"load": "Laden",
|
"load": "Laden",
|
||||||
"getShareableLink": "Teilbaren Link erhalten",
|
"getShareableLink": "Teilbaren Link erhalten",
|
||||||
@@ -124,8 +131,8 @@
|
|||||||
"resetLibrary": "Bibliothek zurücksetzen",
|
"resetLibrary": "Bibliothek zurücksetzen",
|
||||||
"createNewRoom": "Neuen Raum erstellen",
|
"createNewRoom": "Neuen Raum erstellen",
|
||||||
"fullScreen": "Vollbildanzeige",
|
"fullScreen": "Vollbildanzeige",
|
||||||
"darkMode": "Dunkles Design",
|
"darkMode": "Dunkler Modus",
|
||||||
"lightMode": "Helles Design",
|
"lightMode": "Heller Modus",
|
||||||
"zenMode": "Zen-Modus",
|
"zenMode": "Zen-Modus",
|
||||||
"exitZenMode": "Zen-Modus verlassen"
|
"exitZenMode": "Zen-Modus verlassen"
|
||||||
},
|
},
|
||||||
@@ -139,10 +146,12 @@
|
|||||||
"couldNotCopyToClipboard": "Konnte nicht in die Zwischenablage kopieren. Versuch es mit dem Chrome Browser.",
|
"couldNotCopyToClipboard": "Konnte nicht in die Zwischenablage kopieren. Versuch es mit dem Chrome Browser.",
|
||||||
"decryptFailed": "Daten konnten nicht entschlüsselt werden.",
|
"decryptFailed": "Daten konnten nicht entschlüsselt werden.",
|
||||||
"uploadedSecurly": "Der Upload wurde mit Ende-zu-Ende-Verschlüsselung gespeichert. Weder Excalidraw noch Dritte können den Inhalt einsehen.",
|
"uploadedSecurly": "Der Upload wurde mit Ende-zu-Ende-Verschlüsselung gespeichert. Weder Excalidraw noch Dritte können den Inhalt einsehen.",
|
||||||
"loadSceneOverridePrompt": "Das Laden der externen Zeichnung ersetzt den vorhandenen Inhalt. Möchtest Du fortfahren?",
|
"loadSceneOverridePrompt": "Das Laden einer externen Zeichnung ersetzt den vorhandenen Inhalt. Möchtest du fortfahren?",
|
||||||
"collabStopOverridePrompt": "Das Stoppen der Sitzung wird Deine vorherige, lokal gespeicherte Zeichnung überschreiben. Bist Du sicher?\n\n(Wenn Du Deine lokale Zeichnung behalten möchtest, schließe einfach stattdessen den Browser-Tab.)",
|
"collabStopOverridePrompt": "Das Stoppen der Sitzung wird deine vorherige, lokal gespeicherte Zeichnung überschreiben. Bist du dir sicher?\n\n(Wenn du deine lokale Zeichnung behalten möchtest, schließe stattdessen den Browser-Tab.)",
|
||||||
"errorLoadingLibrary": "Beim Laden der Drittanbieter-Bibliothek ist ein Fehler aufgetreten.",
|
"errorLoadingLibrary": "Beim Laden der Drittanbieter-Bibliothek ist ein Fehler aufgetreten.",
|
||||||
"confirmAddLibrary": "Dieses fügt {{numShapes}} Form(en) zu deiner Bibliothek hinzu. Bist du sicher?",
|
"errorAddingToLibrary": "Das Element konnte nicht zur Bibliothek hinzugefügt werden",
|
||||||
|
"errorRemovingFromLibrary": "Das Element konnte nicht aus der Bibliothek entfernt werden",
|
||||||
|
"confirmAddLibrary": "Dies fügt {{numShapes}} Form(en) zu deiner Bibliothek hinzu. Bist du dir sicher?",
|
||||||
"imageDoesNotContainScene": "Das Importieren von Bildern wird derzeit nicht unterstützt.\n\nMöchtest du eine Szene importieren? Dieses Bild scheint keine Zeichnungsdaten zu enthalten. Hast du dies beim Exportieren aktiviert?",
|
"imageDoesNotContainScene": "Das Importieren von Bildern wird derzeit nicht unterstützt.\n\nMöchtest du eine Szene importieren? Dieses Bild scheint keine Zeichnungsdaten zu enthalten. Hast du dies beim Exportieren aktiviert?",
|
||||||
"cannotRestoreFromImage": "Die Zeichnung konnte aus dieser Bilddatei nicht wiederhergestellt werden",
|
"cannotRestoreFromImage": "Die Zeichnung konnte aus dieser Bilddatei nicht wiederhergestellt werden",
|
||||||
"invalidSceneUrl": "Die Szene konnte nicht von der angegebenen URL importiert werden. Sie ist entweder fehlerhaft oder enthält keine gültigen Excalidraw JSON-Daten.",
|
"invalidSceneUrl": "Die Szene konnte nicht von der angegebenen URL importiert werden. Sie ist entweder fehlerhaft oder enthält keine gültigen Excalidraw JSON-Daten.",
|
||||||
@@ -150,12 +159,12 @@
|
|||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "Auswahl",
|
"selection": "Auswahl",
|
||||||
"draw": "Freies Zeichnen",
|
|
||||||
"rectangle": "Rechteck",
|
"rectangle": "Rechteck",
|
||||||
"diamond": "Raute",
|
"diamond": "Raute",
|
||||||
"ellipse": "Ellipse",
|
"ellipse": "Ellipse",
|
||||||
"arrow": "Pfeil",
|
"arrow": "Pfeil",
|
||||||
"line": "Linie",
|
"line": "Linie",
|
||||||
|
"freedraw": "Zeichnen",
|
||||||
"text": "Text",
|
"text": "Text",
|
||||||
"library": "Bibliothek",
|
"library": "Bibliothek",
|
||||||
"lock": "Ausgewähltes Werkzeug nach Zeichnen aktiv lassen"
|
"lock": "Ausgewähltes Werkzeug nach Zeichnen aktiv lassen"
|
||||||
@@ -168,7 +177,7 @@
|
|||||||
"hints": {
|
"hints": {
|
||||||
"linearElement": "Klicken für Linie mit mehreren Punkten, Ziehen für einzelne Linie",
|
"linearElement": "Klicken für Linie mit mehreren Punkten, Ziehen für einzelne Linie",
|
||||||
"freeDraw": "Klicke und ziehe. Lass los, wenn du fertig bist",
|
"freeDraw": "Klicke und ziehe. Lass los, wenn du fertig bist",
|
||||||
"text": "Tipp: Du kannst auch Text hinzufügen indem Du mit dem Auswahlwerkzeug auf eine beliebige Stelle doppelklickst",
|
"text": "Tipp: Du kannst auch Text hinzufügen, indem du mit dem Auswahlwerkzeug auf eine beliebige Stelle doppelklickst",
|
||||||
"linearElementMulti": "Zum Beenden auf den letzten Punkt klicken oder Escape oder Eingabe drücken",
|
"linearElementMulti": "Zum Beenden auf den letzten Punkt klicken oder Escape oder Eingabe drücken",
|
||||||
"lockAngle": "Du kannst Winkel einschränken, indem du SHIFT gedrückt hältst",
|
"lockAngle": "Du kannst Winkel einschränken, indem du SHIFT gedrückt hältst",
|
||||||
"resize": "Du kannst die Proportionen einschränken, indem du SHIFT während der Größenänderung gedrückt hältst. Halte ALT gedrückt, um die Größe vom Zentrum aus zu ändern",
|
"resize": "Du kannst die Proportionen einschränken, indem du SHIFT während der Größenänderung gedrückt hältst. Halte ALT gedrückt, um die Größe vom Zentrum aus zu ändern",
|
||||||
@@ -208,6 +217,14 @@
|
|||||||
"errorDialog": {
|
"errorDialog": {
|
||||||
"title": "Fehler"
|
"title": "Fehler"
|
||||||
},
|
},
|
||||||
|
"exportDialog": {
|
||||||
|
"disk_title": "Auf Festplatte speichern",
|
||||||
|
"disk_details": "Exportiere die Zeichnungsdaten in eine Datei, die Du später importieren kannst.",
|
||||||
|
"disk_button": "In Datei speichern",
|
||||||
|
"link_title": "Teilbarer Link",
|
||||||
|
"link_details": "Als schreibgeschützten Link exportieren.",
|
||||||
|
"link_button": "Als Link exportieren"
|
||||||
|
},
|
||||||
"helpDialog": {
|
"helpDialog": {
|
||||||
"blog": "Lies unseren Blog",
|
"blog": "Lies unseren Blog",
|
||||||
"click": "klicken",
|
"click": "klicken",
|
||||||
@@ -249,12 +266,12 @@
|
|||||||
"width": "Breite"
|
"width": "Breite"
|
||||||
},
|
},
|
||||||
"toast": {
|
"toast": {
|
||||||
"copyStyles": "Formatierung kopiert.",
|
"copyStyles": "Formatierungen kopiert.",
|
||||||
"copyToClipboard": "In die Zwischenablage kopiert.",
|
"copyToClipboard": "In die Zwischenablage kopiert.",
|
||||||
"copyToClipboardAsPng": "{{exportSelection}} als PNG in die Zwischenablage kopiert\n({{exportColorScheme}})",
|
"copyToClipboardAsPng": "{{exportSelection}} als PNG in die Zwischenablage kopiert\n({{exportColorScheme}})",
|
||||||
"fileSaved": "Datei gespeichert.",
|
"fileSaved": "Datei gespeichert.",
|
||||||
"fileSavedToFilename": "Als {filename} gespeichert",
|
"fileSavedToFilename": "Als {filename} gespeichert",
|
||||||
"canvas": "Leinwand",
|
"canvas": "Zeichenfläche",
|
||||||
"selection": "Auswahl"
|
"selection": "Auswahl"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+22
-5
@@ -20,6 +20,10 @@
|
|||||||
"background": "Φόντο",
|
"background": "Φόντο",
|
||||||
"fill": "Γέμισμα",
|
"fill": "Γέμισμα",
|
||||||
"strokeWidth": "Πάχος μολυβιάς",
|
"strokeWidth": "Πάχος μολυβιάς",
|
||||||
|
"strokeShape": "",
|
||||||
|
"strokeShape_gel": "",
|
||||||
|
"strokeShape_fountain": "",
|
||||||
|
"strokeShape_brush": "",
|
||||||
"strokeStyle": "Στυλ περιγράμματος",
|
"strokeStyle": "Στυλ περιγράμματος",
|
||||||
"strokeStyle_solid": "Συμπαγής",
|
"strokeStyle_solid": "Συμπαγής",
|
||||||
"strokeStyle_dashed": "Διακεκομμένη με παύλες",
|
"strokeStyle_dashed": "Διακεκομμένη με παύλες",
|
||||||
@@ -38,8 +42,8 @@
|
|||||||
"fontSize": "Μέγεθος γραμματοσειράς",
|
"fontSize": "Μέγεθος γραμματοσειράς",
|
||||||
"fontFamily": "Γραμματοσειρά",
|
"fontFamily": "Γραμματοσειρά",
|
||||||
"onlySelected": "Μόνο τα Επιλεγμένα",
|
"onlySelected": "Μόνο τα Επιλεγμένα",
|
||||||
"withBackground": "Με φόντο",
|
"withBackground": "",
|
||||||
"exportEmbedScene": "Ενσωμάτωση της σκηνής στο αρχείο προς εξαγωγή",
|
"exportEmbedScene": "",
|
||||||
"exportEmbedScene_details": "Τα δεδομένα σκηνής θα αποθηκευτούν στο αρχείο PNG/SVG προς εξαγωγή ώστε η σκηνή να είναι δυνατό να αποκατασταθεί από αυτό.\nΘα αυξήσει το μέγεθος του αρχείου προς εξαγωγή.",
|
"exportEmbedScene_details": "Τα δεδομένα σκηνής θα αποθηκευτούν στο αρχείο PNG/SVG προς εξαγωγή ώστε η σκηνή να είναι δυνατό να αποκατασταθεί από αυτό.\nΘα αυξήσει το μέγεθος του αρχείου προς εξαγωγή.",
|
||||||
"addWatermark": "Προσθήκη \"Φτιαγμένο με Excalidraw\"",
|
"addWatermark": "Προσθήκη \"Φτιαγμένο με Excalidraw\"",
|
||||||
"handDrawn": "Σχεδιασμένο στο χέρι",
|
"handDrawn": "Σχεδιασμένο στο χέρι",
|
||||||
@@ -96,17 +100,20 @@
|
|||||||
"flipVertical": "Κατακόρυφη αναστροφή",
|
"flipVertical": "Κατακόρυφη αναστροφή",
|
||||||
"viewMode": "Λειτουργία προβολής",
|
"viewMode": "Λειτουργία προβολής",
|
||||||
"toggleExportColorScheme": "Εναλλαγή εξαγωγής θέματος χρωμάτων",
|
"toggleExportColorScheme": "Εναλλαγή εξαγωγής θέματος χρωμάτων",
|
||||||
"share": "Κοινοποίηση"
|
"share": "Κοινοποίηση",
|
||||||
|
"toggleTheme": ""
|
||||||
},
|
},
|
||||||
"buttons": {
|
"buttons": {
|
||||||
"clearReset": "Επαναφορά του καμβά",
|
"clearReset": "Επαναφορά του καμβά",
|
||||||
|
"exportJSON": "",
|
||||||
|
"exportImage": "",
|
||||||
"export": "Εξαγωγή",
|
"export": "Εξαγωγή",
|
||||||
"exportToPng": "Εξαγωγή σε PNG",
|
"exportToPng": "Εξαγωγή σε PNG",
|
||||||
"exportToSvg": "Εξαγωγή σε SVG",
|
"exportToSvg": "Εξαγωγή σε SVG",
|
||||||
"copyToClipboard": "Αντιγραφή στο πρόχειρο",
|
"copyToClipboard": "Αντιγραφή στο πρόχειρο",
|
||||||
"copyPngToClipboard": "Αντιγραφή PNG στο πρόχειρο",
|
"copyPngToClipboard": "Αντιγραφή PNG στο πρόχειρο",
|
||||||
"scale": "Κλίμακα",
|
"scale": "Κλίμακα",
|
||||||
"save": "Αποθήκευση",
|
"save": "",
|
||||||
"saveAs": "Αποθήκευση ως",
|
"saveAs": "Αποθήκευση ως",
|
||||||
"load": "Άνοιγμα",
|
"load": "Άνοιγμα",
|
||||||
"getShareableLink": "Δημόσιος σύνδεσμος",
|
"getShareableLink": "Δημόσιος σύνδεσμος",
|
||||||
@@ -142,6 +149,8 @@
|
|||||||
"loadSceneOverridePrompt": "Η φόρτωση εξωτερικού σχεδίου θα αντικαταστήσει το υπάρχον περιεχόμενο. Επιθυμείτε να συνεχίσετε;",
|
"loadSceneOverridePrompt": "Η φόρτωση εξωτερικού σχεδίου θα αντικαταστήσει το υπάρχον περιεχόμενο. Επιθυμείτε να συνεχίσετε;",
|
||||||
"collabStopOverridePrompt": "Η διακοπή της συνεδρίας θα αντικαταστήσει το προηγούμενο, τοπικά αποθηκευμένο σχέδιο. Είστε σίγουροι?\n\n(Αν θέλετε να διατηρήσετε το τοπικό σας σχέδιο, απλά κλείστε την καρτέλα του προγράμματος περιήγησης.)",
|
"collabStopOverridePrompt": "Η διακοπή της συνεδρίας θα αντικαταστήσει το προηγούμενο, τοπικά αποθηκευμένο σχέδιο. Είστε σίγουροι?\n\n(Αν θέλετε να διατηρήσετε το τοπικό σας σχέδιο, απλά κλείστε την καρτέλα του προγράμματος περιήγησης.)",
|
||||||
"errorLoadingLibrary": "Υπήρξε ένα σφάλμα κατά τη φόρτωση της βιβλιοθήκης τρίτου μέρους.",
|
"errorLoadingLibrary": "Υπήρξε ένα σφάλμα κατά τη φόρτωση της βιβλιοθήκης τρίτου μέρους.",
|
||||||
|
"errorAddingToLibrary": "",
|
||||||
|
"errorRemovingFromLibrary": "",
|
||||||
"confirmAddLibrary": "Αυτό θα προσθέσει {{numShapes}} σχήμα(τα) στη βιβλιοθήκη σας. Είστε σίγουροι;",
|
"confirmAddLibrary": "Αυτό θα προσθέσει {{numShapes}} σχήμα(τα) στη βιβλιοθήκη σας. Είστε σίγουροι;",
|
||||||
"imageDoesNotContainScene": "Η εισαγωγή εικόνων δεν υποστηρίζεται αυτή τη στιγμή.\n\nΜήπως θέλετε να εισαγάγετε μια σκηνή; Αυτή η εικόνα δεν φαίνεται να περιέχει δεδομένα σκηνής. Έχετε ενεργοποιήσει αυτό κατά την εξαγωγή;",
|
"imageDoesNotContainScene": "Η εισαγωγή εικόνων δεν υποστηρίζεται αυτή τη στιγμή.\n\nΜήπως θέλετε να εισαγάγετε μια σκηνή; Αυτή η εικόνα δεν φαίνεται να περιέχει δεδομένα σκηνής. Έχετε ενεργοποιήσει αυτό κατά την εξαγωγή;",
|
||||||
"cannotRestoreFromImage": "Η σκηνή δεν ήταν δυνατό να αποκατασταθεί από αυτό το αρχείο εικόνας",
|
"cannotRestoreFromImage": "Η σκηνή δεν ήταν δυνατό να αποκατασταθεί από αυτό το αρχείο εικόνας",
|
||||||
@@ -150,12 +159,12 @@
|
|||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "Επιλογή",
|
"selection": "Επιλογή",
|
||||||
"draw": "Ελεύθερο σχέδιο",
|
|
||||||
"rectangle": "Ορθογώνιο",
|
"rectangle": "Ορθογώνιο",
|
||||||
"diamond": "Ρόμβος",
|
"diamond": "Ρόμβος",
|
||||||
"ellipse": "Έλλειψη",
|
"ellipse": "Έλλειψη",
|
||||||
"arrow": "Βέλος",
|
"arrow": "Βέλος",
|
||||||
"line": "Γραμμή",
|
"line": "Γραμμή",
|
||||||
|
"freedraw": "",
|
||||||
"text": "Κείμενο",
|
"text": "Κείμενο",
|
||||||
"library": "Βιβλιοθήκη",
|
"library": "Βιβλιοθήκη",
|
||||||
"lock": "Κράτησε επιλεγμένο το εργαλείο μετά το σχέδιο"
|
"lock": "Κράτησε επιλεγμένο το εργαλείο μετά το σχέδιο"
|
||||||
@@ -208,6 +217,14 @@
|
|||||||
"errorDialog": {
|
"errorDialog": {
|
||||||
"title": "Σφάλμα"
|
"title": "Σφάλμα"
|
||||||
},
|
},
|
||||||
|
"exportDialog": {
|
||||||
|
"disk_title": "",
|
||||||
|
"disk_details": "",
|
||||||
|
"disk_button": "",
|
||||||
|
"link_title": "",
|
||||||
|
"link_details": "",
|
||||||
|
"link_button": ""
|
||||||
|
},
|
||||||
"helpDialog": {
|
"helpDialog": {
|
||||||
"blog": "Διαβάστε το Blog μας",
|
"blog": "Διαβάστε το Blog μας",
|
||||||
"click": "κλικ",
|
"click": "κλικ",
|
||||||
|
|||||||
+24
-5
@@ -20,6 +20,10 @@
|
|||||||
"background": "Background",
|
"background": "Background",
|
||||||
"fill": "Fill",
|
"fill": "Fill",
|
||||||
"strokeWidth": "Stroke width",
|
"strokeWidth": "Stroke width",
|
||||||
|
"strokeShape": "Stroke shape",
|
||||||
|
"strokeShape_gel": "Gel pen",
|
||||||
|
"strokeShape_fountain": "Fountain pen",
|
||||||
|
"strokeShape_brush": "Brush pen",
|
||||||
"strokeStyle": "Stroke style",
|
"strokeStyle": "Stroke style",
|
||||||
"strokeStyle_solid": "Solid",
|
"strokeStyle_solid": "Solid",
|
||||||
"strokeStyle_dashed": "Dashed",
|
"strokeStyle_dashed": "Dashed",
|
||||||
@@ -38,8 +42,8 @@
|
|||||||
"fontSize": "Font size",
|
"fontSize": "Font size",
|
||||||
"fontFamily": "Font family",
|
"fontFamily": "Font family",
|
||||||
"onlySelected": "Only selected",
|
"onlySelected": "Only selected",
|
||||||
"withBackground": "With background",
|
"withBackground": "Background",
|
||||||
"exportEmbedScene": "Embed scene into exported file",
|
"exportEmbedScene": "Embed scene",
|
||||||
"exportEmbedScene_details": "Scene data will be saved into the exported PNG/SVG file so that the scene can be restored from it.\nWill increase exported file size.",
|
"exportEmbedScene_details": "Scene data will be saved into the exported PNG/SVG file so that the scene can be restored from it.\nWill increase exported file size.",
|
||||||
"addWatermark": "Add \"Made with Excalidraw\"",
|
"addWatermark": "Add \"Made with Excalidraw\"",
|
||||||
"handDrawn": "Hand-drawn",
|
"handDrawn": "Hand-drawn",
|
||||||
@@ -96,17 +100,22 @@
|
|||||||
"flipVertical": "Flip vertical",
|
"flipVertical": "Flip vertical",
|
||||||
"viewMode": "View mode",
|
"viewMode": "View mode",
|
||||||
"toggleExportColorScheme": "Toggle export color scheme",
|
"toggleExportColorScheme": "Toggle export color scheme",
|
||||||
"share": "Share"
|
"share": "Share",
|
||||||
|
"showStroke": "Show stroke color picker",
|
||||||
|
"showBackground": "Show background color picker",
|
||||||
|
"toggleTheme": "Toggle theme"
|
||||||
},
|
},
|
||||||
"buttons": {
|
"buttons": {
|
||||||
"clearReset": "Reset the canvas",
|
"clearReset": "Reset the canvas",
|
||||||
|
"exportJSON": "Export to file",
|
||||||
|
"exportImage": "Save as image",
|
||||||
"export": "Export",
|
"export": "Export",
|
||||||
"exportToPng": "Export to PNG",
|
"exportToPng": "Export to PNG",
|
||||||
"exportToSvg": "Export to SVG",
|
"exportToSvg": "Export to SVG",
|
||||||
"copyToClipboard": "Copy to clipboard",
|
"copyToClipboard": "Copy to clipboard",
|
||||||
"copyPngToClipboard": "Copy PNG to clipboard",
|
"copyPngToClipboard": "Copy PNG to clipboard",
|
||||||
"scale": "Scale",
|
"scale": "Scale",
|
||||||
"save": "Save",
|
"save": "Save to current file",
|
||||||
"saveAs": "Save as",
|
"saveAs": "Save as",
|
||||||
"load": "Load",
|
"load": "Load",
|
||||||
"getShareableLink": "Get shareable link",
|
"getShareableLink": "Get shareable link",
|
||||||
@@ -142,6 +151,8 @@
|
|||||||
"loadSceneOverridePrompt": "Loading external drawing will replace your existing content. Do you wish to continue?",
|
"loadSceneOverridePrompt": "Loading external drawing will replace your existing content. Do you wish to continue?",
|
||||||
"collabStopOverridePrompt": "Stopping the session will overwrite your previous, locally stored drawing. Are you sure?\n\n(If you want to keep your local drawing, simply close the browser tab instead.)",
|
"collabStopOverridePrompt": "Stopping the session will overwrite your previous, locally stored drawing. Are you sure?\n\n(If you want to keep your local drawing, simply close the browser tab instead.)",
|
||||||
"errorLoadingLibrary": "There was an error loading the third party library.",
|
"errorLoadingLibrary": "There was an error loading the third party library.",
|
||||||
|
"errorAddingToLibrary": "Couldn't add item to the library",
|
||||||
|
"errorRemovingFromLibrary": "Couldn't remove item from the library",
|
||||||
"confirmAddLibrary": "This will add {{numShapes}} shape(s) to your library. Are you sure?",
|
"confirmAddLibrary": "This will add {{numShapes}} shape(s) to your library. Are you sure?",
|
||||||
"imageDoesNotContainScene": "Importing images isn't supported at the moment.\n\nDid you want to import a scene? This image does not seem to contain any scene data. Have you enabled this during export?",
|
"imageDoesNotContainScene": "Importing images isn't supported at the moment.\n\nDid you want to import a scene? This image does not seem to contain any scene data. Have you enabled this during export?",
|
||||||
"cannotRestoreFromImage": "Scene couldn't be restored from this image file",
|
"cannotRestoreFromImage": "Scene couldn't be restored from this image file",
|
||||||
@@ -150,12 +161,12 @@
|
|||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "Selection",
|
"selection": "Selection",
|
||||||
"draw": "Free draw",
|
|
||||||
"rectangle": "Rectangle",
|
"rectangle": "Rectangle",
|
||||||
"diamond": "Diamond",
|
"diamond": "Diamond",
|
||||||
"ellipse": "Ellipse",
|
"ellipse": "Ellipse",
|
||||||
"arrow": "Arrow",
|
"arrow": "Arrow",
|
||||||
"line": "Line",
|
"line": "Line",
|
||||||
|
"freedraw": "Draw",
|
||||||
"text": "Text",
|
"text": "Text",
|
||||||
"library": "Library",
|
"library": "Library",
|
||||||
"lock": "Keep selected tool active after drawing"
|
"lock": "Keep selected tool active after drawing"
|
||||||
@@ -208,6 +219,14 @@
|
|||||||
"errorDialog": {
|
"errorDialog": {
|
||||||
"title": "Error"
|
"title": "Error"
|
||||||
},
|
},
|
||||||
|
"exportDialog": {
|
||||||
|
"disk_title": "Save to disk",
|
||||||
|
"disk_details": "Export the scene data to a file from which you can import later.",
|
||||||
|
"disk_button": "Save to file",
|
||||||
|
"link_title": "Shareable link",
|
||||||
|
"link_details": "Export as a read-only link.",
|
||||||
|
"link_button": "Export to Link"
|
||||||
|
},
|
||||||
"helpDialog": {
|
"helpDialog": {
|
||||||
"blog": "Read our blog",
|
"blog": "Read our blog",
|
||||||
"click": "click",
|
"click": "click",
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user