Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 502cee7b7a | |||
| e57dc405fa | |||
| 41ed019bc2 | |||
| f7c3644342 | |||
| 5e3550fc14 | |||
| 70888327a3 | |||
| 9fc15d81a0 | |||
| a80ac4c748 | |||
| 9f76f8677b | |||
| 2e46e27490 | |||
| cf0413338e | |||
| 49e4289878 | |||
| 3ddcc48e4c | |||
| 29a5e982c3 | |||
| b33fa6d6f6 | |||
| b7350f9707 | |||
| 8dfa2a98bb | |||
| fb01ce2a00 | |||
| 3d57112480 | |||
| 7558a4e2be | |||
| 6d56634289 | |||
| 0aa1e66486 | |||
| 7f7128ec09 | |||
| 6de6a96abf | |||
| 28ab6531c9 |
+1
-5
@@ -20,14 +20,10 @@ REACT_APP_DEV_ENABLE_SW=
|
|||||||
# whether to disable live reload / HMR. Usuaully what you want to do when
|
# whether to disable live reload / HMR. Usuaully what you want to do when
|
||||||
# debugging Service Workers.
|
# debugging Service Workers.
|
||||||
REACT_APP_DEV_DISABLE_LIVE_RELOAD=
|
REACT_APP_DEV_DISABLE_LIVE_RELOAD=
|
||||||
|
REACT_APP_DISABLE_TRACKING=true
|
||||||
|
|
||||||
FAST_REFRESH=false
|
FAST_REFRESH=false
|
||||||
|
|
||||||
# MATOMO
|
|
||||||
REACT_APP_MATOMO_URL=
|
|
||||||
REACT_APP_CDN_MATOMO_TRACKER_URL=
|
|
||||||
REACT_APP_MATOMO_SITE_ID=
|
|
||||||
|
|
||||||
#Debug flags
|
#Debug flags
|
||||||
|
|
||||||
# To enable bounding box for text containers
|
# To enable bounding box for text containers
|
||||||
|
|||||||
+1
-10
@@ -11,14 +11,5 @@ REACT_APP_WS_SERVER_URL=
|
|||||||
|
|
||||||
REACT_APP_FIREBASE_CONFIG='{"apiKey":"AIzaSyAd15pYlMci_xIp9ko6wkEsDzAAA0Dn0RU","authDomain":"excalidraw-room-persistence.firebaseapp.com","databaseURL":"https://excalidraw-room-persistence.firebaseio.com","projectId":"excalidraw-room-persistence","storageBucket":"excalidraw-room-persistence.appspot.com","messagingSenderId":"654800341332","appId":"1:654800341332:web:4a692de832b55bd57ce0c1"}'
|
REACT_APP_FIREBASE_CONFIG='{"apiKey":"AIzaSyAd15pYlMci_xIp9ko6wkEsDzAAA0Dn0RU","authDomain":"excalidraw-room-persistence.firebaseapp.com","databaseURL":"https://excalidraw-room-persistence.firebaseio.com","projectId":"excalidraw-room-persistence","storageBucket":"excalidraw-room-persistence.appspot.com","messagingSenderId":"654800341332","appId":"1:654800341332:web:4a692de832b55bd57ce0c1"}'
|
||||||
|
|
||||||
# production-only vars
|
|
||||||
# GOOGLE ANALYTICS
|
|
||||||
REACT_APP_GOOGLE_ANALYTICS_ID=UA-387204-13
|
|
||||||
# MATOMO
|
|
||||||
REACT_APP_MATOMO_URL=https://excalidraw.matomo.cloud/
|
|
||||||
REACT_APP_CDN_MATOMO_TRACKER_URL=//cdn.matomo.cloud/excalidraw.matomo.cloud/matomo.js
|
|
||||||
REACT_APP_MATOMO_SITE_ID=1
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
REACT_APP_PLUS_APP=https://app.excalidraw.com
|
REACT_APP_PLUS_APP=https://app.excalidraw.com
|
||||||
|
REACT_APP_DISABLE_TRACKING=
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
name: "Bundle Size check @excalidraw/excalidraw"
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
jobs:
|
||||||
|
size:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
CI_JOB_NUMBER: 1
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
- name: Setup Node.js 18.x
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: 18.x
|
||||||
|
- name: Install
|
||||||
|
run: yarn --frozen-lockfile
|
||||||
|
- name: Install in src/packages/excalidraw
|
||||||
|
run: yarn --frozen-lockfile
|
||||||
|
working-directory: src/packages/excalidraw
|
||||||
|
env:
|
||||||
|
CI: true
|
||||||
|
- uses: andresz1/size-limit-action@v1
|
||||||
|
with:
|
||||||
|
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
build_script: build:umd
|
||||||
|
skip_step: install
|
||||||
|
directory: src/packages/excalidraw
|
||||||
@@ -165,3 +165,35 @@ function App() {
|
|||||||
| Prop | Type | Required | Default | Description |
|
| Prop | Type | Required | Default | Description |
|
||||||
| --- | --- | :-: | :-: | --- |
|
| --- | --- | :-: | :-: | --- |
|
||||||
| `children ` | `React.ReactNode` | Yes | - | The content of the `Menu Group` |
|
| `children ` | `React.ReactNode` | Yes | - | The content of the `Menu Group` |
|
||||||
|
|
||||||
|
### MainMenu.Sub
|
||||||
|
|
||||||
|
The MainMenu component now supports submenus. To render a submenu, you can use `MainMenu.Sub`, `MainMenu.Sub.Trigger`, `MainMenu.Sub.Content` and `MainMenu.Sub.Item`. Note that `MainMenu.Sub.Trigger` and `MainMenu.Sub.Content` must be direct children of `MainMenu.Sub`.
|
||||||
|
|
||||||
|
```jsx live
|
||||||
|
function App() {
|
||||||
|
return (
|
||||||
|
<div style={{ height: "500px" }}>
|
||||||
|
<Excalidraw>
|
||||||
|
<MainMenu>
|
||||||
|
<MainMenu.Sub>
|
||||||
|
<MainMenu.Sub.Trigger>Submenu</MainMenu.Sub.Trigger>
|
||||||
|
<MainMenu.Sub.Content>
|
||||||
|
<MainMenu.Sub.Item
|
||||||
|
onSelect={() => window.alert("Submenu item 1")}
|
||||||
|
>
|
||||||
|
Submenu item 1
|
||||||
|
</MainMenu.Sub.Item>
|
||||||
|
<MainMenu.Sub.Item
|
||||||
|
onSelect={() => window.alert("Submenu item 2")}
|
||||||
|
>
|
||||||
|
Submenu item 2
|
||||||
|
</MainMenu.Sub.Item>
|
||||||
|
</MainMenu.Sub.Content>
|
||||||
|
</MainMenu.Sub>
|
||||||
|
</MainMenu>
|
||||||
|
</Excalidraw>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|||||||
@@ -306,30 +306,32 @@ This is the history API. history.clear() will clear the history.
|
|||||||
|
|
||||||
## scrollToContent
|
## scrollToContent
|
||||||
|
|
||||||
<pre>
|
```tsx
|
||||||
(<br />
|
(
|
||||||
{" "}
|
target?: ExcalidrawElement | ExcalidrawElement[],
|
||||||
target?:{" "}
|
opts?:
|
||||||
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L115">
|
| {
|
||||||
ExcalidrawElement
|
fitToContent?: boolean;
|
||||||
</a>{" "}
|
animate?: boolean;
|
||||||
|{" "}
|
duration?: number;
|
||||||
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L115">
|
}
|
||||||
ExcalidrawElement
|
| {
|
||||||
</a>
|
fitToViewport?: boolean;
|
||||||
[],
|
viewportZoomFactor?: number;
|
||||||
<br />
|
animate?: boolean;
|
||||||
{" "}opts?: { fitToContent?: boolean; animate?: boolean; duration?: number
|
duration?: number;
|
||||||
}
|
}
|
||||||
<br />) => void
|
) => void
|
||||||
</pre>
|
```
|
||||||
|
|
||||||
Scroll the nearest element out of the elements supplied to the center of the viewport. Defaults to the elements on the scene.
|
Scroll the nearest element out of the elements supplied to the center of the viewport. Defaults to the elements on the scene.
|
||||||
|
|
||||||
| Attribute | type | default | Description |
|
| Attribute | type | default | Description |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| target | <code>ExcalidrawElement | ExcalidrawElement[]</code> | All scene elements | The element(s) to scroll to. |
|
| target | [ExcalidrawElement](https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L115) | [ExcalidrawElement[]](https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L115) | All scene elements | The element(s) to scroll to. |
|
||||||
| opts.fitToContent | boolean | false | Whether to fit the elements to viewport by automatically changing zoom as needed. |
|
| opts.fitToContent | boolean | false | Whether to fit the elements to viewport by automatically changing zoom as needed. Note that the zoom range is between 10%-100%. |
|
||||||
|
| opts.fitToViewport | boolean | false | Similar to fitToContent but the zoom range is not limited. If elements are smaller than the viewport, zoom will go above 100%. |
|
||||||
|
| opts.viewportZoomFactor | number | 0.7 | when fitToViewport=true, how much screen should the content cover, between 0.1 (10%) and 1 (100%) |
|
||||||
| opts.animate | boolean | false | Whether to animate between starting and ending position. Note that for larger scenes the animation may not be smooth due to performance issues. |
|
| opts.animate | boolean | false | Whether to animate between starting and ending position. Note that for larger scenes the animation may not be smooth due to performance issues. |
|
||||||
| opts.duration | number | 500 | Duration of the animation if `opts.animate` is `true`. |
|
| opts.duration | number | 500 | Duration of the animation if `opts.animate` is `true`. |
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,11 @@
|
|||||||
|
|
||||||
Pull requests are welcome. For major changes, please [open an issue](https://github.com/excalidraw/excalidraw/issues/new) first to discuss what you would like to change.
|
Pull requests are welcome. For major changes, please [open an issue](https://github.com/excalidraw/excalidraw/issues/new) first to discuss what you would like to change.
|
||||||
|
|
||||||
|
We have a [roadmap](https://github.com/orgs/excalidraw/projects/3) which we strongly recommend to go through and check if something interests you.
|
||||||
|
For new contributors we would recommend to start with *Easy* tasks.
|
||||||
|
|
||||||
|
In case you want to pick up something from the roadmap, comment on that issue and one of the project maintainers will assign it to you, post which you can discuss in the issue and start working on it.
|
||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
|
|
||||||
### Option 1 - Manual
|
### Option 1 - Manual
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@braintree/sanitize-url": "6.0.2",
|
||||||
"@excalidraw/random-username": "1.0.0",
|
"@excalidraw/random-username": "1.0.0",
|
||||||
"@radix-ui/react-popover": "1.0.3",
|
"@radix-ui/react-popover": "1.0.3",
|
||||||
"@radix-ui/react-tabs": "1.0.2",
|
"@radix-ui/react-tabs": "1.0.2",
|
||||||
|
|||||||
+33
-38
@@ -148,33 +148,6 @@
|
|||||||
// setting this so that libraries installation reuses this window tab.
|
// setting this so that libraries installation reuses this window tab.
|
||||||
window.name = "_excalidraw";
|
window.name = "_excalidraw";
|
||||||
</script>
|
</script>
|
||||||
<% if (process.env.REACT_APP_DISABLE_TRACKING !== 'true') { %>
|
|
||||||
|
|
||||||
<!-- Fathom - privacy-friendly analytics -->
|
|
||||||
<script
|
|
||||||
src="https://cdn.usefathom.com/script.js"
|
|
||||||
data-site="VMSBUEYA"
|
|
||||||
defer
|
|
||||||
></script>
|
|
||||||
<!-- / Fathom -->
|
|
||||||
|
|
||||||
<!-- LEGACY GOOGLE ANALYTICS -->
|
|
||||||
<% if (process.env.REACT_APP_GOOGLE_ANALYTICS_ID) { %>
|
|
||||||
<script
|
|
||||||
async
|
|
||||||
src="https://www.googletagmanager.com/gtag/js?id=%REACT_APP_GOOGLE_ANALYTICS_ID%"
|
|
||||||
></script>
|
|
||||||
<script>
|
|
||||||
window.dataLayer = window.dataLayer || [];
|
|
||||||
function gtag() {
|
|
||||||
dataLayer.push(arguments);
|
|
||||||
}
|
|
||||||
gtag("js", new Date());
|
|
||||||
gtag("config", "%REACT_APP_GOOGLE_ANALYTICS_ID%");
|
|
||||||
</script>
|
|
||||||
<% } %>
|
|
||||||
<!-- end LEGACY GOOGLE ANALYTICS -->
|
|
||||||
<% } %>
|
|
||||||
|
|
||||||
<!-- FIXME: remove this when we update CRA (fix SW caching) -->
|
<!-- FIXME: remove this when we update CRA (fix SW caching) -->
|
||||||
<style>
|
<style>
|
||||||
@@ -227,17 +200,39 @@
|
|||||||
<h1 class="visually-hidden">Excalidraw</h1>
|
<h1 class="visually-hidden">Excalidraw</h1>
|
||||||
</header>
|
</header>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
<% if (process.env.REACT_APP_DISABLE_TRACKING !== 'true') { %>
|
||||||
<!-- 100% privacy friendly analytics -->
|
<!-- 100% privacy friendly analytics -->
|
||||||
<script
|
<script>
|
||||||
async
|
// need to load this script dynamically bcs. of iframe embed tracking
|
||||||
defer
|
var scriptEle = document.createElement("script");
|
||||||
src="https://scripts.simpleanalyticscdn.com/latest.js"
|
scriptEle.setAttribute(
|
||||||
></script>
|
"src",
|
||||||
<noscript
|
"https://scripts.simpleanalyticscdn.com/latest.js",
|
||||||
><img
|
);
|
||||||
src="https://queue.simpleanalyticscdn.com/noscript.gif"
|
scriptEle.setAttribute("type", "text/javascript");
|
||||||
alt=""
|
scriptEle.setAttribute("defer", true);
|
||||||
referrerpolicy="no-referrer-when-downgrade"
|
scriptEle.setAttribute("async", true);
|
||||||
/></noscript>
|
// if iframe
|
||||||
|
if (window.self !== window.top) {
|
||||||
|
scriptEle.setAttribute("data-auto-collect", true);
|
||||||
|
}
|
||||||
|
|
||||||
|
document.body.appendChild(scriptEle);
|
||||||
|
|
||||||
|
// if iframe
|
||||||
|
if (window.self !== window.top) {
|
||||||
|
scriptEle.addEventListener("load", () => {
|
||||||
|
if (window.sa_pageview) {
|
||||||
|
window.window.sa_event(action, {
|
||||||
|
category: "iframe",
|
||||||
|
label: "embed",
|
||||||
|
value: window.location.pathname,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<!-- end LEGACY GOOGLE ANALYTICS -->
|
||||||
|
<% } %>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
import { getSelectedElements } from "../scene";
|
|
||||||
import { getNonDeletedElements } from "../element";
|
|
||||||
import { deepCopyElement } from "../element/newElement";
|
import { deepCopyElement } from "../element/newElement";
|
||||||
import { randomId } from "../random";
|
import { randomId } from "../random";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
@@ -9,14 +7,11 @@ export const actionAddToLibrary = register({
|
|||||||
name: "addToLibrary",
|
name: "addToLibrary",
|
||||||
trackEvent: { category: "element" },
|
trackEvent: { category: "element" },
|
||||||
perform: (elements, appState, _, app) => {
|
perform: (elements, appState, _, app) => {
|
||||||
const selectedElements = getSelectedElements(
|
const selectedElements = app.scene.getSelectedElements({
|
||||||
getNonDeletedElements(elements),
|
selectedElementIds: appState.selectedElementIds,
|
||||||
appState,
|
includeBoundTextElement: true,
|
||||||
{
|
includeElementsInFrames: true,
|
||||||
includeBoundTextElement: true,
|
});
|
||||||
includeElementsInFrames: true,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
if (selectedElements.some((element) => element.type === "image")) {
|
if (selectedElements.some((element) => element.type === "image")) {
|
||||||
return {
|
return {
|
||||||
commitToHistory: false,
|
commitToHistory: false,
|
||||||
|
|||||||
+32
-34
@@ -13,19 +13,18 @@ import { ExcalidrawElement } from "../element/types";
|
|||||||
import { updateFrameMembershipOfSelectedElements } from "../frame";
|
import { updateFrameMembershipOfSelectedElements } from "../frame";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { KEYS } from "../keys";
|
import { KEYS } from "../keys";
|
||||||
import { getSelectedElements, isSomeElementSelected } from "../scene";
|
import { isSomeElementSelected } from "../scene";
|
||||||
import { AppState } from "../types";
|
import { AppClassProperties, AppState } from "../types";
|
||||||
import { arrayToMap, getShortcutKey } from "../utils";
|
import { arrayToMap, getShortcutKey } from "../utils";
|
||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
|
|
||||||
const alignActionsPredicate = (
|
const alignActionsPredicate = (
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly ExcalidrawElement[],
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
|
_: unknown,
|
||||||
|
app: AppClassProperties,
|
||||||
) => {
|
) => {
|
||||||
const selectedElements = getSelectedElements(
|
const selectedElements = app.scene.getSelectedElements(appState);
|
||||||
getNonDeletedElements(elements),
|
|
||||||
appState,
|
|
||||||
);
|
|
||||||
return (
|
return (
|
||||||
selectedElements.length > 1 &&
|
selectedElements.length > 1 &&
|
||||||
// TODO enable aligning frames when implemented properly
|
// TODO enable aligning frames when implemented properly
|
||||||
@@ -36,12 +35,10 @@ const alignActionsPredicate = (
|
|||||||
const alignSelectedElements = (
|
const alignSelectedElements = (
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly ExcalidrawElement[],
|
||||||
appState: Readonly<AppState>,
|
appState: Readonly<AppState>,
|
||||||
|
app: AppClassProperties,
|
||||||
alignment: Alignment,
|
alignment: Alignment,
|
||||||
) => {
|
) => {
|
||||||
const selectedElements = getSelectedElements(
|
const selectedElements = app.scene.getSelectedElements(appState);
|
||||||
getNonDeletedElements(elements),
|
|
||||||
appState,
|
|
||||||
);
|
|
||||||
|
|
||||||
const updatedElements = alignElements(selectedElements, alignment);
|
const updatedElements = alignElements(selectedElements, alignment);
|
||||||
|
|
||||||
@@ -50,6 +47,7 @@ const alignSelectedElements = (
|
|||||||
return updateFrameMembershipOfSelectedElements(
|
return updateFrameMembershipOfSelectedElements(
|
||||||
elements.map((element) => updatedElementsMap.get(element.id) || element),
|
elements.map((element) => updatedElementsMap.get(element.id) || element),
|
||||||
appState,
|
appState,
|
||||||
|
app,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -57,10 +55,10 @@ export const actionAlignTop = register({
|
|||||||
name: "alignTop",
|
name: "alignTop",
|
||||||
trackEvent: { category: "element" },
|
trackEvent: { category: "element" },
|
||||||
predicate: alignActionsPredicate,
|
predicate: alignActionsPredicate,
|
||||||
perform: (elements, appState) => {
|
perform: (elements, appState, _, app) => {
|
||||||
return {
|
return {
|
||||||
appState,
|
appState,
|
||||||
elements: alignSelectedElements(elements, appState, {
|
elements: alignSelectedElements(elements, appState, app, {
|
||||||
position: "start",
|
position: "start",
|
||||||
axis: "y",
|
axis: "y",
|
||||||
}),
|
}),
|
||||||
@@ -69,9 +67,9 @@ export const actionAlignTop = register({
|
|||||||
},
|
},
|
||||||
keyTest: (event) =>
|
keyTest: (event) =>
|
||||||
event[KEYS.CTRL_OR_CMD] && event.shiftKey && event.key === KEYS.ARROW_UP,
|
event[KEYS.CTRL_OR_CMD] && event.shiftKey && event.key === KEYS.ARROW_UP,
|
||||||
PanelComponent: ({ elements, appState, updateData }) => (
|
PanelComponent: ({ elements, appState, updateData, app }) => (
|
||||||
<ToolButton
|
<ToolButton
|
||||||
hidden={!alignActionsPredicate(elements, appState)}
|
hidden={!alignActionsPredicate(elements, appState, null, app)}
|
||||||
type="button"
|
type="button"
|
||||||
icon={AlignTopIcon}
|
icon={AlignTopIcon}
|
||||||
onClick={() => updateData(null)}
|
onClick={() => updateData(null)}
|
||||||
@@ -88,10 +86,10 @@ export const actionAlignBottom = register({
|
|||||||
name: "alignBottom",
|
name: "alignBottom",
|
||||||
trackEvent: { category: "element" },
|
trackEvent: { category: "element" },
|
||||||
predicate: alignActionsPredicate,
|
predicate: alignActionsPredicate,
|
||||||
perform: (elements, appState) => {
|
perform: (elements, appState, _, app) => {
|
||||||
return {
|
return {
|
||||||
appState,
|
appState,
|
||||||
elements: alignSelectedElements(elements, appState, {
|
elements: alignSelectedElements(elements, appState, app, {
|
||||||
position: "end",
|
position: "end",
|
||||||
axis: "y",
|
axis: "y",
|
||||||
}),
|
}),
|
||||||
@@ -100,9 +98,9 @@ export const actionAlignBottom = register({
|
|||||||
},
|
},
|
||||||
keyTest: (event) =>
|
keyTest: (event) =>
|
||||||
event[KEYS.CTRL_OR_CMD] && event.shiftKey && event.key === KEYS.ARROW_DOWN,
|
event[KEYS.CTRL_OR_CMD] && event.shiftKey && event.key === KEYS.ARROW_DOWN,
|
||||||
PanelComponent: ({ elements, appState, updateData }) => (
|
PanelComponent: ({ elements, appState, updateData, app }) => (
|
||||||
<ToolButton
|
<ToolButton
|
||||||
hidden={!alignActionsPredicate(elements, appState)}
|
hidden={!alignActionsPredicate(elements, appState, null, app)}
|
||||||
type="button"
|
type="button"
|
||||||
icon={AlignBottomIcon}
|
icon={AlignBottomIcon}
|
||||||
onClick={() => updateData(null)}
|
onClick={() => updateData(null)}
|
||||||
@@ -119,10 +117,10 @@ export const actionAlignLeft = register({
|
|||||||
name: "alignLeft",
|
name: "alignLeft",
|
||||||
trackEvent: { category: "element" },
|
trackEvent: { category: "element" },
|
||||||
predicate: alignActionsPredicate,
|
predicate: alignActionsPredicate,
|
||||||
perform: (elements, appState) => {
|
perform: (elements, appState, _, app) => {
|
||||||
return {
|
return {
|
||||||
appState,
|
appState,
|
||||||
elements: alignSelectedElements(elements, appState, {
|
elements: alignSelectedElements(elements, appState, app, {
|
||||||
position: "start",
|
position: "start",
|
||||||
axis: "x",
|
axis: "x",
|
||||||
}),
|
}),
|
||||||
@@ -131,9 +129,9 @@ export const actionAlignLeft = register({
|
|||||||
},
|
},
|
||||||
keyTest: (event) =>
|
keyTest: (event) =>
|
||||||
event[KEYS.CTRL_OR_CMD] && event.shiftKey && event.key === KEYS.ARROW_LEFT,
|
event[KEYS.CTRL_OR_CMD] && event.shiftKey && event.key === KEYS.ARROW_LEFT,
|
||||||
PanelComponent: ({ elements, appState, updateData }) => (
|
PanelComponent: ({ elements, appState, updateData, app }) => (
|
||||||
<ToolButton
|
<ToolButton
|
||||||
hidden={!alignActionsPredicate(elements, appState)}
|
hidden={!alignActionsPredicate(elements, appState, null, app)}
|
||||||
type="button"
|
type="button"
|
||||||
icon={AlignLeftIcon}
|
icon={AlignLeftIcon}
|
||||||
onClick={() => updateData(null)}
|
onClick={() => updateData(null)}
|
||||||
@@ -150,10 +148,10 @@ export const actionAlignRight = register({
|
|||||||
name: "alignRight",
|
name: "alignRight",
|
||||||
trackEvent: { category: "element" },
|
trackEvent: { category: "element" },
|
||||||
predicate: alignActionsPredicate,
|
predicate: alignActionsPredicate,
|
||||||
perform: (elements, appState) => {
|
perform: (elements, appState, _, app) => {
|
||||||
return {
|
return {
|
||||||
appState,
|
appState,
|
||||||
elements: alignSelectedElements(elements, appState, {
|
elements: alignSelectedElements(elements, appState, app, {
|
||||||
position: "end",
|
position: "end",
|
||||||
axis: "x",
|
axis: "x",
|
||||||
}),
|
}),
|
||||||
@@ -162,9 +160,9 @@ export const actionAlignRight = register({
|
|||||||
},
|
},
|
||||||
keyTest: (event) =>
|
keyTest: (event) =>
|
||||||
event[KEYS.CTRL_OR_CMD] && event.shiftKey && event.key === KEYS.ARROW_RIGHT,
|
event[KEYS.CTRL_OR_CMD] && event.shiftKey && event.key === KEYS.ARROW_RIGHT,
|
||||||
PanelComponent: ({ elements, appState, updateData }) => (
|
PanelComponent: ({ elements, appState, updateData, app }) => (
|
||||||
<ToolButton
|
<ToolButton
|
||||||
hidden={!alignActionsPredicate(elements, appState)}
|
hidden={!alignActionsPredicate(elements, appState, null, app)}
|
||||||
type="button"
|
type="button"
|
||||||
icon={AlignRightIcon}
|
icon={AlignRightIcon}
|
||||||
onClick={() => updateData(null)}
|
onClick={() => updateData(null)}
|
||||||
@@ -181,19 +179,19 @@ export const actionAlignVerticallyCentered = register({
|
|||||||
name: "alignVerticallyCentered",
|
name: "alignVerticallyCentered",
|
||||||
trackEvent: { category: "element" },
|
trackEvent: { category: "element" },
|
||||||
predicate: alignActionsPredicate,
|
predicate: alignActionsPredicate,
|
||||||
perform: (elements, appState) => {
|
perform: (elements, appState, _, app) => {
|
||||||
return {
|
return {
|
||||||
appState,
|
appState,
|
||||||
elements: alignSelectedElements(elements, appState, {
|
elements: alignSelectedElements(elements, appState, app, {
|
||||||
position: "center",
|
position: "center",
|
||||||
axis: "y",
|
axis: "y",
|
||||||
}),
|
}),
|
||||||
commitToHistory: true,
|
commitToHistory: true,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
PanelComponent: ({ elements, appState, updateData }) => (
|
PanelComponent: ({ elements, appState, updateData, app }) => (
|
||||||
<ToolButton
|
<ToolButton
|
||||||
hidden={!alignActionsPredicate(elements, appState)}
|
hidden={!alignActionsPredicate(elements, appState, null, app)}
|
||||||
type="button"
|
type="button"
|
||||||
icon={CenterVerticallyIcon}
|
icon={CenterVerticallyIcon}
|
||||||
onClick={() => updateData(null)}
|
onClick={() => updateData(null)}
|
||||||
@@ -208,19 +206,19 @@ export const actionAlignHorizontallyCentered = register({
|
|||||||
name: "alignHorizontallyCentered",
|
name: "alignHorizontallyCentered",
|
||||||
trackEvent: { category: "element" },
|
trackEvent: { category: "element" },
|
||||||
predicate: alignActionsPredicate,
|
predicate: alignActionsPredicate,
|
||||||
perform: (elements, appState) => {
|
perform: (elements, appState, _, app) => {
|
||||||
return {
|
return {
|
||||||
appState,
|
appState,
|
||||||
elements: alignSelectedElements(elements, appState, {
|
elements: alignSelectedElements(elements, appState, app, {
|
||||||
position: "center",
|
position: "center",
|
||||||
axis: "x",
|
axis: "x",
|
||||||
}),
|
}),
|
||||||
commitToHistory: true,
|
commitToHistory: true,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
PanelComponent: ({ elements, appState, updateData }) => (
|
PanelComponent: ({ elements, appState, updateData, app }) => (
|
||||||
<ToolButton
|
<ToolButton
|
||||||
hidden={!alignActionsPredicate(elements, appState)}
|
hidden={!alignActionsPredicate(elements, appState, null, app)}
|
||||||
type="button"
|
type="button"
|
||||||
icon={CenterHorizontallyIcon}
|
icon={CenterHorizontallyIcon}
|
||||||
onClick={() => updateData(null)}
|
onClick={() => updateData(null)}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import {
|
|||||||
VERTICAL_ALIGN,
|
VERTICAL_ALIGN,
|
||||||
TEXT_ALIGN,
|
TEXT_ALIGN,
|
||||||
} from "../constants";
|
} from "../constants";
|
||||||
import { getNonDeletedElements, isTextElement, newElement } from "../element";
|
import { isTextElement, newElement } from "../element";
|
||||||
import { mutateElement } from "../element/mutateElement";
|
import { mutateElement } from "../element/mutateElement";
|
||||||
import {
|
import {
|
||||||
computeBoundTextPosition,
|
computeBoundTextPosition,
|
||||||
@@ -29,8 +29,8 @@ import {
|
|||||||
ExcalidrawTextContainer,
|
ExcalidrawTextContainer,
|
||||||
ExcalidrawTextElement,
|
ExcalidrawTextElement,
|
||||||
} from "../element/types";
|
} from "../element/types";
|
||||||
import { getSelectedElements } from "../scene";
|
|
||||||
import { AppState } from "../types";
|
import { AppState } from "../types";
|
||||||
|
import { Mutable } from "../utility-types";
|
||||||
import { getFontString } from "../utils";
|
import { getFontString } from "../utils";
|
||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
|
|
||||||
@@ -38,16 +38,13 @@ export const actionUnbindText = register({
|
|||||||
name: "unbindText",
|
name: "unbindText",
|
||||||
contextItemLabel: "labels.unbindText",
|
contextItemLabel: "labels.unbindText",
|
||||||
trackEvent: { category: "element" },
|
trackEvent: { category: "element" },
|
||||||
predicate: (elements, appState) => {
|
predicate: (elements, appState, _, app) => {
|
||||||
const selectedElements = getSelectedElements(elements, appState);
|
const selectedElements = app.scene.getSelectedElements(appState);
|
||||||
|
|
||||||
return selectedElements.some((element) => hasBoundTextElement(element));
|
return selectedElements.some((element) => hasBoundTextElement(element));
|
||||||
},
|
},
|
||||||
perform: (elements, appState) => {
|
perform: (elements, appState, _, app) => {
|
||||||
const selectedElements = getSelectedElements(
|
const selectedElements = app.scene.getSelectedElements(appState);
|
||||||
getNonDeletedElements(elements),
|
|
||||||
appState,
|
|
||||||
);
|
|
||||||
selectedElements.forEach((element) => {
|
selectedElements.forEach((element) => {
|
||||||
const boundTextElement = getBoundTextElement(element);
|
const boundTextElement = getBoundTextElement(element);
|
||||||
if (boundTextElement) {
|
if (boundTextElement) {
|
||||||
@@ -92,8 +89,8 @@ export const actionBindText = register({
|
|||||||
name: "bindText",
|
name: "bindText",
|
||||||
contextItemLabel: "labels.bindText",
|
contextItemLabel: "labels.bindText",
|
||||||
trackEvent: { category: "element" },
|
trackEvent: { category: "element" },
|
||||||
predicate: (elements, appState) => {
|
predicate: (elements, appState, _, app) => {
|
||||||
const selectedElements = getSelectedElements(elements, appState);
|
const selectedElements = app.scene.getSelectedElements(appState);
|
||||||
|
|
||||||
if (selectedElements.length === 2) {
|
if (selectedElements.length === 2) {
|
||||||
const textElement =
|
const textElement =
|
||||||
@@ -116,11 +113,8 @@ export const actionBindText = register({
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
perform: (elements, appState) => {
|
perform: (elements, appState, _, app) => {
|
||||||
const selectedElements = getSelectedElements(
|
const selectedElements = app.scene.getSelectedElements(appState);
|
||||||
getNonDeletedElements(elements),
|
|
||||||
appState,
|
|
||||||
);
|
|
||||||
|
|
||||||
let textElement: ExcalidrawTextElement;
|
let textElement: ExcalidrawTextElement;
|
||||||
let container: ExcalidrawTextContainer;
|
let container: ExcalidrawTextContainer;
|
||||||
@@ -200,18 +194,15 @@ export const actionWrapTextInContainer = register({
|
|||||||
name: "wrapTextInContainer",
|
name: "wrapTextInContainer",
|
||||||
contextItemLabel: "labels.createContainerFromText",
|
contextItemLabel: "labels.createContainerFromText",
|
||||||
trackEvent: { category: "element" },
|
trackEvent: { category: "element" },
|
||||||
predicate: (elements, appState) => {
|
predicate: (elements, appState, _, app) => {
|
||||||
const selectedElements = getSelectedElements(elements, appState);
|
const selectedElements = app.scene.getSelectedElements(appState);
|
||||||
const areTextElements = selectedElements.every((el) => isTextElement(el));
|
const areTextElements = selectedElements.every((el) => isTextElement(el));
|
||||||
return selectedElements.length > 0 && areTextElements;
|
return selectedElements.length > 0 && areTextElements;
|
||||||
},
|
},
|
||||||
perform: (elements, appState) => {
|
perform: (elements, appState, _, app) => {
|
||||||
const selectedElements = getSelectedElements(
|
const selectedElements = app.scene.getSelectedElements(appState);
|
||||||
getNonDeletedElements(elements),
|
|
||||||
appState,
|
|
||||||
);
|
|
||||||
let updatedElements: readonly ExcalidrawElement[] = elements.slice();
|
let updatedElements: readonly ExcalidrawElement[] = elements.slice();
|
||||||
const containerIds: AppState["selectedElementIds"] = {};
|
const containerIds: Mutable<AppState["selectedElementIds"]> = {};
|
||||||
|
|
||||||
for (const textElement of selectedElements) {
|
for (const textElement of selectedElements) {
|
||||||
if (isTextElement(textElement)) {
|
if (isTextElement(textElement)) {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { getCommonBounds, getNonDeletedElements } from "../element";
|
|||||||
import { ExcalidrawElement } from "../element/types";
|
import { ExcalidrawElement } from "../element/types";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { CODES, KEYS } from "../keys";
|
import { CODES, KEYS } from "../keys";
|
||||||
import { getNormalizedZoom, getSelectedElements } from "../scene";
|
import { getNormalizedZoom } from "../scene";
|
||||||
import { centerScrollOn } from "../scene/scroll";
|
import { centerScrollOn } from "../scene/scroll";
|
||||||
import { getStateForZoom } from "../scene/zoom";
|
import { getStateForZoom } from "../scene/zoom";
|
||||||
import { AppState, NormalizedZoomValue } from "../types";
|
import { AppState, NormalizedZoomValue } from "../types";
|
||||||
@@ -20,7 +20,6 @@ import {
|
|||||||
isHandToolActive,
|
isHandToolActive,
|
||||||
} from "../appState";
|
} from "../appState";
|
||||||
import { DEFAULT_CANVAS_BACKGROUND_PICKS } from "../colors";
|
import { DEFAULT_CANVAS_BACKGROUND_PICKS } from "../colors";
|
||||||
import { excludeElementsInFramesFromSelection } from "../scene/selection";
|
|
||||||
import { Bounds } from "../element/bounds";
|
import { Bounds } from "../element/bounds";
|
||||||
|
|
||||||
export const actionChangeViewBackgroundColor = register({
|
export const actionChangeViewBackgroundColor = register({
|
||||||
@@ -226,52 +225,93 @@ const zoomValueToFitBoundsOnViewport = (
|
|||||||
return clampedZoomValueToFitElements as NormalizedZoomValue;
|
return clampedZoomValueToFitElements as NormalizedZoomValue;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const zoomToFitElements = (
|
export const zoomToFit = ({
|
||||||
elements: readonly ExcalidrawElement[],
|
targetElements,
|
||||||
appState: Readonly<AppState>,
|
appState,
|
||||||
zoomToSelection: boolean,
|
fitToViewport = false,
|
||||||
) => {
|
viewportZoomFactor = 0.7,
|
||||||
const nonDeletedElements = getNonDeletedElements(elements);
|
}: {
|
||||||
const selectedElements = getSelectedElements(nonDeletedElements, appState);
|
targetElements: readonly ExcalidrawElement[];
|
||||||
|
appState: Readonly<AppState>;
|
||||||
const commonBounds =
|
/** whether to fit content to viewport (beyond >100%) */
|
||||||
zoomToSelection && selectedElements.length > 0
|
fitToViewport: boolean;
|
||||||
? getCommonBounds(excludeElementsInFramesFromSelection(selectedElements))
|
/** zoom content to cover X of the viewport, when fitToViewport=true */
|
||||||
: getCommonBounds(
|
viewportZoomFactor?: number;
|
||||||
excludeElementsInFramesFromSelection(nonDeletedElements),
|
}) => {
|
||||||
);
|
const commonBounds = getCommonBounds(getNonDeletedElements(targetElements));
|
||||||
|
|
||||||
const newZoom = {
|
|
||||||
value: zoomValueToFitBoundsOnViewport(commonBounds, {
|
|
||||||
width: appState.width,
|
|
||||||
height: appState.height,
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
const [x1, y1, x2, y2] = commonBounds;
|
const [x1, y1, x2, y2] = commonBounds;
|
||||||
const centerX = (x1 + x2) / 2;
|
const centerX = (x1 + x2) / 2;
|
||||||
const centerY = (y1 + y2) / 2;
|
const centerY = (y1 + y2) / 2;
|
||||||
|
|
||||||
|
let newZoomValue;
|
||||||
|
let scrollX;
|
||||||
|
let scrollY;
|
||||||
|
|
||||||
|
if (fitToViewport) {
|
||||||
|
const commonBoundsWidth = x2 - x1;
|
||||||
|
const commonBoundsHeight = y2 - y1;
|
||||||
|
|
||||||
|
newZoomValue =
|
||||||
|
Math.min(
|
||||||
|
appState.width / commonBoundsWidth,
|
||||||
|
appState.height / commonBoundsHeight,
|
||||||
|
) * Math.min(1, Math.max(viewportZoomFactor, 0.1));
|
||||||
|
|
||||||
|
// Apply clamping to newZoomValue to be between 10% and 3000%
|
||||||
|
newZoomValue = Math.min(
|
||||||
|
Math.max(newZoomValue, 0.1),
|
||||||
|
30.0,
|
||||||
|
) as NormalizedZoomValue;
|
||||||
|
|
||||||
|
scrollX = (appState.width / 2) * (1 / newZoomValue) - centerX;
|
||||||
|
scrollY = (appState.height / 2) * (1 / newZoomValue) - centerY;
|
||||||
|
} else {
|
||||||
|
newZoomValue = zoomValueToFitBoundsOnViewport(commonBounds, {
|
||||||
|
width: appState.width,
|
||||||
|
height: appState.height,
|
||||||
|
});
|
||||||
|
|
||||||
|
const centerScroll = centerScrollOn({
|
||||||
|
scenePoint: { x: centerX, y: centerY },
|
||||||
|
viewportDimensions: {
|
||||||
|
width: appState.width,
|
||||||
|
height: appState.height,
|
||||||
|
},
|
||||||
|
zoom: { value: newZoomValue },
|
||||||
|
});
|
||||||
|
|
||||||
|
scrollX = centerScroll.scrollX;
|
||||||
|
scrollY = centerScroll.scrollY;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
appState: {
|
appState: {
|
||||||
...appState,
|
...appState,
|
||||||
...centerScrollOn({
|
scrollX,
|
||||||
scenePoint: { x: centerX, y: centerY },
|
scrollY,
|
||||||
viewportDimensions: {
|
zoom: { value: newZoomValue },
|
||||||
width: appState.width,
|
|
||||||
height: appState.height,
|
|
||||||
},
|
|
||||||
zoom: newZoom,
|
|
||||||
}),
|
|
||||||
zoom: newZoom,
|
|
||||||
},
|
},
|
||||||
commitToHistory: false,
|
commitToHistory: false,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const actionZoomToSelected = register({
|
// Note, this action differs from actionZoomToFitSelection in that it doesn't
|
||||||
name: "zoomToSelection",
|
// zoom beyond 100%. In other words, if the content is smaller than viewport
|
||||||
|
// size, it won't be zoomed in.
|
||||||
|
export const actionZoomToFitSelectionInViewport = register({
|
||||||
|
name: "zoomToFitSelectionInViewport",
|
||||||
trackEvent: { category: "canvas" },
|
trackEvent: { category: "canvas" },
|
||||||
perform: (elements, appState) => zoomToFitElements(elements, appState, true),
|
perform: (elements, appState, _, app) => {
|
||||||
|
const selectedElements = app.scene.getSelectedElements(appState);
|
||||||
|
return zoomToFit({
|
||||||
|
targetElements: selectedElements.length ? selectedElements : elements,
|
||||||
|
appState,
|
||||||
|
fitToViewport: false,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
// NOTE shift-2 should have been assigned actionZoomToFitSelection.
|
||||||
|
// TBD on how proceed
|
||||||
keyTest: (event) =>
|
keyTest: (event) =>
|
||||||
event.code === CODES.TWO &&
|
event.code === CODES.TWO &&
|
||||||
event.shiftKey &&
|
event.shiftKey &&
|
||||||
@@ -279,11 +319,31 @@ export const actionZoomToSelected = register({
|
|||||||
!event[KEYS.CTRL_OR_CMD],
|
!event[KEYS.CTRL_OR_CMD],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const actionZoomToFitSelection = register({
|
||||||
|
name: "zoomToFitSelection",
|
||||||
|
trackEvent: { category: "canvas" },
|
||||||
|
perform: (elements, appState, _, app) => {
|
||||||
|
const selectedElements = app.scene.getSelectedElements(appState);
|
||||||
|
return zoomToFit({
|
||||||
|
targetElements: selectedElements.length ? selectedElements : elements,
|
||||||
|
appState,
|
||||||
|
fitToViewport: true,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
// NOTE this action should use shift-2 per figma, alas
|
||||||
|
keyTest: (event) =>
|
||||||
|
event.code === CODES.THREE &&
|
||||||
|
event.shiftKey &&
|
||||||
|
!event.altKey &&
|
||||||
|
!event[KEYS.CTRL_OR_CMD],
|
||||||
|
});
|
||||||
|
|
||||||
export const actionZoomToFit = register({
|
export const actionZoomToFit = register({
|
||||||
name: "zoomToFit",
|
name: "zoomToFit",
|
||||||
viewMode: true,
|
viewMode: true,
|
||||||
trackEvent: { category: "canvas" },
|
trackEvent: { category: "canvas" },
|
||||||
perform: (elements, appState) => zoomToFitElements(elements, appState, false),
|
perform: (elements, appState) =>
|
||||||
|
zoomToFit({ targetElements: elements, appState, fitToViewport: false }),
|
||||||
keyTest: (event) =>
|
keyTest: (event) =>
|
||||||
event.code === CODES.ONE &&
|
event.code === CODES.ONE &&
|
||||||
event.shiftKey &&
|
event.shiftKey &&
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import {
|
|||||||
probablySupportsClipboardWriteText,
|
probablySupportsClipboardWriteText,
|
||||||
} from "../clipboard";
|
} from "../clipboard";
|
||||||
import { actionDeleteSelected } from "./actionDeleteSelected";
|
import { actionDeleteSelected } from "./actionDeleteSelected";
|
||||||
import { getSelectedElements } from "../scene/selection";
|
|
||||||
import { exportCanvas } from "../data/index";
|
import { exportCanvas } from "../data/index";
|
||||||
import { getNonDeletedElements, isTextElement } from "../element";
|
import { getNonDeletedElements, isTextElement } from "../element";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
@@ -16,7 +15,8 @@ export const actionCopy = register({
|
|||||||
name: "copy",
|
name: "copy",
|
||||||
trackEvent: { category: "element" },
|
trackEvent: { category: "element" },
|
||||||
perform: (elements, appState, _, app) => {
|
perform: (elements, appState, _, app) => {
|
||||||
const elementsToCopy = getSelectedElements(elements, appState, {
|
const elementsToCopy = app.scene.getSelectedElements({
|
||||||
|
selectedElementIds: appState.selectedElementIds,
|
||||||
includeBoundTextElement: true,
|
includeBoundTextElement: true,
|
||||||
includeElementsInFrames: true,
|
includeElementsInFrames: true,
|
||||||
});
|
});
|
||||||
@@ -75,14 +75,11 @@ export const actionCopyAsSvg = register({
|
|||||||
commitToHistory: false,
|
commitToHistory: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const selectedElements = getSelectedElements(
|
const selectedElements = app.scene.getSelectedElements({
|
||||||
getNonDeletedElements(elements),
|
selectedElementIds: appState.selectedElementIds,
|
||||||
appState,
|
includeBoundTextElement: true,
|
||||||
{
|
includeElementsInFrames: true,
|
||||||
includeBoundTextElement: true,
|
});
|
||||||
includeElementsInFrames: true,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
try {
|
try {
|
||||||
await exportCanvas(
|
await exportCanvas(
|
||||||
"clipboard-svg",
|
"clipboard-svg",
|
||||||
@@ -122,14 +119,11 @@ export const actionCopyAsPng = register({
|
|||||||
commitToHistory: false,
|
commitToHistory: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const selectedElements = getSelectedElements(
|
const selectedElements = app.scene.getSelectedElements({
|
||||||
getNonDeletedElements(elements),
|
selectedElementIds: appState.selectedElementIds,
|
||||||
appState,
|
includeBoundTextElement: true,
|
||||||
{
|
includeElementsInFrames: true,
|
||||||
includeBoundTextElement: true,
|
});
|
||||||
includeElementsInFrames: true,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
try {
|
try {
|
||||||
await exportCanvas(
|
await exportCanvas(
|
||||||
"clipboard",
|
"clipboard",
|
||||||
@@ -177,14 +171,11 @@ export const actionCopyAsPng = register({
|
|||||||
export const copyText = register({
|
export const copyText = register({
|
||||||
name: "copyText",
|
name: "copyText",
|
||||||
trackEvent: { category: "element" },
|
trackEvent: { category: "element" },
|
||||||
perform: (elements, appState) => {
|
perform: (elements, appState, _, app) => {
|
||||||
const selectedElements = getSelectedElements(
|
const selectedElements = app.scene.getSelectedElements({
|
||||||
getNonDeletedElements(elements),
|
selectedElementIds: appState.selectedElementIds,
|
||||||
appState,
|
includeBoundTextElement: true,
|
||||||
{
|
});
|
||||||
includeBoundTextElement: true,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const text = selectedElements
|
const text = selectedElements
|
||||||
.reduce((acc: string[], element) => {
|
.reduce((acc: string[], element) => {
|
||||||
@@ -199,12 +190,15 @@ export const copyText = register({
|
|||||||
commitToHistory: false,
|
commitToHistory: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
predicate: (elements, appState) => {
|
predicate: (elements, appState, _, app) => {
|
||||||
return (
|
return (
|
||||||
probablySupportsClipboardWriteText &&
|
probablySupportsClipboardWriteText &&
|
||||||
getSelectedElements(elements, appState, {
|
app.scene
|
||||||
includeBoundTextElement: true,
|
.getSelectedElements({
|
||||||
}).some(isTextElement)
|
selectedElementIds: appState.selectedElementIds,
|
||||||
|
includeBoundTextElement: true,
|
||||||
|
})
|
||||||
|
.some(isTextElement)
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
contextItemLabel: "labels.copyText",
|
contextItemLabel: "labels.copyText",
|
||||||
|
|||||||
@@ -9,19 +9,13 @@ import { ExcalidrawElement } from "../element/types";
|
|||||||
import { updateFrameMembershipOfSelectedElements } from "../frame";
|
import { updateFrameMembershipOfSelectedElements } from "../frame";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { CODES, KEYS } from "../keys";
|
import { CODES, KEYS } from "../keys";
|
||||||
import { getSelectedElements, isSomeElementSelected } from "../scene";
|
import { isSomeElementSelected } from "../scene";
|
||||||
import { AppState } from "../types";
|
import { AppClassProperties, AppState } from "../types";
|
||||||
import { arrayToMap, getShortcutKey } from "../utils";
|
import { arrayToMap, getShortcutKey } from "../utils";
|
||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
|
|
||||||
const enableActionGroup = (
|
const enableActionGroup = (appState: AppState, app: AppClassProperties) => {
|
||||||
elements: readonly ExcalidrawElement[],
|
const selectedElements = app.scene.getSelectedElements(appState);
|
||||||
appState: AppState,
|
|
||||||
) => {
|
|
||||||
const selectedElements = getSelectedElements(
|
|
||||||
getNonDeletedElements(elements),
|
|
||||||
appState,
|
|
||||||
);
|
|
||||||
return (
|
return (
|
||||||
selectedElements.length > 1 &&
|
selectedElements.length > 1 &&
|
||||||
// TODO enable distributing frames when implemented properly
|
// TODO enable distributing frames when implemented properly
|
||||||
@@ -32,12 +26,10 @@ const enableActionGroup = (
|
|||||||
const distributeSelectedElements = (
|
const distributeSelectedElements = (
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly ExcalidrawElement[],
|
||||||
appState: Readonly<AppState>,
|
appState: Readonly<AppState>,
|
||||||
|
app: AppClassProperties,
|
||||||
distribution: Distribution,
|
distribution: Distribution,
|
||||||
) => {
|
) => {
|
||||||
const selectedElements = getSelectedElements(
|
const selectedElements = app.scene.getSelectedElements(appState);
|
||||||
getNonDeletedElements(elements),
|
|
||||||
appState,
|
|
||||||
);
|
|
||||||
|
|
||||||
const updatedElements = distributeElements(selectedElements, distribution);
|
const updatedElements = distributeElements(selectedElements, distribution);
|
||||||
|
|
||||||
@@ -46,16 +38,17 @@ const distributeSelectedElements = (
|
|||||||
return updateFrameMembershipOfSelectedElements(
|
return updateFrameMembershipOfSelectedElements(
|
||||||
elements.map((element) => updatedElementsMap.get(element.id) || element),
|
elements.map((element) => updatedElementsMap.get(element.id) || element),
|
||||||
appState,
|
appState,
|
||||||
|
app,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const distributeHorizontally = register({
|
export const distributeHorizontally = register({
|
||||||
name: "distributeHorizontally",
|
name: "distributeHorizontally",
|
||||||
trackEvent: { category: "element" },
|
trackEvent: { category: "element" },
|
||||||
perform: (elements, appState) => {
|
perform: (elements, appState, _, app) => {
|
||||||
return {
|
return {
|
||||||
appState,
|
appState,
|
||||||
elements: distributeSelectedElements(elements, appState, {
|
elements: distributeSelectedElements(elements, appState, app, {
|
||||||
space: "between",
|
space: "between",
|
||||||
axis: "x",
|
axis: "x",
|
||||||
}),
|
}),
|
||||||
@@ -64,9 +57,9 @@ export const distributeHorizontally = register({
|
|||||||
},
|
},
|
||||||
keyTest: (event) =>
|
keyTest: (event) =>
|
||||||
!event[KEYS.CTRL_OR_CMD] && event.altKey && event.code === CODES.H,
|
!event[KEYS.CTRL_OR_CMD] && event.altKey && event.code === CODES.H,
|
||||||
PanelComponent: ({ elements, appState, updateData }) => (
|
PanelComponent: ({ elements, appState, updateData, app }) => (
|
||||||
<ToolButton
|
<ToolButton
|
||||||
hidden={!enableActionGroup(elements, appState)}
|
hidden={!enableActionGroup(appState, app)}
|
||||||
type="button"
|
type="button"
|
||||||
icon={DistributeHorizontallyIcon}
|
icon={DistributeHorizontallyIcon}
|
||||||
onClick={() => updateData(null)}
|
onClick={() => updateData(null)}
|
||||||
@@ -82,10 +75,10 @@ export const distributeHorizontally = register({
|
|||||||
export const distributeVertically = register({
|
export const distributeVertically = register({
|
||||||
name: "distributeVertically",
|
name: "distributeVertically",
|
||||||
trackEvent: { category: "element" },
|
trackEvent: { category: "element" },
|
||||||
perform: (elements, appState) => {
|
perform: (elements, appState, _, app) => {
|
||||||
return {
|
return {
|
||||||
appState,
|
appState,
|
||||||
elements: distributeSelectedElements(elements, appState, {
|
elements: distributeSelectedElements(elements, appState, app, {
|
||||||
space: "between",
|
space: "between",
|
||||||
axis: "y",
|
axis: "y",
|
||||||
}),
|
}),
|
||||||
@@ -94,9 +87,9 @@ export const distributeVertically = register({
|
|||||||
},
|
},
|
||||||
keyTest: (event) =>
|
keyTest: (event) =>
|
||||||
!event[KEYS.CTRL_OR_CMD] && event.altKey && event.code === CODES.V,
|
!event[KEYS.CTRL_OR_CMD] && event.altKey && event.code === CODES.V,
|
||||||
PanelComponent: ({ elements, appState, updateData }) => (
|
PanelComponent: ({ elements, appState, updateData, app }) => (
|
||||||
<ToolButton
|
<ToolButton
|
||||||
hidden={!enableActionGroup(elements, appState)}
|
hidden={!enableActionGroup(appState, app)}
|
||||||
type="button"
|
type="button"
|
||||||
icon={DistributeVerticallyIcon}
|
icon={DistributeVerticallyIcon}
|
||||||
onClick={() => updateData(null)}
|
onClick={() => updateData(null)}
|
||||||
|
|||||||
@@ -274,6 +274,8 @@ const duplicateElements = (
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
getNonDeletedElements(finalElements),
|
getNonDeletedElements(finalElements),
|
||||||
|
appState,
|
||||||
|
null,
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { newElementWith } from "../element/mutateElement";
|
import { newElementWith } from "../element/mutateElement";
|
||||||
import { ExcalidrawElement } from "../element/types";
|
import { ExcalidrawElement } from "../element/types";
|
||||||
import { KEYS } from "../keys";
|
import { KEYS } from "../keys";
|
||||||
import { getSelectedElements } from "../scene";
|
|
||||||
import { arrayToMap } from "../utils";
|
import { arrayToMap } from "../utils";
|
||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
|
|
||||||
@@ -11,14 +10,15 @@ const shouldLock = (elements: readonly ExcalidrawElement[]) =>
|
|||||||
export const actionToggleElementLock = register({
|
export const actionToggleElementLock = register({
|
||||||
name: "toggleElementLock",
|
name: "toggleElementLock",
|
||||||
trackEvent: { category: "element" },
|
trackEvent: { category: "element" },
|
||||||
predicate: (elements, appState) => {
|
predicate: (elements, appState, _, app) => {
|
||||||
const selectedElements = getSelectedElements(elements, appState);
|
const selectedElements = app.scene.getSelectedElements(appState);
|
||||||
return !selectedElements.some(
|
return !selectedElements.some(
|
||||||
(element) => element.locked && element.frameId,
|
(element) => element.locked && element.frameId,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
perform: (elements, appState) => {
|
perform: (elements, appState, _, app) => {
|
||||||
const selectedElements = getSelectedElements(elements, appState, {
|
const selectedElements = app.scene.getSelectedElements({
|
||||||
|
selectedElementIds: appState.selectedElementIds,
|
||||||
includeBoundTextElement: true,
|
includeBoundTextElement: true,
|
||||||
includeElementsInFrames: true,
|
includeElementsInFrames: true,
|
||||||
});
|
});
|
||||||
@@ -46,8 +46,9 @@ export const actionToggleElementLock = register({
|
|||||||
commitToHistory: true,
|
commitToHistory: true,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
contextItemLabel: (elements, appState) => {
|
contextItemLabel: (elements, appState, app) => {
|
||||||
const selected = getSelectedElements(elements, appState, {
|
const selected = app.scene.getSelectedElements({
|
||||||
|
selectedElementIds: appState.selectedElementIds,
|
||||||
includeBoundTextElement: false,
|
includeBoundTextElement: false,
|
||||||
});
|
});
|
||||||
if (selected.length === 1 && selected[0].type !== "frame") {
|
if (selected.length === 1 && selected[0].type !== "frame") {
|
||||||
@@ -60,12 +61,13 @@ export const actionToggleElementLock = register({
|
|||||||
? "labels.elementLock.lockAll"
|
? "labels.elementLock.lockAll"
|
||||||
: "labels.elementLock.unlockAll";
|
: "labels.elementLock.unlockAll";
|
||||||
},
|
},
|
||||||
keyTest: (event, appState, elements) => {
|
keyTest: (event, appState, elements, app) => {
|
||||||
return (
|
return (
|
||||||
event.key.toLocaleLowerCase() === KEYS.L &&
|
event.key.toLocaleLowerCase() === KEYS.L &&
|
||||||
event[KEYS.CTRL_OR_CMD] &&
|
event[KEYS.CTRL_OR_CMD] &&
|
||||||
event.shiftKey &&
|
event.shiftKey &&
|
||||||
getSelectedElements(elements, appState, {
|
app.scene.getSelectedElements({
|
||||||
|
selectedElementIds: appState.selectedElementIds,
|
||||||
includeBoundTextElement: false,
|
includeBoundTextElement: false,
|
||||||
}).length > 0
|
}).length > 0
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ export const actionChangeExportScale = register({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const scaleButtonTitle = `${t(
|
const scaleButtonTitle = `${t(
|
||||||
"buttons.scale",
|
"imageExportDialog.label.scale",
|
||||||
)} ${s}x (${width}x${height})`;
|
)} ${s}x (${width}x${height})`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -102,7 +102,7 @@ export const actionChangeExportBackground = register({
|
|||||||
checked={appState.exportBackground}
|
checked={appState.exportBackground}
|
||||||
onChange={(checked) => updateData(checked)}
|
onChange={(checked) => updateData(checked)}
|
||||||
>
|
>
|
||||||
{t("labels.withBackground")}
|
{t("imageExportDialog.label.withBackground")}
|
||||||
</CheckboxItem>
|
</CheckboxItem>
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
@@ -121,8 +121,8 @@ export const actionChangeExportEmbedScene = register({
|
|||||||
checked={appState.exportEmbedScene}
|
checked={appState.exportEmbedScene}
|
||||||
onChange={(checked) => updateData(checked)}
|
onChange={(checked) => updateData(checked)}
|
||||||
>
|
>
|
||||||
{t("labels.exportEmbedScene")}
|
{t("imageExportDialog.label.embedScene")}
|
||||||
<Tooltip label={t("labels.exportEmbedScene_details")} long={true}>
|
<Tooltip label={t("imageExportDialog.tooltip.embedScene")} long={true}>
|
||||||
<div className="excalidraw-tooltip-icon">{questionCircle}</div>
|
<div className="excalidraw-tooltip-icon">{questionCircle}</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</CheckboxItem>
|
</CheckboxItem>
|
||||||
@@ -277,7 +277,7 @@ export const actionExportWithDarkMode = register({
|
|||||||
onChange={(theme: Theme) => {
|
onChange={(theme: Theme) => {
|
||||||
updateData(theme === THEME.DARK);
|
updateData(theme === THEME.DARK);
|
||||||
}}
|
}}
|
||||||
title={t("labels.toggleExportColorScheme")}
|
title={t("imageExportDialog.label.darkMode")}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -125,13 +125,6 @@ export const actionFinalize = register({
|
|||||||
{ x, y },
|
{ x, y },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
|
||||||
!appState.activeTool.locked &&
|
|
||||||
appState.activeTool.type !== "freedraw"
|
|
||||||
) {
|
|
||||||
appState.selectedElementIds[multiPointElement.id] = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
|||||||
@@ -17,11 +17,12 @@ import { updateFrameMembershipOfSelectedElements } from "../frame";
|
|||||||
export const actionFlipHorizontal = register({
|
export const actionFlipHorizontal = register({
|
||||||
name: "flipHorizontal",
|
name: "flipHorizontal",
|
||||||
trackEvent: { category: "element" },
|
trackEvent: { category: "element" },
|
||||||
perform: (elements, appState) => {
|
perform: (elements, appState, _, app) => {
|
||||||
return {
|
return {
|
||||||
elements: updateFrameMembershipOfSelectedElements(
|
elements: updateFrameMembershipOfSelectedElements(
|
||||||
flipSelectedElements(elements, appState, "horizontal"),
|
flipSelectedElements(elements, appState, "horizontal"),
|
||||||
appState,
|
appState,
|
||||||
|
app,
|
||||||
),
|
),
|
||||||
appState,
|
appState,
|
||||||
commitToHistory: true,
|
commitToHistory: true,
|
||||||
@@ -34,11 +35,12 @@ export const actionFlipHorizontal = register({
|
|||||||
export const actionFlipVertical = register({
|
export const actionFlipVertical = register({
|
||||||
name: "flipVertical",
|
name: "flipVertical",
|
||||||
trackEvent: { category: "element" },
|
trackEvent: { category: "element" },
|
||||||
perform: (elements, appState) => {
|
perform: (elements, appState, _, app) => {
|
||||||
return {
|
return {
|
||||||
elements: updateFrameMembershipOfSelectedElements(
|
elements: updateFrameMembershipOfSelectedElements(
|
||||||
flipSelectedElements(elements, appState, "vertical"),
|
flipSelectedElements(elements, appState, "vertical"),
|
||||||
appState,
|
appState,
|
||||||
|
app,
|
||||||
),
|
),
|
||||||
appState,
|
appState,
|
||||||
commitToHistory: true,
|
commitToHistory: true,
|
||||||
|
|||||||
+19
-27
@@ -3,19 +3,12 @@ import { ExcalidrawElement } from "../element/types";
|
|||||||
import { removeAllElementsFromFrame } from "../frame";
|
import { removeAllElementsFromFrame } from "../frame";
|
||||||
import { getFrameElements } from "../frame";
|
import { getFrameElements } from "../frame";
|
||||||
import { KEYS } from "../keys";
|
import { KEYS } from "../keys";
|
||||||
import { getSelectedElements } from "../scene";
|
import { AppClassProperties, AppState } from "../types";
|
||||||
import { AppState } from "../types";
|
|
||||||
import { setCursorForShape, updateActiveTool } from "../utils";
|
import { setCursorForShape, updateActiveTool } from "../utils";
|
||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
|
|
||||||
const isSingleFrameSelected = (
|
const isSingleFrameSelected = (appState: AppState, app: AppClassProperties) => {
|
||||||
elements: readonly ExcalidrawElement[],
|
const selectedElements = app.scene.getSelectedElements(appState);
|
||||||
appState: AppState,
|
|
||||||
) => {
|
|
||||||
const selectedElements = getSelectedElements(
|
|
||||||
getNonDeletedElements(elements),
|
|
||||||
appState,
|
|
||||||
);
|
|
||||||
|
|
||||||
return selectedElements.length === 1 && selectedElements[0].type === "frame";
|
return selectedElements.length === 1 && selectedElements[0].type === "frame";
|
||||||
};
|
};
|
||||||
@@ -23,11 +16,8 @@ const isSingleFrameSelected = (
|
|||||||
export const actionSelectAllElementsInFrame = register({
|
export const actionSelectAllElementsInFrame = register({
|
||||||
name: "selectAllElementsInFrame",
|
name: "selectAllElementsInFrame",
|
||||||
trackEvent: { category: "canvas" },
|
trackEvent: { category: "canvas" },
|
||||||
perform: (elements, appState) => {
|
perform: (elements, appState, _, app) => {
|
||||||
const selectedFrame = getSelectedElements(
|
const selectedFrame = app.scene.getSelectedElements(appState)[0];
|
||||||
getNonDeletedElements(elements),
|
|
||||||
appState,
|
|
||||||
)[0];
|
|
||||||
|
|
||||||
if (selectedFrame && selectedFrame.type === "frame") {
|
if (selectedFrame && selectedFrame.type === "frame") {
|
||||||
const elementsInFrame = getFrameElements(
|
const elementsInFrame = getFrameElements(
|
||||||
@@ -55,17 +45,15 @@ export const actionSelectAllElementsInFrame = register({
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
contextItemLabel: "labels.selectAllElementsInFrame",
|
contextItemLabel: "labels.selectAllElementsInFrame",
|
||||||
predicate: (elements, appState) => isSingleFrameSelected(elements, appState),
|
predicate: (elements, appState, _, app) =>
|
||||||
|
isSingleFrameSelected(appState, app),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const actionRemoveAllElementsFromFrame = register({
|
export const actionRemoveAllElementsFromFrame = register({
|
||||||
name: "removeAllElementsFromFrame",
|
name: "removeAllElementsFromFrame",
|
||||||
trackEvent: { category: "history" },
|
trackEvent: { category: "history" },
|
||||||
perform: (elements, appState) => {
|
perform: (elements, appState, _, app) => {
|
||||||
const selectedFrame = getSelectedElements(
|
const selectedFrame = app.scene.getSelectedElements(appState)[0];
|
||||||
getNonDeletedElements(elements),
|
|
||||||
appState,
|
|
||||||
)[0];
|
|
||||||
|
|
||||||
if (selectedFrame && selectedFrame.type === "frame") {
|
if (selectedFrame && selectedFrame.type === "frame") {
|
||||||
return {
|
return {
|
||||||
@@ -87,11 +75,12 @@ export const actionRemoveAllElementsFromFrame = register({
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
contextItemLabel: "labels.removeAllElementsFromFrame",
|
contextItemLabel: "labels.removeAllElementsFromFrame",
|
||||||
predicate: (elements, appState) => isSingleFrameSelected(elements, appState),
|
predicate: (elements, appState, _, app) =>
|
||||||
|
isSingleFrameSelected(appState, app),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const actionToggleFrameRendering = register({
|
export const actionupdateFrameRendering = register({
|
||||||
name: "toggleFrameRendering",
|
name: "updateFrameRendering",
|
||||||
viewMode: true,
|
viewMode: true,
|
||||||
trackEvent: { category: "canvas" },
|
trackEvent: { category: "canvas" },
|
||||||
perform: (elements, appState) => {
|
perform: (elements, appState) => {
|
||||||
@@ -99,13 +88,16 @@ export const actionToggleFrameRendering = register({
|
|||||||
elements,
|
elements,
|
||||||
appState: {
|
appState: {
|
||||||
...appState,
|
...appState,
|
||||||
shouldRenderFrames: !appState.shouldRenderFrames,
|
frameRendering: {
|
||||||
|
...appState.frameRendering,
|
||||||
|
enabled: !appState.frameRendering.enabled,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
commitToHistory: false,
|
commitToHistory: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
contextItemLabel: "labels.toggleFrameRendering",
|
contextItemLabel: "labels.updateFrameRendering",
|
||||||
checked: (appState: AppState) => appState.shouldRenderFrames,
|
checked: (appState: AppState) => appState.frameRendering.enabled,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const actionSetFrameAsActiveTool = register({
|
export const actionSetFrameAsActiveTool = register({
|
||||||
|
|||||||
+29
-22
@@ -4,7 +4,7 @@ import { arrayToMap, getShortcutKey } from "../utils";
|
|||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
import { UngroupIcon, GroupIcon } from "../components/icons";
|
import { UngroupIcon, GroupIcon } from "../components/icons";
|
||||||
import { newElementWith } from "../element/mutateElement";
|
import { newElementWith } from "../element/mutateElement";
|
||||||
import { getSelectedElements, isSomeElementSelected } from "../scene";
|
import { isSomeElementSelected } from "../scene";
|
||||||
import {
|
import {
|
||||||
getSelectedGroupIds,
|
getSelectedGroupIds,
|
||||||
selectGroup,
|
selectGroup,
|
||||||
@@ -22,7 +22,7 @@ import {
|
|||||||
ExcalidrawFrameElement,
|
ExcalidrawFrameElement,
|
||||||
ExcalidrawTextElement,
|
ExcalidrawTextElement,
|
||||||
} from "../element/types";
|
} from "../element/types";
|
||||||
import { AppState } from "../types";
|
import { AppClassProperties, AppState } from "../types";
|
||||||
import { isBoundToContainer } from "../element/typeChecks";
|
import { isBoundToContainer } from "../element/typeChecks";
|
||||||
import {
|
import {
|
||||||
getElementsInResizingFrame,
|
getElementsInResizingFrame,
|
||||||
@@ -51,14 +51,12 @@ const allElementsInSameGroup = (elements: readonly ExcalidrawElement[]) => {
|
|||||||
const enableActionGroup = (
|
const enableActionGroup = (
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly ExcalidrawElement[],
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
|
app: AppClassProperties,
|
||||||
) => {
|
) => {
|
||||||
const selectedElements = getSelectedElements(
|
const selectedElements = app.scene.getSelectedElements({
|
||||||
getNonDeletedElements(elements),
|
selectedElementIds: appState.selectedElementIds,
|
||||||
appState,
|
includeBoundTextElement: true,
|
||||||
{
|
});
|
||||||
includeBoundTextElement: true,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
return (
|
return (
|
||||||
selectedElements.length >= 2 && !allElementsInSameGroup(selectedElements)
|
selectedElements.length >= 2 && !allElementsInSameGroup(selectedElements)
|
||||||
);
|
);
|
||||||
@@ -68,13 +66,10 @@ export const actionGroup = register({
|
|||||||
name: "group",
|
name: "group",
|
||||||
trackEvent: { category: "element" },
|
trackEvent: { category: "element" },
|
||||||
perform: (elements, appState, _, app) => {
|
perform: (elements, appState, _, app) => {
|
||||||
const selectedElements = getSelectedElements(
|
const selectedElements = app.scene.getSelectedElements({
|
||||||
getNonDeletedElements(elements),
|
selectedElementIds: appState.selectedElementIds,
|
||||||
appState,
|
includeBoundTextElement: true,
|
||||||
{
|
});
|
||||||
includeBoundTextElement: true,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
if (selectedElements.length < 2) {
|
if (selectedElements.length < 2) {
|
||||||
// nothing to group
|
// nothing to group
|
||||||
return { appState, elements, commitToHistory: false };
|
return { appState, elements, commitToHistory: false };
|
||||||
@@ -164,12 +159,13 @@ export const actionGroup = register({
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
contextItemLabel: "labels.group",
|
contextItemLabel: "labels.group",
|
||||||
predicate: (elements, appState) => enableActionGroup(elements, appState),
|
predicate: (elements, appState, _, app) =>
|
||||||
|
enableActionGroup(elements, appState, app),
|
||||||
keyTest: (event) =>
|
keyTest: (event) =>
|
||||||
!event.shiftKey && event[KEYS.CTRL_OR_CMD] && event.key === KEYS.G,
|
!event.shiftKey && event[KEYS.CTRL_OR_CMD] && event.key === KEYS.G,
|
||||||
PanelComponent: ({ elements, appState, updateData }) => (
|
PanelComponent: ({ elements, appState, updateData, app }) => (
|
||||||
<ToolButton
|
<ToolButton
|
||||||
hidden={!enableActionGroup(elements, appState)}
|
hidden={!enableActionGroup(elements, appState, app)}
|
||||||
type="button"
|
type="button"
|
||||||
icon={<GroupIcon theme={appState.theme} />}
|
icon={<GroupIcon theme={appState.theme} />}
|
||||||
onClick={() => updateData(null)}
|
onClick={() => updateData(null)}
|
||||||
@@ -191,7 +187,7 @@ export const actionUngroup = register({
|
|||||||
|
|
||||||
let nextElements = [...elements];
|
let nextElements = [...elements];
|
||||||
|
|
||||||
const selectedElements = getSelectedElements(nextElements, appState);
|
const selectedElements = app.scene.getSelectedElements(appState);
|
||||||
const frames = selectedElements
|
const frames = selectedElements
|
||||||
.filter((element) => element.frameId)
|
.filter((element) => element.frameId)
|
||||||
.map((element) =>
|
.map((element) =>
|
||||||
@@ -218,6 +214,8 @@ export const actionUngroup = register({
|
|||||||
const updateAppState = selectGroupsForSelectedElements(
|
const updateAppState = selectGroupsForSelectedElements(
|
||||||
{ ...appState, selectedGroupIds: {} },
|
{ ...appState, selectedGroupIds: {} },
|
||||||
getNonDeletedElements(nextElements),
|
getNonDeletedElements(nextElements),
|
||||||
|
appState,
|
||||||
|
null,
|
||||||
);
|
);
|
||||||
|
|
||||||
frames.forEach((frame) => {
|
frames.forEach((frame) => {
|
||||||
@@ -232,9 +230,18 @@ export const actionUngroup = register({
|
|||||||
});
|
});
|
||||||
|
|
||||||
// remove binded text elements from selection
|
// remove binded text elements from selection
|
||||||
boundTextElementIds.forEach(
|
updateAppState.selectedElementIds = Object.entries(
|
||||||
(id) => (updateAppState.selectedElementIds[id] = false),
|
updateAppState.selectedElementIds,
|
||||||
|
).reduce(
|
||||||
|
(acc: { [key: ExcalidrawElement["id"]]: true }, [id, selected]) => {
|
||||||
|
if (selected && !boundTextElementIds.includes(id)) {
|
||||||
|
acc[id] = true;
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{},
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
appState: updateAppState,
|
appState: updateAppState,
|
||||||
elements: nextElements,
|
elements: nextElements,
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import { getNonDeletedElements } from "../element";
|
|
||||||
import { LinearElementEditor } from "../element/linearElementEditor";
|
import { LinearElementEditor } from "../element/linearElementEditor";
|
||||||
import { isLinearElement } from "../element/typeChecks";
|
import { isLinearElement } from "../element/typeChecks";
|
||||||
import { ExcalidrawLinearElement } from "../element/types";
|
import { ExcalidrawLinearElement } from "../element/types";
|
||||||
import { getSelectedElements } from "../scene";
|
|
||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
|
|
||||||
export const actionToggleLinearEditor = register({
|
export const actionToggleLinearEditor = register({
|
||||||
@@ -10,21 +8,18 @@ export const actionToggleLinearEditor = register({
|
|||||||
trackEvent: {
|
trackEvent: {
|
||||||
category: "element",
|
category: "element",
|
||||||
},
|
},
|
||||||
predicate: (elements, appState) => {
|
predicate: (elements, appState, _, app) => {
|
||||||
const selectedElements = getSelectedElements(elements, appState);
|
const selectedElements = app.scene.getSelectedElements(appState);
|
||||||
if (selectedElements.length === 1 && isLinearElement(selectedElements[0])) {
|
if (selectedElements.length === 1 && isLinearElement(selectedElements[0])) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
perform(elements, appState, _, app) {
|
perform(elements, appState, _, app) {
|
||||||
const selectedElement = getSelectedElements(
|
const selectedElement = app.scene.getSelectedElements({
|
||||||
getNonDeletedElements(elements),
|
selectedElementIds: appState.selectedElementIds,
|
||||||
appState,
|
includeBoundTextElement: true,
|
||||||
{
|
})[0] as ExcalidrawLinearElement;
|
||||||
includeBoundTextElement: true,
|
|
||||||
},
|
|
||||||
)[0] as ExcalidrawLinearElement;
|
|
||||||
|
|
||||||
const editingLinearElement =
|
const editingLinearElement =
|
||||||
appState.editingLinearElement?.elementId === selectedElement.id
|
appState.editingLinearElement?.elementId === selectedElement.id
|
||||||
@@ -38,14 +33,11 @@ export const actionToggleLinearEditor = register({
|
|||||||
commitToHistory: false,
|
commitToHistory: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
contextItemLabel: (elements, appState) => {
|
contextItemLabel: (elements, appState, app) => {
|
||||||
const selectedElement = getSelectedElements(
|
const selectedElement = app.scene.getSelectedElements({
|
||||||
getNonDeletedElements(elements),
|
selectedElementIds: appState.selectedElementIds,
|
||||||
appState,
|
includeBoundTextElement: true,
|
||||||
{
|
})[0] as ExcalidrawLinearElement;
|
||||||
includeBoundTextElement: true,
|
|
||||||
},
|
|
||||||
)[0] as ExcalidrawLinearElement;
|
|
||||||
return appState.editingLinearElement?.elementId === selectedElement.id
|
return appState.editingLinearElement?.elementId === selectedElement.id
|
||||||
? "labels.lineEditor.exit"
|
? "labels.lineEditor.exit"
|
||||||
: "labels.lineEditor.edit";
|
: "labels.lineEditor.edit";
|
||||||
|
|||||||
@@ -41,6 +41,8 @@ export const actionSelectAll = register({
|
|||||||
selectedElementIds,
|
selectedElementIds,
|
||||||
},
|
},
|
||||||
getNonDeletedElements(elements),
|
getNonDeletedElements(elements),
|
||||||
|
appState,
|
||||||
|
app,
|
||||||
),
|
),
|
||||||
commitToHistory: true,
|
commitToHistory: true,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ export class ActionManager {
|
|||||||
event,
|
event,
|
||||||
this.getAppState(),
|
this.getAppState(),
|
||||||
this.getElementsIncludingDeleted(),
|
this.getElementsIncludingDeleted(),
|
||||||
|
this.app,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -168,6 +169,7 @@ export class ActionManager {
|
|||||||
appState={this.getAppState()}
|
appState={this.getAppState()}
|
||||||
updateData={updateData}
|
updateData={updateData}
|
||||||
appProps={this.app.props}
|
appProps={this.app.props}
|
||||||
|
app={this.app}
|
||||||
data={data}
|
data={data}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -82,7 +82,8 @@ export type ActionName =
|
|||||||
| "zoomOut"
|
| "zoomOut"
|
||||||
| "resetZoom"
|
| "resetZoom"
|
||||||
| "zoomToFit"
|
| "zoomToFit"
|
||||||
| "zoomToSelection"
|
| "zoomToFitSelection"
|
||||||
|
| "zoomToFitSelectionInViewport"
|
||||||
| "changeFontFamily"
|
| "changeFontFamily"
|
||||||
| "changeTextAlign"
|
| "changeTextAlign"
|
||||||
| "changeVerticalAlign"
|
| "changeVerticalAlign"
|
||||||
@@ -118,7 +119,7 @@ export type ActionName =
|
|||||||
| "toggleHandTool"
|
| "toggleHandTool"
|
||||||
| "selectAllElementsInFrame"
|
| "selectAllElementsInFrame"
|
||||||
| "removeAllElementsFromFrame"
|
| "removeAllElementsFromFrame"
|
||||||
| "toggleFrameRendering"
|
| "updateFrameRendering"
|
||||||
| "setFrameAsActiveTool"
|
| "setFrameAsActiveTool"
|
||||||
| "createContainerFromText"
|
| "createContainerFromText"
|
||||||
| "wrapTextInContainer";
|
| "wrapTextInContainer";
|
||||||
@@ -129,6 +130,7 @@ export type PanelComponentProps = {
|
|||||||
updateData: (formData?: any) => void;
|
updateData: (formData?: any) => void;
|
||||||
appProps: ExcalidrawProps;
|
appProps: ExcalidrawProps;
|
||||||
data?: Record<string, any>;
|
data?: Record<string, any>;
|
||||||
|
app: AppClassProperties;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface Action {
|
export interface Action {
|
||||||
@@ -140,12 +142,14 @@ export interface Action {
|
|||||||
event: React.KeyboardEvent | KeyboardEvent,
|
event: React.KeyboardEvent | KeyboardEvent,
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly ExcalidrawElement[],
|
||||||
|
app: AppClassProperties,
|
||||||
) => boolean;
|
) => boolean;
|
||||||
contextItemLabel?:
|
contextItemLabel?:
|
||||||
| string
|
| string
|
||||||
| ((
|
| ((
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly ExcalidrawElement[],
|
||||||
appState: Readonly<AppState>,
|
appState: Readonly<AppState>,
|
||||||
|
app: AppClassProperties,
|
||||||
) => string);
|
) => string);
|
||||||
predicate?: (
|
predicate?: (
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly ExcalidrawElement[],
|
||||||
|
|||||||
+5
-14
@@ -5,6 +5,9 @@ export const trackEvent = (
|
|||||||
value?: number,
|
value?: number,
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
|
// place here categories that you want to track as events
|
||||||
|
// KEEP IN MIND THE PRICING
|
||||||
|
const ALLOWED_CATEGORIES_TO_TRACK = [] as string[];
|
||||||
// Uncomment the next line to track locally
|
// Uncomment the next line to track locally
|
||||||
// console.log("Track Event", { category, action, label, value });
|
// console.log("Track Event", { category, action, label, value });
|
||||||
|
|
||||||
@@ -12,12 +15,8 @@ export const trackEvent = (
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (process.env.REACT_APP_GOOGLE_ANALYTICS_ID && window.gtag) {
|
if (!ALLOWED_CATEGORIES_TO_TRACK.includes(category)) {
|
||||||
window.gtag("event", action, {
|
return;
|
||||||
event_category: category,
|
|
||||||
event_label: label,
|
|
||||||
value,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (window.sa_event) {
|
if (window.sa_event) {
|
||||||
@@ -27,14 +26,6 @@ export const trackEvent = (
|
|||||||
value,
|
value,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (window.fathom) {
|
|
||||||
window.fathom.trackEvent(action, {
|
|
||||||
category,
|
|
||||||
label,
|
|
||||||
value,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("error during analytics", error);
|
console.error("error during analytics", error);
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -84,7 +84,7 @@ export const getDefaultAppState = (): Omit<
|
|||||||
showStats: false,
|
showStats: false,
|
||||||
startBoundElement: null,
|
startBoundElement: null,
|
||||||
suggestedBindings: [],
|
suggestedBindings: [],
|
||||||
shouldRenderFrames: true,
|
frameRendering: { enabled: true, clip: true, name: true, outline: true },
|
||||||
frameToHighlight: null,
|
frameToHighlight: null,
|
||||||
editingFrame: null,
|
editingFrame: null,
|
||||||
elementsToHighlight: null,
|
elementsToHighlight: null,
|
||||||
@@ -191,7 +191,7 @@ const APP_STATE_STORAGE_CONF = (<
|
|||||||
showStats: { browser: true, export: false, server: false },
|
showStats: { browser: true, export: false, server: false },
|
||||||
startBoundElement: { browser: false, export: false, server: false },
|
startBoundElement: { browser: false, export: false, server: false },
|
||||||
suggestedBindings: { browser: false, export: false, server: false },
|
suggestedBindings: { browser: false, export: false, server: false },
|
||||||
shouldRenderFrames: { browser: false, export: false, server: false },
|
frameRendering: { browser: false, export: false, server: false },
|
||||||
frameToHighlight: { browser: false, export: false, server: false },
|
frameToHighlight: { browser: false, export: false, server: false },
|
||||||
editingFrame: { browser: false, export: false, server: false },
|
editingFrame: { browser: false, export: false, server: false },
|
||||||
elementsToHighlight: { browser: false, export: false, server: false },
|
elementsToHighlight: { browser: false, export: false, server: false },
|
||||||
|
|||||||
+3
-3
@@ -180,7 +180,7 @@ const commonProps = {
|
|||||||
locked: false,
|
locked: false,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
const getChartDimentions = (spreadsheet: Spreadsheet) => {
|
const getChartDimensions = (spreadsheet: Spreadsheet) => {
|
||||||
const chartWidth =
|
const chartWidth =
|
||||||
(BAR_WIDTH + BAR_GAP) * spreadsheet.values.length + BAR_GAP;
|
(BAR_WIDTH + BAR_GAP) * spreadsheet.values.length + BAR_GAP;
|
||||||
const chartHeight = BAR_HEIGHT + BAR_GAP * 2;
|
const chartHeight = BAR_HEIGHT + BAR_GAP * 2;
|
||||||
@@ -250,7 +250,7 @@ const chartLines = (
|
|||||||
groupId: string,
|
groupId: string,
|
||||||
backgroundColor: string,
|
backgroundColor: string,
|
||||||
): ChartElements => {
|
): ChartElements => {
|
||||||
const { chartWidth, chartHeight } = getChartDimentions(spreadsheet);
|
const { chartWidth, chartHeight } = getChartDimensions(spreadsheet);
|
||||||
const xLine = newLinearElement({
|
const xLine = newLinearElement({
|
||||||
backgroundColor,
|
backgroundColor,
|
||||||
groupIds: [groupId],
|
groupIds: [groupId],
|
||||||
@@ -313,7 +313,7 @@ const chartBaseElements = (
|
|||||||
backgroundColor: string,
|
backgroundColor: string,
|
||||||
debug?: boolean,
|
debug?: boolean,
|
||||||
): ChartElements => {
|
): ChartElements => {
|
||||||
const { chartWidth, chartHeight } = getChartDimentions(spreadsheet);
|
const { chartWidth, chartHeight } = getChartDimensions(spreadsheet);
|
||||||
|
|
||||||
const title = spreadsheet.title
|
const title = spreadsheet.title
|
||||||
? newTextElement({
|
? newTextElement({
|
||||||
|
|||||||
+360
-247
File diff suppressed because it is too large
Load Diff
@@ -8,7 +8,7 @@ import {
|
|||||||
} from "./colorPickerUtils";
|
} from "./colorPickerUtils";
|
||||||
import HotkeyLabel from "./HotkeyLabel";
|
import HotkeyLabel from "./HotkeyLabel";
|
||||||
import { ColorPaletteCustom } from "../../colors";
|
import { ColorPaletteCustom } from "../../colors";
|
||||||
import { t } from "../../i18n";
|
import { TranslationKeys, t } from "../../i18n";
|
||||||
|
|
||||||
interface PickerColorListProps {
|
interface PickerColorListProps {
|
||||||
palette: ColorPaletteCustom;
|
palette: ColorPaletteCustom;
|
||||||
@@ -48,7 +48,11 @@ const PickerColorList = ({
|
|||||||
(Array.isArray(value) ? value[activeShade] : value) || "transparent";
|
(Array.isArray(value) ? value[activeShade] : value) || "transparent";
|
||||||
|
|
||||||
const keybinding = colorPickerHotkeyBindings[index];
|
const keybinding = colorPickerHotkeyBindings[index];
|
||||||
const label = t(`colors.${key.replace(/\d+/, "")}`, null, "");
|
const label = t(
|
||||||
|
`colors.${key.replace(/\d+/, "")}` as unknown as TranslationKeys,
|
||||||
|
null,
|
||||||
|
"",
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { Popover } from "./Popover";
|
import { Popover } from "./Popover";
|
||||||
import { t } from "../i18n";
|
import { t, TranslationKeys } from "../i18n";
|
||||||
|
|
||||||
import "./ContextMenu.scss";
|
import "./ContextMenu.scss";
|
||||||
import {
|
import {
|
||||||
@@ -82,9 +82,15 @@ export const ContextMenu = React.memo(
|
|||||||
let label = "";
|
let label = "";
|
||||||
if (item.contextItemLabel) {
|
if (item.contextItemLabel) {
|
||||||
if (typeof item.contextItemLabel === "function") {
|
if (typeof item.contextItemLabel === "function") {
|
||||||
label = t(item.contextItemLabel(elements, appState));
|
label = t(
|
||||||
|
item.contextItemLabel(
|
||||||
|
elements,
|
||||||
|
appState,
|
||||||
|
actionManager.app,
|
||||||
|
) as unknown as TranslationKeys,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
label = t(item.contextItemLabel);
|
label = t(item.contextItemLabel as unknown as TranslationKeys);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,16 +17,34 @@ import { useSetAtom } from "jotai";
|
|||||||
import { isLibraryMenuOpenAtom } from "./LibraryMenu";
|
import { isLibraryMenuOpenAtom } from "./LibraryMenu";
|
||||||
import { jotaiScope } from "../jotai";
|
import { jotaiScope } from "../jotai";
|
||||||
|
|
||||||
|
export type DialogSize = number | "small" | "regular" | "wide" | undefined;
|
||||||
|
|
||||||
export interface DialogProps {
|
export interface DialogProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
className?: string;
|
className?: string;
|
||||||
size?: "small" | "regular" | "wide";
|
size?: DialogSize;
|
||||||
onCloseRequest(): void;
|
onCloseRequest(): void;
|
||||||
title: React.ReactNode | false;
|
title: React.ReactNode | false;
|
||||||
autofocus?: boolean;
|
autofocus?: boolean;
|
||||||
closeOnClickOutside?: boolean;
|
closeOnClickOutside?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getDialogSize(size: DialogSize): number {
|
||||||
|
if (size && typeof size === "number") {
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (size) {
|
||||||
|
case "small":
|
||||||
|
return 550;
|
||||||
|
case "wide":
|
||||||
|
return 1024;
|
||||||
|
case "regular":
|
||||||
|
default:
|
||||||
|
return 800;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const Dialog = (props: DialogProps) => {
|
export const Dialog = (props: DialogProps) => {
|
||||||
const [islandNode, setIslandNode] = useCallbackRefState<HTMLDivElement>();
|
const [islandNode, setIslandNode] = useCallbackRefState<HTMLDivElement>();
|
||||||
const [lastActiveElement] = useState(document.activeElement);
|
const [lastActiveElement] = useState(document.activeElement);
|
||||||
@@ -85,9 +103,7 @@ export const Dialog = (props: DialogProps) => {
|
|||||||
<Modal
|
<Modal
|
||||||
className={clsx("Dialog", props.className)}
|
className={clsx("Dialog", props.className)}
|
||||||
labelledBy="dialog-title"
|
labelledBy="dialog-title"
|
||||||
maxWidth={
|
maxWidth={getDialogSize(props.size)}
|
||||||
props.size === "wide" ? 1024 : props.size === "small" ? 550 : 800
|
|
||||||
}
|
|
||||||
onCloseRequest={onClose}
|
onCloseRequest={onClose}
|
||||||
closeOnClickOutside={props.closeOnClickOutside}
|
closeOnClickOutside={props.closeOnClickOutside}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -2,20 +2,140 @@
|
|||||||
|
|
||||||
.excalidraw {
|
.excalidraw {
|
||||||
.ExcButton {
|
.ExcButton {
|
||||||
&--color-primary {
|
--text-color: transparent;
|
||||||
color: var(--input-bg-color);
|
--border-color: transparent;
|
||||||
|
--back-color: transparent;
|
||||||
|
|
||||||
--accent-color: var(--color-primary);
|
color: var(--text-color);
|
||||||
--accent-color-hover: var(--color-primary-darker);
|
background-color: var(--back-color);
|
||||||
--accent-color-active: var(--color-primary-darkest);
|
border-color: var(--border-color);
|
||||||
|
|
||||||
|
&--color-primary {
|
||||||
|
&.ExcButton--variant-filled {
|
||||||
|
--text-color: var(--input-bg-color);
|
||||||
|
--back-color: var(--color-primary);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
--back-color: var(--color-primary-darker);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
--back-color: var(--color-primary-darkest);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.ExcButton--variant-outlined,
|
||||||
|
&.ExcButton--variant-icon {
|
||||||
|
--text-color: var(--color-primary);
|
||||||
|
--border-color: var(--color-primary);
|
||||||
|
--back-color: var(--input-bg-color);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
--text-color: var(--color-primary-darker);
|
||||||
|
--border-color: var(--color-primary-darker);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
--text-color: var(--color-primary-darkest);
|
||||||
|
--border-color: var(--color-primary-darkest);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&--color-danger {
|
&--color-danger {
|
||||||
color: var(--input-bg-color);
|
&.ExcButton--variant-filled {
|
||||||
|
--text-color: var(--color-danger-text);
|
||||||
|
--back-color: var(--color-danger-dark);
|
||||||
|
|
||||||
--accent-color: var(--color-danger);
|
&:hover {
|
||||||
--accent-color-hover: #d65550;
|
--back-color: var(--color-danger-darker);
|
||||||
--accent-color-active: #d1413c;
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
--back-color: var(--color-danger-darkest);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.ExcButton--variant-outlined,
|
||||||
|
&.ExcButton--variant-icon {
|
||||||
|
--text-color: var(--color-danger);
|
||||||
|
--border-color: var(--color-danger);
|
||||||
|
--back-color: transparent;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
--text-color: var(--color-danger-darkest);
|
||||||
|
--border-color: var(--color-danger-darkest);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
--text-color: var(--color-danger-darker);
|
||||||
|
--border-color: var(--color-danger-darker);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&--color-muted {
|
||||||
|
&.ExcButton--variant-filled {
|
||||||
|
--text-color: var(--island-bg-color);
|
||||||
|
--back-color: var(--color-gray-50);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
--back-color: var(--color-gray-60);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
--back-color: var(--color-gray-80);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.ExcButton--variant-outlined,
|
||||||
|
&.ExcButton--variant-icon {
|
||||||
|
--text-color: var(--color-muted-background);
|
||||||
|
--border-color: var(--color-muted);
|
||||||
|
--back-color: var(--island-bg-color);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
--text-color: var(--color-muted-background-darker);
|
||||||
|
--border-color: var(--color-muted-darker);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
--text-color: var(--color-muted-background-darker);
|
||||||
|
--border-color: var(--color-muted-darkest);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&--color-warning {
|
||||||
|
&.ExcButton--variant-filled {
|
||||||
|
--text-color: black;
|
||||||
|
--back-color: var(--color-warning-dark);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
--back-color: var(--color-warning-darker);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
--back-color: var(--color-warning-darkest);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.ExcButton--variant-outlined,
|
||||||
|
&.ExcButton--variant-icon {
|
||||||
|
--text-color: var(--color-warning-dark);
|
||||||
|
--border-color: var(--color-warning-dark);
|
||||||
|
--back-color: var(--input-bg-color);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
--text-color: var(--color-warning-darker);
|
||||||
|
--border-color: var(--color-warning-darker);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
--text-color: var(--color-warning-darkest);
|
||||||
|
--border-color: var(--color-warning-darkest);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -25,6 +145,8 @@
|
|||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
|
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
|
border-width: 1px;
|
||||||
|
border-style: solid;
|
||||||
|
|
||||||
font-family: "Assistant";
|
font-family: "Assistant";
|
||||||
|
|
||||||
@@ -33,9 +155,9 @@
|
|||||||
transition: all 150ms ease-out;
|
transition: all 150ms ease-out;
|
||||||
|
|
||||||
&--size-large {
|
&--size-large {
|
||||||
font-weight: 400;
|
font-weight: 600;
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
height: 3rem;
|
min-height: 3rem;
|
||||||
padding: 0.5rem 1.5rem;
|
padding: 0.5rem 1.5rem;
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
|
|
||||||
@@ -45,48 +167,22 @@
|
|||||||
&--size-medium {
|
&--size-medium {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
height: 2.5rem;
|
min-height: 2.5rem;
|
||||||
padding: 0.5rem 1rem;
|
padding: 0.5rem 1rem;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
|
|
||||||
letter-spacing: normal;
|
letter-spacing: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
&--variant-filled {
|
|
||||||
background: var(--accent-color);
|
|
||||||
border: 1px solid transparent;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: var(--accent-color-hover);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:active {
|
|
||||||
background: var(--accent-color-active);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&--variant-outlined,
|
|
||||||
&--variant-icon {
|
|
||||||
border: 1px solid var(--accent-color);
|
|
||||||
color: var(--accent-color);
|
|
||||||
background: transparent;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
border: 1px solid var(--accent-color-hover);
|
|
||||||
color: var(--accent-color-hover);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:active {
|
|
||||||
border: 1px solid var(--accent-color-active);
|
|
||||||
color: var(--accent-color-active);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&--variant-icon {
|
&--variant-icon {
|
||||||
padding: 0.5rem 0.75rem;
|
padding: 0.5rem 0.75rem;
|
||||||
width: 3rem;
|
width: 3rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&--fullWidth {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
&__icon {
|
&__icon {
|
||||||
width: 1.25rem;
|
width: 1.25rem;
|
||||||
height: 1.25rem;
|
height: 1.25rem;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import clsx from "clsx";
|
|||||||
import "./FilledButton.scss";
|
import "./FilledButton.scss";
|
||||||
|
|
||||||
export type ButtonVariant = "filled" | "outlined" | "icon";
|
export type ButtonVariant = "filled" | "outlined" | "icon";
|
||||||
export type ButtonColor = "primary" | "danger";
|
export type ButtonColor = "primary" | "danger" | "warning" | "muted";
|
||||||
export type ButtonSize = "medium" | "large";
|
export type ButtonSize = "medium" | "large";
|
||||||
|
|
||||||
export type FilledButtonProps = {
|
export type FilledButtonProps = {
|
||||||
@@ -17,6 +17,7 @@ export type FilledButtonProps = {
|
|||||||
color?: ButtonColor;
|
color?: ButtonColor;
|
||||||
size?: ButtonSize;
|
size?: ButtonSize;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
fullWidth?: boolean;
|
||||||
|
|
||||||
startIcon?: React.ReactNode;
|
startIcon?: React.ReactNode;
|
||||||
};
|
};
|
||||||
@@ -31,6 +32,7 @@ export const FilledButton = forwardRef<HTMLButtonElement, FilledButtonProps>(
|
|||||||
variant = "filled",
|
variant = "filled",
|
||||||
color = "primary",
|
color = "primary",
|
||||||
size = "medium",
|
size = "medium",
|
||||||
|
fullWidth,
|
||||||
className,
|
className,
|
||||||
},
|
},
|
||||||
ref,
|
ref,
|
||||||
@@ -42,6 +44,7 @@ export const FilledButton = forwardRef<HTMLButtonElement, FilledButtonProps>(
|
|||||||
`ExcButton--color-${color}`,
|
`ExcButton--color-${color}`,
|
||||||
`ExcButton--variant-${variant}`,
|
`ExcButton--variant-${variant}`,
|
||||||
`ExcButton--size-${size}`,
|
`ExcButton--size-${size}`,
|
||||||
|
{ "ExcButton--fullWidth": fullWidth },
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ const Header = () => (
|
|||||||
<div className="HelpDialog__header">
|
<div className="HelpDialog__header">
|
||||||
<a
|
<a
|
||||||
className="HelpDialog__btn"
|
className="HelpDialog__btn"
|
||||||
href="https://github.com/excalidraw/excalidraw#documentation"
|
href="https://docs.excalidraw.com"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { NonDeletedExcalidrawElement } from "../element/types";
|
import { AppClassProperties, Device, UIAppState } from "../types";
|
||||||
import { getSelectedElements } from "../scene";
|
|
||||||
import { Device, UIAppState } from "../types";
|
|
||||||
import {
|
import {
|
||||||
isImageElement,
|
isImageElement,
|
||||||
isLinearElement,
|
isLinearElement,
|
||||||
@@ -15,17 +13,12 @@ import "./HintViewer.scss";
|
|||||||
|
|
||||||
interface HintViewerProps {
|
interface HintViewerProps {
|
||||||
appState: UIAppState;
|
appState: UIAppState;
|
||||||
elements: readonly NonDeletedExcalidrawElement[];
|
|
||||||
isMobile: boolean;
|
isMobile: boolean;
|
||||||
device: Device;
|
device: Device;
|
||||||
|
app: AppClassProperties;
|
||||||
}
|
}
|
||||||
|
|
||||||
const getHints = ({
|
const getHints = ({ appState, isMobile, device, app }: HintViewerProps) => {
|
||||||
appState,
|
|
||||||
elements,
|
|
||||||
isMobile,
|
|
||||||
device,
|
|
||||||
}: HintViewerProps) => {
|
|
||||||
const { activeTool, isResizing, isRotating, lastPointerDownWith } = appState;
|
const { activeTool, isResizing, isRotating, lastPointerDownWith } = appState;
|
||||||
const multiMode = appState.multiElement !== null;
|
const multiMode = appState.multiElement !== null;
|
||||||
|
|
||||||
@@ -55,7 +48,7 @@ const getHints = ({
|
|||||||
return t("hints.placeImage");
|
return t("hints.placeImage");
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectedElements = getSelectedElements(elements, appState);
|
const selectedElements = app.scene.getSelectedElements(appState);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
isResizing &&
|
isResizing &&
|
||||||
@@ -115,15 +108,15 @@ const getHints = ({
|
|||||||
|
|
||||||
export const HintViewer = ({
|
export const HintViewer = ({
|
||||||
appState,
|
appState,
|
||||||
elements,
|
|
||||||
isMobile,
|
isMobile,
|
||||||
device,
|
device,
|
||||||
|
app,
|
||||||
}: HintViewerProps) => {
|
}: HintViewerProps) => {
|
||||||
let hint = getHints({
|
let hint = getHints({
|
||||||
appState,
|
appState,
|
||||||
elements,
|
|
||||||
isMobile,
|
isMobile,
|
||||||
device,
|
device,
|
||||||
|
app,
|
||||||
});
|
});
|
||||||
if (!hint) {
|
if (!hint) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ import { jotaiScope } from "../jotai";
|
|||||||
import { Provider, useAtom, useAtomValue } from "jotai";
|
import { Provider, useAtom, useAtomValue } from "jotai";
|
||||||
import MainMenu from "./main-menu/MainMenu";
|
import MainMenu from "./main-menu/MainMenu";
|
||||||
import { ActiveConfirmDialog } from "./ActiveConfirmDialog";
|
import { ActiveConfirmDialog } from "./ActiveConfirmDialog";
|
||||||
|
import { OverwriteConfirmDialog } from "./OverwriteConfirm/OverwriteConfirm";
|
||||||
import { HandButton } from "./HandButton";
|
import { HandButton } from "./HandButton";
|
||||||
import { isHandToolActive } from "../appState";
|
import { isHandToolActive } from "../appState";
|
||||||
import { TunnelsContext, useInitializeTunnels } from "../context/tunnels";
|
import { TunnelsContext, useInitializeTunnels } from "../context/tunnels";
|
||||||
@@ -71,6 +72,7 @@ interface LayerUIProps {
|
|||||||
onExportImage: AppClassProperties["onExportImage"];
|
onExportImage: AppClassProperties["onExportImage"];
|
||||||
renderWelcomeScreen: boolean;
|
renderWelcomeScreen: boolean;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
|
app: AppClassProperties;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DefaultMainMenu: React.FC<{
|
const DefaultMainMenu: React.FC<{
|
||||||
@@ -99,6 +101,15 @@ const DefaultMainMenu: React.FC<{
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const DefaultOverwriteConfirmDialog = () => {
|
||||||
|
return (
|
||||||
|
<OverwriteConfirmDialog __fallback>
|
||||||
|
<OverwriteConfirmDialog.Actions.SaveToDisk />
|
||||||
|
<OverwriteConfirmDialog.Actions.ExportToImage />
|
||||||
|
</OverwriteConfirmDialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const LayerUI = ({
|
const LayerUI = ({
|
||||||
actionManager,
|
actionManager,
|
||||||
appState,
|
appState,
|
||||||
@@ -117,6 +128,7 @@ const LayerUI = ({
|
|||||||
onExportImage,
|
onExportImage,
|
||||||
renderWelcomeScreen,
|
renderWelcomeScreen,
|
||||||
children,
|
children,
|
||||||
|
app,
|
||||||
}: LayerUIProps) => {
|
}: LayerUIProps) => {
|
||||||
const device = useDevice();
|
const device = useDevice();
|
||||||
const tunnels = useInitializeTunnels();
|
const tunnels = useInitializeTunnels();
|
||||||
@@ -230,9 +242,9 @@ const LayerUI = ({
|
|||||||
>
|
>
|
||||||
<HintViewer
|
<HintViewer
|
||||||
appState={appState}
|
appState={appState}
|
||||||
elements={elements}
|
|
||||||
isMobile={device.isMobile}
|
isMobile={device.isMobile}
|
||||||
device={device}
|
device={device}
|
||||||
|
app={app}
|
||||||
/>
|
/>
|
||||||
{heading}
|
{heading}
|
||||||
<Stack.Row gap={1}>
|
<Stack.Row gap={1}>
|
||||||
@@ -343,6 +355,7 @@ const LayerUI = ({
|
|||||||
>
|
>
|
||||||
{t("toolBar.library")}
|
{t("toolBar.library")}
|
||||||
</DefaultSidebar.Trigger>
|
</DefaultSidebar.Trigger>
|
||||||
|
<DefaultOverwriteConfirmDialog />
|
||||||
{/* ------------------------------------------------------------------ */}
|
{/* ------------------------------------------------------------------ */}
|
||||||
|
|
||||||
{appState.isLoading && <LoadingMessage delay={250} />}
|
{appState.isLoading && <LoadingMessage delay={250} />}
|
||||||
@@ -374,6 +387,7 @@ const LayerUI = ({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<ActiveConfirmDialog />
|
<ActiveConfirmDialog />
|
||||||
|
<tunnels.OverwriteConfirmDialogTunnel.Out />
|
||||||
{renderImageExportDialog()}
|
{renderImageExportDialog()}
|
||||||
{renderJSONExportDialog()}
|
{renderJSONExportDialog()}
|
||||||
{appState.pasteDialog.shown && (
|
{appState.pasteDialog.shown && (
|
||||||
@@ -387,8 +401,9 @@ const LayerUI = ({
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{device.isMobile && !eyeDropperState && (
|
{device.isMobile && (
|
||||||
<MobileMenu
|
<MobileMenu
|
||||||
|
app={app}
|
||||||
appState={appState}
|
appState={appState}
|
||||||
elements={elements}
|
elements={elements}
|
||||||
actionManager={actionManager}
|
actionManager={actionManager}
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { AppState, Device, ExcalidrawProps, UIAppState } from "../types";
|
import {
|
||||||
|
AppClassProperties,
|
||||||
|
AppState,
|
||||||
|
Device,
|
||||||
|
ExcalidrawProps,
|
||||||
|
UIAppState,
|
||||||
|
} from "../types";
|
||||||
import { ActionManager } from "../actions/manager";
|
import { ActionManager } from "../actions/manager";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import Stack from "./Stack";
|
import Stack from "./Stack";
|
||||||
@@ -41,6 +47,7 @@ type MobileMenuProps = {
|
|||||||
renderSidebars: () => JSX.Element | null;
|
renderSidebars: () => JSX.Element | null;
|
||||||
device: Device;
|
device: Device;
|
||||||
renderWelcomeScreen: boolean;
|
renderWelcomeScreen: boolean;
|
||||||
|
app: AppClassProperties;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const MobileMenu = ({
|
export const MobileMenu = ({
|
||||||
@@ -58,6 +65,7 @@ export const MobileMenu = ({
|
|||||||
renderSidebars,
|
renderSidebars,
|
||||||
device,
|
device,
|
||||||
renderWelcomeScreen,
|
renderWelcomeScreen,
|
||||||
|
app,
|
||||||
}: MobileMenuProps) => {
|
}: MobileMenuProps) => {
|
||||||
const {
|
const {
|
||||||
WelcomeScreenCenterTunnel,
|
WelcomeScreenCenterTunnel,
|
||||||
@@ -119,9 +127,9 @@ export const MobileMenu = ({
|
|||||||
</Section>
|
</Section>
|
||||||
<HintViewer
|
<HintViewer
|
||||||
appState={appState}
|
appState={appState}
|
||||||
elements={elements}
|
|
||||||
isMobile={true}
|
isMobile={true}
|
||||||
device={device}
|
device={device}
|
||||||
|
app={app}
|
||||||
/>
|
/>
|
||||||
</FixedSideContainer>
|
</FixedSideContainer>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
.excalidraw {
|
.excalidraw {
|
||||||
&.excalidraw-modal-container {
|
&.excalidraw-modal-container {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 10;
|
z-index: var(--zIndex-modal);
|
||||||
}
|
}
|
||||||
|
|
||||||
.Modal {
|
.Modal {
|
||||||
|
|||||||
@@ -0,0 +1,126 @@
|
|||||||
|
@import "../../css/variables.module";
|
||||||
|
|
||||||
|
.excalidraw {
|
||||||
|
.OverwriteConfirm {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
isolation: isolate;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 1.3125rem;
|
||||||
|
line-height: 130%;
|
||||||
|
align-self: flex-start;
|
||||||
|
|
||||||
|
color: var(--text-primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__Description {
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
gap: 1rem;
|
||||||
|
|
||||||
|
@include isMobile {
|
||||||
|
flex-direction: column;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
padding: 2.5rem;
|
||||||
|
|
||||||
|
background: var(--color-danger-background);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
|
||||||
|
font-family: "Assistant";
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 150%;
|
||||||
|
|
||||||
|
color: var(--color-danger-color);
|
||||||
|
|
||||||
|
&__spacer {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__icon {
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 2.5rem;
|
||||||
|
background: var(--color-danger-icon-background);
|
||||||
|
width: 3.5rem;
|
||||||
|
height: 3.5rem;
|
||||||
|
|
||||||
|
padding: 0.75rem;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
color: var(--color-danger-icon-color);
|
||||||
|
width: 1.5rem;
|
||||||
|
height: 1.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.OverwriteConfirm__Description--color-warning {
|
||||||
|
background: var(--color-warning-background);
|
||||||
|
color: var(--color-warning-color);
|
||||||
|
|
||||||
|
.OverwriteConfirm__Description__icon {
|
||||||
|
background: var(--color-warning-icon-background);
|
||||||
|
flex: 0 0 auto;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
color: var(--color-warning-icon-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__Actions {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: stretch;
|
||||||
|
justify-items: stretch;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 1.5rem;
|
||||||
|
|
||||||
|
@include isMobile {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__Action {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
padding: 1.5rem;
|
||||||
|
gap: 0.75rem;
|
||||||
|
flex-basis: 50%;
|
||||||
|
flex-grow: 0;
|
||||||
|
|
||||||
|
&__content {
|
||||||
|
height: 100%;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 1.125rem;
|
||||||
|
line-height: 130%;
|
||||||
|
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
color: var(--text-primary-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { useAtom } from "jotai";
|
||||||
|
|
||||||
|
import { useTunnels } from "../../context/tunnels";
|
||||||
|
import { jotaiScope } from "../../jotai";
|
||||||
|
import { Dialog } from "../Dialog";
|
||||||
|
import { withInternalFallback } from "../hoc/withInternalFallback";
|
||||||
|
import { overwriteConfirmStateAtom } from "./OverwriteConfirmState";
|
||||||
|
|
||||||
|
import { FilledButton } from "../FilledButton";
|
||||||
|
import { alertTriangleIcon } from "../icons";
|
||||||
|
import { Actions, Action } from "./OverwriteConfirmActions";
|
||||||
|
import "./OverwriteConfirm.scss";
|
||||||
|
|
||||||
|
export type OverwriteConfirmDialogProps = {
|
||||||
|
children: React.ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
const OverwriteConfirmDialog = Object.assign(
|
||||||
|
withInternalFallback(
|
||||||
|
"OverwriteConfirmDialog",
|
||||||
|
({ children }: OverwriteConfirmDialogProps) => {
|
||||||
|
const { OverwriteConfirmDialogTunnel } = useTunnels();
|
||||||
|
const [overwriteConfirmState, setState] = useAtom(
|
||||||
|
overwriteConfirmStateAtom,
|
||||||
|
jotaiScope,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!overwriteConfirmState.active) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
overwriteConfirmState.onClose();
|
||||||
|
setState((state) => ({ ...state, active: false }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleConfirm = () => {
|
||||||
|
overwriteConfirmState.onConfirm();
|
||||||
|
setState((state) => ({ ...state, active: false }));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<OverwriteConfirmDialogTunnel.In>
|
||||||
|
<Dialog onCloseRequest={handleClose} title={false} size={916}>
|
||||||
|
<div className="OverwriteConfirm">
|
||||||
|
<h3>{overwriteConfirmState.title}</h3>
|
||||||
|
<div
|
||||||
|
className={`OverwriteConfirm__Description OverwriteConfirm__Description--color-${overwriteConfirmState.color}`}
|
||||||
|
>
|
||||||
|
<div className="OverwriteConfirm__Description__icon">
|
||||||
|
{alertTriangleIcon}
|
||||||
|
</div>
|
||||||
|
<div>{overwriteConfirmState.description}</div>
|
||||||
|
<div className="OverwriteConfirm__Description__spacer"></div>
|
||||||
|
<FilledButton
|
||||||
|
color={overwriteConfirmState.color}
|
||||||
|
size="large"
|
||||||
|
label={overwriteConfirmState.actionLabel}
|
||||||
|
onClick={handleConfirm}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Actions>{children}</Actions>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
</OverwriteConfirmDialogTunnel.In>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
{
|
||||||
|
Actions,
|
||||||
|
Action,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export { OverwriteConfirmDialog };
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { FilledButton } from "../FilledButton";
|
||||||
|
import { useExcalidrawActionManager, useExcalidrawSetAppState } from "../App";
|
||||||
|
import { actionSaveFileToDisk } from "../../actions";
|
||||||
|
import { useI18n } from "../../i18n";
|
||||||
|
import { actionChangeExportEmbedScene } from "../../actions/actionExport";
|
||||||
|
|
||||||
|
export type ActionProps = {
|
||||||
|
title: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
actionLabel: string;
|
||||||
|
onClick: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Action = ({
|
||||||
|
title,
|
||||||
|
children,
|
||||||
|
actionLabel,
|
||||||
|
onClick,
|
||||||
|
}: ActionProps) => {
|
||||||
|
return (
|
||||||
|
<div className="OverwriteConfirm__Actions__Action">
|
||||||
|
<h4>{title}</h4>
|
||||||
|
<div className="OverwriteConfirm__Actions__Action__content">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
<FilledButton
|
||||||
|
variant="outlined"
|
||||||
|
color="muted"
|
||||||
|
label={actionLabel}
|
||||||
|
size="large"
|
||||||
|
fullWidth
|
||||||
|
onClick={onClick}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ExportToImage = () => {
|
||||||
|
const { t } = useI18n();
|
||||||
|
const actionManager = useExcalidrawActionManager();
|
||||||
|
const setAppState = useExcalidrawSetAppState();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Action
|
||||||
|
title={t("overwriteConfirm.action.exportToImage.title")}
|
||||||
|
actionLabel={t("overwriteConfirm.action.exportToImage.button")}
|
||||||
|
onClick={() => {
|
||||||
|
actionManager.executeAction(actionChangeExportEmbedScene, "ui", true);
|
||||||
|
setAppState({ openDialog: "imageExport" });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("overwriteConfirm.action.exportToImage.description")}
|
||||||
|
</Action>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SaveToDisk = () => {
|
||||||
|
const { t } = useI18n();
|
||||||
|
const actionManager = useExcalidrawActionManager();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Action
|
||||||
|
title={t("overwriteConfirm.action.saveToDisk.title")}
|
||||||
|
actionLabel={t("overwriteConfirm.action.saveToDisk.button")}
|
||||||
|
onClick={() => {
|
||||||
|
actionManager.executeAction(actionSaveFileToDisk, "ui");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("overwriteConfirm.action.saveToDisk.description")}
|
||||||
|
</Action>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const Actions = Object.assign(
|
||||||
|
({ children }: { children: React.ReactNode }) => {
|
||||||
|
return <div className="OverwriteConfirm__Actions">{children}</div>;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ExportToImage,
|
||||||
|
SaveToDisk,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export { Actions };
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { atom } from "jotai";
|
||||||
|
import { jotaiStore } from "../../jotai";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
export type OverwriteConfirmState =
|
||||||
|
| {
|
||||||
|
active: true;
|
||||||
|
title: string;
|
||||||
|
description: React.ReactNode;
|
||||||
|
actionLabel: string;
|
||||||
|
color: "danger" | "warning";
|
||||||
|
|
||||||
|
onClose: () => void;
|
||||||
|
onConfirm: () => void;
|
||||||
|
onReject: () => void;
|
||||||
|
}
|
||||||
|
| { active: false };
|
||||||
|
|
||||||
|
export const overwriteConfirmStateAtom = atom<OverwriteConfirmState>({
|
||||||
|
active: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
export async function openConfirmModal({
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
actionLabel,
|
||||||
|
color,
|
||||||
|
}: {
|
||||||
|
title: string;
|
||||||
|
description: React.ReactNode;
|
||||||
|
actionLabel: string;
|
||||||
|
color: "danger" | "warning";
|
||||||
|
}) {
|
||||||
|
return new Promise<boolean>((resolve) => {
|
||||||
|
jotaiStore.set(overwriteConfirmStateAtom, {
|
||||||
|
active: true,
|
||||||
|
onConfirm: () => resolve(true),
|
||||||
|
onClose: () => resolve(false),
|
||||||
|
onReject: () => resolve(false),
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
actionLabel,
|
||||||
|
color,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@ import { t } from "../i18n";
|
|||||||
import { useExcalidrawContainer } from "./App";
|
import { useExcalidrawContainer } from "./App";
|
||||||
|
|
||||||
export const Section: React.FC<{
|
export const Section: React.FC<{
|
||||||
heading: string;
|
heading: "canvasActions" | "selectedShapeActions" | "shapes";
|
||||||
children?: React.ReactNode | ((heading: React.ReactNode) => React.ReactNode);
|
children?: React.ReactNode | ((heading: React.ReactNode) => React.ReactNode);
|
||||||
className?: string;
|
className?: string;
|
||||||
}> = ({ heading, children, ...props }) => {
|
}> = ({ heading, children, ...props }) => {
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
@import "../css/variables.module";
|
||||||
|
|
||||||
|
.excalidraw {
|
||||||
|
.ShareableLinkDialog {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1.5rem;
|
||||||
|
|
||||||
|
color: var(--text-primary-color);
|
||||||
|
|
||||||
|
::selection {
|
||||||
|
background: var(--color-primary-light-darker);
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-family: "Assistant";
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 1.313rem;
|
||||||
|
line-height: 130%;
|
||||||
|
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__popover {
|
||||||
|
@keyframes RoomDialog__popover__scaleIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
box-sizing: border-box;
|
||||||
|
z-index: 100;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: flex-start;
|
||||||
|
padding: 0.125rem 0.5rem;
|
||||||
|
gap: 0.125rem;
|
||||||
|
|
||||||
|
height: 1.125rem;
|
||||||
|
|
||||||
|
border: none;
|
||||||
|
border-radius: 0.6875rem;
|
||||||
|
|
||||||
|
font-family: "Assistant";
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
line-height: 110%;
|
||||||
|
|
||||||
|
background: var(--color-success-lighter);
|
||||||
|
color: var(--color-success);
|
||||||
|
|
||||||
|
& > svg {
|
||||||
|
width: 0.875rem;
|
||||||
|
height: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
transform-origin: var(--radix-popover-content-transform-origin);
|
||||||
|
animation: RoomDialog__popover__scaleIn 150ms ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__linkRow {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: flex-end;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__description {
|
||||||
|
border-top: 1px solid var(--color-gray-20);
|
||||||
|
|
||||||
|
padding: 0.5rem 0.5rem 0;
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
line-height: 150%;
|
||||||
|
|
||||||
|
& p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
& p + p {
|
||||||
|
margin-top: 1em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
import { useRef, useState } from "react";
|
||||||
|
import * as Popover from "@radix-ui/react-popover";
|
||||||
|
|
||||||
|
import { copyTextToSystemClipboard } from "../clipboard";
|
||||||
|
import { useI18n } from "../i18n";
|
||||||
|
|
||||||
|
import { Dialog } from "./Dialog";
|
||||||
|
import { TextField } from "./TextField";
|
||||||
|
import { FilledButton } from "./FilledButton";
|
||||||
|
import { copyIcon, tablerCheckIcon } from "./icons";
|
||||||
|
|
||||||
|
import "./ShareableLinkDialog.scss";
|
||||||
|
|
||||||
|
export type ShareableLinkDialogProps = {
|
||||||
|
link: string;
|
||||||
|
|
||||||
|
onCloseRequest: () => void;
|
||||||
|
setErrorMessage: (error: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ShareableLinkDialog = ({
|
||||||
|
link,
|
||||||
|
onCloseRequest,
|
||||||
|
setErrorMessage,
|
||||||
|
}: ShareableLinkDialogProps) => {
|
||||||
|
const { t } = useI18n();
|
||||||
|
const [justCopied, setJustCopied] = useState(false);
|
||||||
|
const timerRef = useRef<number>(0);
|
||||||
|
const ref = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
const copyRoomLink = async () => {
|
||||||
|
try {
|
||||||
|
await copyTextToSystemClipboard(link);
|
||||||
|
|
||||||
|
setJustCopied(true);
|
||||||
|
|
||||||
|
if (timerRef.current) {
|
||||||
|
window.clearTimeout(timerRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
timerRef.current = window.setTimeout(() => {
|
||||||
|
setJustCopied(false);
|
||||||
|
}, 3000);
|
||||||
|
} catch (error: any) {
|
||||||
|
setErrorMessage(error.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
ref.current?.select();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog onCloseRequest={onCloseRequest} title={false} size="small">
|
||||||
|
<div className="ShareableLinkDialog">
|
||||||
|
<h3>Shareable link</h3>
|
||||||
|
<div className="ShareableLinkDialog__linkRow">
|
||||||
|
<TextField
|
||||||
|
ref={ref}
|
||||||
|
label="Link"
|
||||||
|
readonly
|
||||||
|
fullWidth
|
||||||
|
value={link}
|
||||||
|
selectOnRender
|
||||||
|
/>
|
||||||
|
<Popover.Root open={justCopied}>
|
||||||
|
<Popover.Trigger asChild>
|
||||||
|
<FilledButton
|
||||||
|
size="large"
|
||||||
|
label="Copy link"
|
||||||
|
startIcon={copyIcon}
|
||||||
|
onClick={copyRoomLink}
|
||||||
|
/>
|
||||||
|
</Popover.Trigger>
|
||||||
|
<Popover.Content
|
||||||
|
onOpenAutoFocus={(event) => event.preventDefault()}
|
||||||
|
onCloseAutoFocus={(event) => event.preventDefault()}
|
||||||
|
className="ShareableLinkDialog__popover"
|
||||||
|
side="top"
|
||||||
|
align="end"
|
||||||
|
sideOffset={5.5}
|
||||||
|
>
|
||||||
|
{tablerCheckIcon} copied
|
||||||
|
</Popover.Content>
|
||||||
|
</Popover.Root>
|
||||||
|
</div>
|
||||||
|
<div className="ShareableLinkDialog__description">
|
||||||
|
🔒 {t("alerts.uploadedSecurly")}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,4 +1,10 @@
|
|||||||
import { forwardRef, useRef, useImperativeHandle, KeyboardEvent } from "react";
|
import {
|
||||||
|
forwardRef,
|
||||||
|
useRef,
|
||||||
|
useImperativeHandle,
|
||||||
|
KeyboardEvent,
|
||||||
|
useLayoutEffect,
|
||||||
|
} from "react";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
|
|
||||||
import "./TextField.scss";
|
import "./TextField.scss";
|
||||||
@@ -12,6 +18,7 @@ export type TextFieldProps = {
|
|||||||
|
|
||||||
readonly?: boolean;
|
readonly?: boolean;
|
||||||
fullWidth?: boolean;
|
fullWidth?: boolean;
|
||||||
|
selectOnRender?: boolean;
|
||||||
|
|
||||||
label?: string;
|
label?: string;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
@@ -19,13 +26,28 @@ export type TextFieldProps = {
|
|||||||
|
|
||||||
export const TextField = forwardRef<HTMLInputElement, TextFieldProps>(
|
export const TextField = forwardRef<HTMLInputElement, TextFieldProps>(
|
||||||
(
|
(
|
||||||
{ value, onChange, label, fullWidth, placeholder, readonly, onKeyDown },
|
{
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
label,
|
||||||
|
fullWidth,
|
||||||
|
placeholder,
|
||||||
|
readonly,
|
||||||
|
selectOnRender,
|
||||||
|
onKeyDown,
|
||||||
|
},
|
||||||
ref,
|
ref,
|
||||||
) => {
|
) => {
|
||||||
const innerRef = useRef<HTMLInputElement | null>(null);
|
const innerRef = useRef<HTMLInputElement | null>(null);
|
||||||
|
|
||||||
useImperativeHandle(ref, () => innerRef.current!);
|
useImperativeHandle(ref, () => innerRef.current!);
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
if (selectOnRender) {
|
||||||
|
innerRef.current?.select();
|
||||||
|
}
|
||||||
|
}, [selectOnRender]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={clsx("ExcTextField", {
|
className={clsx("ExcTextField", {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
Roboto, Helvetica, Arial, sans-serif;
|
Roboto, Helvetica, Arial, sans-serif;
|
||||||
font-family: var(--ui-font);
|
font-family: var(--ui-font);
|
||||||
position: fixed;
|
position: fixed;
|
||||||
z-index: 1000;
|
z-index: var(--zIndex-popup);
|
||||||
|
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { render } from "@testing-library/react";
|
|||||||
import fallbackLangData from "../locales/en.json";
|
import fallbackLangData from "../locales/en.json";
|
||||||
|
|
||||||
import Trans from "./Trans";
|
import Trans from "./Trans";
|
||||||
|
import { TranslationKeys } from "../i18n";
|
||||||
|
|
||||||
describe("Test <Trans/>", () => {
|
describe("Test <Trans/>", () => {
|
||||||
it("should translate the the strings correctly", () => {
|
it("should translate the the strings correctly", () => {
|
||||||
@@ -18,24 +19,27 @@ describe("Test <Trans/>", () => {
|
|||||||
const { getByTestId } = render(
|
const { getByTestId } = render(
|
||||||
<>
|
<>
|
||||||
<div data-testid="test1">
|
<div data-testid="test1">
|
||||||
<Trans i18nKey="transTest.key1" audience="world" />
|
<Trans
|
||||||
|
i18nKey={"transTest.key1" as unknown as TranslationKeys}
|
||||||
|
audience="world"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div data-testid="test2">
|
<div data-testid="test2">
|
||||||
<Trans
|
<Trans
|
||||||
i18nKey="transTest.key2"
|
i18nKey={"transTest.key2" as unknown as TranslationKeys}
|
||||||
link={(el) => <a href="https://example.com">{el}</a>}
|
link={(el) => <a href="https://example.com">{el}</a>}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div data-testid="test3">
|
<div data-testid="test3">
|
||||||
<Trans
|
<Trans
|
||||||
i18nKey="transTest.key3"
|
i18nKey={"transTest.key3" as unknown as TranslationKeys}
|
||||||
link={(el) => <a href="https://example.com">{el}</a>}
|
link={(el) => <a href="https://example.com">{el}</a>}
|
||||||
location="the button"
|
location="the button"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div data-testid="test4">
|
<div data-testid="test4">
|
||||||
<Trans
|
<Trans
|
||||||
i18nKey="transTest.key4"
|
i18nKey={"transTest.key4" as unknown as TranslationKeys}
|
||||||
link={(el) => <a href="https://example.com">{el}</a>}
|
link={(el) => <a href="https://example.com">{el}</a>}
|
||||||
location="the button"
|
location="the button"
|
||||||
bold={(el) => <strong>{el}</strong>}
|
bold={(el) => <strong>{el}</strong>}
|
||||||
@@ -43,7 +47,7 @@ describe("Test <Trans/>", () => {
|
|||||||
</div>
|
</div>
|
||||||
<div data-testid="test5">
|
<div data-testid="test5">
|
||||||
<Trans
|
<Trans
|
||||||
i18nKey="transTest.key5"
|
i18nKey={"transTest.key5" as unknown as TranslationKeys}
|
||||||
connect-link={(el) => <a href="https://example.com">{el}</a>}
|
connect-link={(el) => <a href="https://example.com">{el}</a>}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { useI18n } from "../i18n";
|
import { TranslationKeys, useI18n } from "../i18n";
|
||||||
|
|
||||||
// Used for splitting i18nKey into tokens in Trans component
|
// Used for splitting i18nKey into tokens in Trans component
|
||||||
// Example:
|
// Example:
|
||||||
@@ -153,7 +153,7 @@ const Trans = ({
|
|||||||
children,
|
children,
|
||||||
...props
|
...props
|
||||||
}: {
|
}: {
|
||||||
i18nKey: string;
|
i18nKey: TranslationKeys;
|
||||||
[key: string]: React.ReactNode | ((el: React.ReactNode) => React.ReactNode);
|
[key: string]: React.ReactNode | ((el: React.ReactNode) => React.ReactNode);
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|||||||
@@ -1608,6 +1608,16 @@ export const tablerCheckIcon = createIcon(
|
|||||||
tablerIconProps,
|
tablerIconProps,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const alertTriangleIcon = createIcon(
|
||||||
|
<>
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||||
|
<path d="M10.24 3.957l-8.422 14.06a1.989 1.989 0 0 0 1.7 2.983h16.845a1.989 1.989 0 0 0 1.7 -2.983l-8.423 -14.06a1.989 1.989 0 0 0 -3.4 0z" />
|
||||||
|
<path d="M12 9v4" />
|
||||||
|
<path d="M12 17h.01" />
|
||||||
|
</>,
|
||||||
|
tablerIconProps,
|
||||||
|
);
|
||||||
|
|
||||||
export const eyeDropperIcon = createIcon(
|
export const eyeDropperIcon = createIcon(
|
||||||
<g strokeWidth={1.25}>
|
<g strokeWidth={1.25}>
|
||||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import { getShortcutFromShortcutName } from "../../actions/shortcuts";
|
import { getShortcutFromShortcutName } from "../../actions/shortcuts";
|
||||||
import { useI18n } from "../../i18n";
|
import { useI18n } from "../../i18n";
|
||||||
import { useExcalidrawSetAppState, useExcalidrawActionManager } from "../App";
|
import {
|
||||||
|
useExcalidrawSetAppState,
|
||||||
|
useExcalidrawActionManager,
|
||||||
|
useExcalidrawElements,
|
||||||
|
} from "../App";
|
||||||
import {
|
import {
|
||||||
ExportIcon,
|
ExportIcon,
|
||||||
ExportImageIcon,
|
ExportImageIcon,
|
||||||
@@ -29,19 +33,42 @@ import { useSetAtom } from "jotai";
|
|||||||
import { activeConfirmDialogAtom } from "../ActiveConfirmDialog";
|
import { activeConfirmDialogAtom } from "../ActiveConfirmDialog";
|
||||||
import { jotaiScope } from "../../jotai";
|
import { jotaiScope } from "../../jotai";
|
||||||
import { useUIAppState } from "../../context/ui-appState";
|
import { useUIAppState } from "../../context/ui-appState";
|
||||||
|
import { openConfirmModal } from "../OverwriteConfirm/OverwriteConfirmState";
|
||||||
|
import Trans from "../Trans";
|
||||||
|
|
||||||
export const LoadScene = () => {
|
export const LoadScene = () => {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const actionManager = useExcalidrawActionManager();
|
const actionManager = useExcalidrawActionManager();
|
||||||
|
const elements = useExcalidrawElements();
|
||||||
|
|
||||||
if (!actionManager.isActionEnabled(actionLoadScene)) {
|
if (!actionManager.isActionEnabled(actionLoadScene)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleSelect = async () => {
|
||||||
|
if (
|
||||||
|
!elements.length ||
|
||||||
|
(await openConfirmModal({
|
||||||
|
title: t("overwriteConfirm.modal.loadFromFile.title"),
|
||||||
|
actionLabel: t("overwriteConfirm.modal.loadFromFile.button"),
|
||||||
|
color: "warning",
|
||||||
|
description: (
|
||||||
|
<Trans
|
||||||
|
i18nKey="overwriteConfirm.modal.loadFromFile.description"
|
||||||
|
bold={(text) => <strong>{text}</strong>}
|
||||||
|
br={() => <br />}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
}))
|
||||||
|
) {
|
||||||
|
actionManager.executeAction(actionLoadScene);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
icon={LoadIcon}
|
icon={LoadIcon}
|
||||||
onSelect={() => actionManager.executeAction(actionLoadScene)}
|
onSelect={handleSelect}
|
||||||
data-testid="load-button"
|
data-testid="load-button"
|
||||||
shortcut={getShortcutFromShortcutName("loadScene")}
|
shortcut={getShortcutFromShortcutName("loadScene")}
|
||||||
aria-label={t("buttons.load")}
|
aria-label={t("buttons.load")}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ type TunnelsContextValue = {
|
|||||||
FooterCenterTunnel: Tunnel;
|
FooterCenterTunnel: Tunnel;
|
||||||
DefaultSidebarTriggerTunnel: Tunnel;
|
DefaultSidebarTriggerTunnel: Tunnel;
|
||||||
DefaultSidebarTabTriggersTunnel: Tunnel;
|
DefaultSidebarTabTriggersTunnel: Tunnel;
|
||||||
|
OverwriteConfirmDialogTunnel: Tunnel;
|
||||||
jotaiScope: symbol;
|
jotaiScope: symbol;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -30,6 +31,7 @@ export const useInitializeTunnels = () => {
|
|||||||
FooterCenterTunnel: tunnel(),
|
FooterCenterTunnel: tunnel(),
|
||||||
DefaultSidebarTriggerTunnel: tunnel(),
|
DefaultSidebarTriggerTunnel: tunnel(),
|
||||||
DefaultSidebarTabTriggersTunnel: tunnel(),
|
DefaultSidebarTabTriggersTunnel: tunnel(),
|
||||||
|
OverwriteConfirmDialogTunnel: tunnel(),
|
||||||
jotaiScope: Symbol(),
|
jotaiScope: Symbol(),
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|||||||
@@ -5,6 +5,15 @@
|
|||||||
--zIndex-canvas: 1;
|
--zIndex-canvas: 1;
|
||||||
--zIndex-wysiwyg: 2;
|
--zIndex-wysiwyg: 2;
|
||||||
--zIndex-layerUI: 3;
|
--zIndex-layerUI: 3;
|
||||||
|
|
||||||
|
--zIndex-modal: 1000;
|
||||||
|
--zIndex-popup: 1001;
|
||||||
|
--zIndex-toast: 999999;
|
||||||
|
|
||||||
|
--sab: env(safe-area-inset-bottom);
|
||||||
|
--sal: env(safe-area-inset-left);
|
||||||
|
--sar: env(safe-area-inset-right);
|
||||||
|
--sat: env(safe-area-inset-top);
|
||||||
}
|
}
|
||||||
|
|
||||||
.excalidraw {
|
.excalidraw {
|
||||||
|
|||||||
+45
-4
@@ -27,10 +27,6 @@
|
|||||||
--popup-secondary-bg-color: #{$oc-gray-1};
|
--popup-secondary-bg-color: #{$oc-gray-1};
|
||||||
--popup-text-color: #{$oc-black};
|
--popup-text-color: #{$oc-black};
|
||||||
--popup-text-inverted-color: #{$oc-white};
|
--popup-text-inverted-color: #{$oc-white};
|
||||||
--sab: env(safe-area-inset-bottom);
|
|
||||||
--sal: env(safe-area-inset-left);
|
|
||||||
--sar: env(safe-area-inset-right);
|
|
||||||
--sat: env(safe-area-inset-top);
|
|
||||||
--select-highlight-color: #{$oc-blue-5};
|
--select-highlight-color: #{$oc-blue-5};
|
||||||
--shadow-island: 0px 7px 14px rgba(0, 0, 0, 0.05),
|
--shadow-island: 0px 7px 14px rgba(0, 0, 0, 0.05),
|
||||||
0px 0px 3.12708px rgba(0, 0, 0, 0.0798),
|
0px 0px 3.12708px rgba(0, 0, 0, 0.0798),
|
||||||
@@ -99,9 +95,33 @@
|
|||||||
--color-gray-100: #121212;
|
--color-gray-100: #121212;
|
||||||
|
|
||||||
--color-warning: #fceeca;
|
--color-warning: #fceeca;
|
||||||
|
--color-warning-dark: #f5c354;
|
||||||
|
--color-warning-darker: #f3ab2c;
|
||||||
|
--color-warning-darkest: #ec8b14;
|
||||||
--color-text-warning: var(--text-primary-color);
|
--color-text-warning: var(--text-primary-color);
|
||||||
|
|
||||||
--color-danger: #db6965;
|
--color-danger: #db6965;
|
||||||
|
--color-danger-dark: #db6965;
|
||||||
|
--color-danger-darker: #d65550;
|
||||||
|
--color-danger-darkest: #d1413c;
|
||||||
|
--color-danger-text: black;
|
||||||
|
|
||||||
|
--color-danger-background: #fff0f0;
|
||||||
|
--color-danger-icon-background: #ffdad6;
|
||||||
|
--color-danger-color: #700000;
|
||||||
|
--color-danger-icon-color: #700000;
|
||||||
|
|
||||||
|
--color-warning-background: var(--color-warning);
|
||||||
|
--color-warning-icon-background: var(--color-warning-dark);
|
||||||
|
--color-warning-color: var(--text-primary-color);
|
||||||
|
--color-warning-icon-color: var(--text-primary-color);
|
||||||
|
|
||||||
|
--color-muted: var(--color-gray-30);
|
||||||
|
--color-muted-darker: var(--color-gray-60);
|
||||||
|
--color-muted-darkest: var(--color-gray-100);
|
||||||
|
--color-muted-background: var(--color-gray-80);
|
||||||
|
--color-muted-background-darker: var(--color-gray-100);
|
||||||
|
|
||||||
--color-promo: #e70078;
|
--color-promo: #e70078;
|
||||||
--color-success: #268029;
|
--color-success: #268029;
|
||||||
--color-success-lighter: #cafccc;
|
--color-success-lighter: #cafccc;
|
||||||
@@ -177,6 +197,27 @@
|
|||||||
--color-text-warning: var(--color-gray-80);
|
--color-text-warning: var(--color-gray-80);
|
||||||
|
|
||||||
--color-danger: #ffa8a5;
|
--color-danger: #ffa8a5;
|
||||||
|
--color-danger-dark: #672120;
|
||||||
|
--color-danger-darker: #8f2625;
|
||||||
|
--color-danger-darkest: #ac2b29;
|
||||||
|
--color-danger-text: #fbcbcc;
|
||||||
|
|
||||||
|
--color-danger-background: #fbcbcc;
|
||||||
|
--color-danger-icon-background: #672120;
|
||||||
|
--color-danger-color: #261919;
|
||||||
|
--color-danger-icon-color: #fbcbcc;
|
||||||
|
|
||||||
|
--color-warning-background: var(--color-warning);
|
||||||
|
--color-warning-icon-background: var(--color-warning-dark);
|
||||||
|
--color-warning-color: var(--color-gray-80);
|
||||||
|
--color-warning-icon-color: var(--color-gray-80);
|
||||||
|
|
||||||
|
--color-muted: var(--color-gray-80);
|
||||||
|
--color-muted-darker: var(--color-gray-60);
|
||||||
|
--color-muted-darkest: var(--color-gray-20);
|
||||||
|
--color-muted-background: var(--color-gray-40);
|
||||||
|
--color-muted-background-darker: var(--color-gray-20);
|
||||||
|
|
||||||
--color-promo: #d297ff;
|
--color-promo: #d297ff;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+24
-1
@@ -41,6 +41,7 @@ import {
|
|||||||
measureBaseline,
|
measureBaseline,
|
||||||
} from "../element/textElement";
|
} from "../element/textElement";
|
||||||
import { COLOR_PALETTE } from "../colors";
|
import { COLOR_PALETTE } from "../colors";
|
||||||
|
import { normalizeLink } from "./url";
|
||||||
|
|
||||||
type RestoredAppState = Omit<
|
type RestoredAppState = Omit<
|
||||||
AppState,
|
AppState,
|
||||||
@@ -142,7 +143,7 @@ const restoreElementWithProperties = <
|
|||||||
? element.boundElementIds.map((id) => ({ type: "arrow", id }))
|
? element.boundElementIds.map((id) => ({ type: "arrow", id }))
|
||||||
: element.boundElements ?? [],
|
: element.boundElements ?? [],
|
||||||
updated: element.updated ?? getUpdatedTimestamp(),
|
updated: element.updated ?? getUpdatedTimestamp(),
|
||||||
link: element.link ?? null,
|
link: element.link ? normalizeLink(element.link) : null,
|
||||||
locked: element.locked ?? false,
|
locked: element.locked ?? false,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -370,6 +371,24 @@ const repairBoundElement = (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove an element's frameId if its containing frame is non-existent
|
||||||
|
*
|
||||||
|
* NOTE mutates elements.
|
||||||
|
*/
|
||||||
|
const repairFrameMembership = (
|
||||||
|
element: Mutable<ExcalidrawElement>,
|
||||||
|
elementsMap: Map<string, Mutable<ExcalidrawElement>>,
|
||||||
|
) => {
|
||||||
|
if (element.frameId) {
|
||||||
|
const containingFrame = elementsMap.get(element.frameId);
|
||||||
|
|
||||||
|
if (!containingFrame) {
|
||||||
|
element.frameId = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const restoreElements = (
|
export const restoreElements = (
|
||||||
elements: ImportedDataState["elements"],
|
elements: ImportedDataState["elements"],
|
||||||
/** NOTE doesn't serve for reconciliation */
|
/** NOTE doesn't serve for reconciliation */
|
||||||
@@ -410,6 +429,10 @@ export const restoreElements = (
|
|||||||
// repair binding. Mutates elements.
|
// repair binding. Mutates elements.
|
||||||
const restoredElementsMap = arrayToMap(restoredElements);
|
const restoredElementsMap = arrayToMap(restoredElements);
|
||||||
for (const element of restoredElements) {
|
for (const element of restoredElements) {
|
||||||
|
if (element.frameId) {
|
||||||
|
repairFrameMembership(element, restoredElementsMap);
|
||||||
|
}
|
||||||
|
|
||||||
if (isTextElement(element) && element.containerId) {
|
if (isTextElement(element) && element.containerId) {
|
||||||
repairBoundElement(element, restoredElementsMap);
|
repairBoundElement(element, restoredElementsMap);
|
||||||
} else if (element.boundElements) {
|
} else if (element.boundElements) {
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { normalizeLink } from "./url";
|
||||||
|
|
||||||
|
describe("normalizeLink", () => {
|
||||||
|
// NOTE not an extensive XSS test suite, just to check if we're not
|
||||||
|
// regressing in sanitization
|
||||||
|
it("should sanitize links", () => {
|
||||||
|
expect(
|
||||||
|
// eslint-disable-next-line no-script-url
|
||||||
|
normalizeLink(`javascript://%0aalert(document.domain)`).startsWith(
|
||||||
|
// eslint-disable-next-line no-script-url
|
||||||
|
`javascript:`,
|
||||||
|
),
|
||||||
|
).toBe(false);
|
||||||
|
expect(normalizeLink("ola")).toBe("ola");
|
||||||
|
expect(normalizeLink(" ola")).toBe("ola");
|
||||||
|
|
||||||
|
expect(normalizeLink("https://www.excalidraw.com")).toBe(
|
||||||
|
"https://www.excalidraw.com",
|
||||||
|
);
|
||||||
|
expect(normalizeLink("www.excalidraw.com")).toBe("www.excalidraw.com");
|
||||||
|
expect(normalizeLink("/ola")).toBe("/ola");
|
||||||
|
expect(normalizeLink("http://test")).toBe("http://test");
|
||||||
|
expect(normalizeLink("ftp://test")).toBe("ftp://test");
|
||||||
|
expect(normalizeLink("file://")).toBe("file://");
|
||||||
|
expect(normalizeLink("file://")).toBe("file://");
|
||||||
|
expect(normalizeLink("[test](https://test)")).toBe("[test](https://test)");
|
||||||
|
expect(normalizeLink("[[test]]")).toBe("[[test]]");
|
||||||
|
expect(normalizeLink("<test>")).toBe("<test>");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { sanitizeUrl } from "@braintree/sanitize-url";
|
||||||
|
|
||||||
|
export const normalizeLink = (link: string) => {
|
||||||
|
return sanitizeUrl(link);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isLocalLink = (link: string | null) => {
|
||||||
|
return !!(link?.includes(location.origin) || link?.startsWith("/"));
|
||||||
|
};
|
||||||
@@ -29,6 +29,7 @@ import { getTooltipDiv, updateTooltipPosition } from "../components/Tooltip";
|
|||||||
import { getSelectedElements } from "../scene";
|
import { getSelectedElements } from "../scene";
|
||||||
import { isPointHittingElementBoundingBox } from "./collision";
|
import { isPointHittingElementBoundingBox } from "./collision";
|
||||||
import { getElementAbsoluteCoords } from "./";
|
import { getElementAbsoluteCoords } from "./";
|
||||||
|
import { isLocalLink, normalizeLink } from "../data/url";
|
||||||
|
|
||||||
import "./Hyperlink.scss";
|
import "./Hyperlink.scss";
|
||||||
import { trackEvent } from "../analytics";
|
import { trackEvent } from "../analytics";
|
||||||
@@ -166,7 +167,7 @@ export const Hyperlink = ({
|
|||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<a
|
<a
|
||||||
href={element.link || ""}
|
href={normalizeLink(element.link || "")}
|
||||||
className={clsx("excalidraw-hyperlinkContainer-link", {
|
className={clsx("excalidraw-hyperlinkContainer-link", {
|
||||||
"d-none": isEditing,
|
"d-none": isEditing,
|
||||||
})}
|
})}
|
||||||
@@ -177,7 +178,13 @@ export const Hyperlink = ({
|
|||||||
EVENT.EXCALIDRAW_LINK,
|
EVENT.EXCALIDRAW_LINK,
|
||||||
event.nativeEvent,
|
event.nativeEvent,
|
||||||
);
|
);
|
||||||
onLinkOpen(element, customEvent);
|
onLinkOpen(
|
||||||
|
{
|
||||||
|
...element,
|
||||||
|
link: normalizeLink(element.link),
|
||||||
|
},
|
||||||
|
customEvent,
|
||||||
|
);
|
||||||
if (customEvent.defaultPrevented) {
|
if (customEvent.defaultPrevented) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}
|
}
|
||||||
@@ -231,21 +238,6 @@ const getCoordsForPopover = (
|
|||||||
return { x, y };
|
return { x, y };
|
||||||
};
|
};
|
||||||
|
|
||||||
export const normalizeLink = (link: string) => {
|
|
||||||
link = link.trim();
|
|
||||||
if (link) {
|
|
||||||
// prefix with protocol if not fully-qualified
|
|
||||||
if (!link.includes("://") && !/^[[\\/]/.test(link)) {
|
|
||||||
link = `https://${link}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return link;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const isLocalLink = (link: string | null) => {
|
|
||||||
return !!(link?.includes(location.origin) || link?.startsWith("/"));
|
|
||||||
};
|
|
||||||
|
|
||||||
export const actionLink = register({
|
export const actionLink = register({
|
||||||
name: "hyperlink",
|
name: "hyperlink",
|
||||||
perform: (elements, appState) => {
|
perform: (elements, appState) => {
|
||||||
|
|||||||
@@ -862,7 +862,6 @@ const VALID_CONTAINER_TYPES = new Set([
|
|||||||
"rectangle",
|
"rectangle",
|
||||||
"ellipse",
|
"ellipse",
|
||||||
"diamond",
|
"diamond",
|
||||||
"image",
|
|
||||||
"arrow",
|
"arrow",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@@ -1459,5 +1459,54 @@ describe("textWysiwyg", () => {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("shouldn't bind to container if container has bound text not centered and text tool is used", async () => {
|
||||||
|
expect(h.elements.length).toBe(1);
|
||||||
|
|
||||||
|
Keyboard.keyPress(KEYS.ENTER);
|
||||||
|
|
||||||
|
expect(h.elements.length).toBe(2);
|
||||||
|
|
||||||
|
// Bind first text
|
||||||
|
let text = h.elements[1] as ExcalidrawTextElementWithContainer;
|
||||||
|
expect(text.containerId).toBe(rectangle.id);
|
||||||
|
let editor = getTextEditor();
|
||||||
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
|
updateTextEditor(editor, "Hello!");
|
||||||
|
expect(
|
||||||
|
(h.elements[1] as ExcalidrawTextElementWithContainer).verticalAlign,
|
||||||
|
).toBe(VERTICAL_ALIGN.MIDDLE);
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByTitle("Align bottom"));
|
||||||
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
|
|
||||||
|
editor.blur();
|
||||||
|
|
||||||
|
expect(rectangle.boundElements).toStrictEqual([
|
||||||
|
{ id: text.id, type: "text" },
|
||||||
|
]);
|
||||||
|
expect(
|
||||||
|
(h.elements[1] as ExcalidrawTextElementWithContainer).verticalAlign,
|
||||||
|
).toBe(VERTICAL_ALIGN.BOTTOM);
|
||||||
|
|
||||||
|
// Attempt to Bind 2nd text using text tool
|
||||||
|
UI.clickTool("text");
|
||||||
|
mouse.clickAt(
|
||||||
|
rectangle.x + rectangle.width / 2,
|
||||||
|
rectangle.y + rectangle.height / 2,
|
||||||
|
);
|
||||||
|
editor = getTextEditor();
|
||||||
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
|
updateTextEditor(editor, "Excalidraw");
|
||||||
|
editor.blur();
|
||||||
|
|
||||||
|
expect(h.elements.length).toBe(3);
|
||||||
|
expect(rectangle.boundElements).toStrictEqual([
|
||||||
|
{ id: h.elements[1].id, type: "text" },
|
||||||
|
]);
|
||||||
|
text = h.elements[2] as ExcalidrawTextElementWithContainer;
|
||||||
|
expect(text.containerId).toBe(null);
|
||||||
|
expect(text.text).toBe("Excalidraw");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -30,15 +30,6 @@ describe("Test TypeChecks", () => {
|
|||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
).toBeTruthy();
|
).toBeTruthy();
|
||||||
|
|
||||||
expect(
|
|
||||||
hasBoundTextElement(
|
|
||||||
API.createElement({
|
|
||||||
type: "image",
|
|
||||||
boundElements: [{ type: "text", id: "text-id" }],
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
).toBeTruthy();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return false for text bindable containers without bound text", () => {
|
it("should return false for text bindable containers without bound text", () => {
|
||||||
@@ -62,5 +53,14 @@ describe("Test TypeChecks", () => {
|
|||||||
),
|
),
|
||||||
).toBeFalsy();
|
).toBeFalsy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
expect(
|
||||||
|
hasBoundTextElement(
|
||||||
|
API.createElement({
|
||||||
|
type: "image",
|
||||||
|
boundElements: [{ type: "text", id: "text-id" }],
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
).toBeFalsy();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -126,7 +126,6 @@ export const isTextBindableContainer = (
|
|||||||
(element.type === "rectangle" ||
|
(element.type === "rectangle" ||
|
||||||
element.type === "diamond" ||
|
element.type === "diamond" ||
|
||||||
element.type === "ellipse" ||
|
element.type === "ellipse" ||
|
||||||
element.type === "image" ||
|
|
||||||
isArrowElement(element))
|
isArrowElement(element))
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -162,7 +162,6 @@ export type ExcalidrawTextContainer =
|
|||||||
| ExcalidrawRectangleElement
|
| ExcalidrawRectangleElement
|
||||||
| ExcalidrawDiamondElement
|
| ExcalidrawDiamondElement
|
||||||
| ExcalidrawEllipseElement
|
| ExcalidrawEllipseElement
|
||||||
| ExcalidrawImageElement
|
|
||||||
| ExcalidrawArrowElement;
|
| ExcalidrawArrowElement;
|
||||||
|
|
||||||
export type ExcalidrawTextElementWithContainer = {
|
export type ExcalidrawTextElementWithContainer = {
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import { MIME_TYPES } from "../../constants";
|
|||||||
import { trackEvent } from "../../analytics";
|
import { trackEvent } from "../../analytics";
|
||||||
import { getFrame } from "../../utils";
|
import { getFrame } from "../../utils";
|
||||||
|
|
||||||
const exportToExcalidrawPlus = async (
|
export const exportToExcalidrawPlus = async (
|
||||||
elements: readonly NonDeletedExcalidrawElement[],
|
elements: readonly NonDeletedExcalidrawElement[],
|
||||||
appState: Partial<AppState>,
|
appState: Partial<AppState>,
|
||||||
files: BinaryFiles,
|
files: BinaryFiles,
|
||||||
|
|||||||
@@ -282,11 +282,15 @@ export const loadScene = async (
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type ExportToBackendResult =
|
||||||
|
| { url: null; errorMessage: string }
|
||||||
|
| { url: string; errorMessage: null };
|
||||||
|
|
||||||
export const exportToBackend = async (
|
export const exportToBackend = async (
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly ExcalidrawElement[],
|
||||||
appState: Partial<AppState>,
|
appState: Partial<AppState>,
|
||||||
files: BinaryFiles,
|
files: BinaryFiles,
|
||||||
) => {
|
): Promise<ExportToBackendResult> => {
|
||||||
const encryptionKey = await generateEncryptionKey("string");
|
const encryptionKey = await generateEncryptionKey("string");
|
||||||
|
|
||||||
const payload = await compressData(
|
const payload = await compressData(
|
||||||
@@ -327,14 +331,18 @@ export const exportToBackend = async (
|
|||||||
files: filesToUpload,
|
files: filesToUpload,
|
||||||
});
|
});
|
||||||
|
|
||||||
window.prompt(`🔒${t("alerts.uploadedSecurly")}`, urlString);
|
return { url: urlString, errorMessage: null };
|
||||||
} else if (json.error_class === "RequestTooLargeError") {
|
} else if (json.error_class === "RequestTooLargeError") {
|
||||||
window.alert(t("alerts.couldNotCreateShareableLinkTooBig"));
|
return {
|
||||||
} else {
|
url: null,
|
||||||
window.alert(t("alerts.couldNotCreateShareableLink"));
|
errorMessage: t("alerts.couldNotCreateShareableLinkTooBig"),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return { url: null, errorMessage: t("alerts.couldNotCreateShareableLink") };
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
window.alert(t("alerts.couldNotCreateShareableLink"));
|
|
||||||
|
return { url: null, errorMessage: t("alerts.couldNotCreateShareableLink") };
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,135 @@
|
|||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
debug: typeof Debug;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const lessPrecise = (num: number, precision = 5) =>
|
||||||
|
parseFloat(num.toPrecision(precision));
|
||||||
|
|
||||||
|
const getAvgFrameTime = (times: number[]) =>
|
||||||
|
lessPrecise(times.reduce((a, b) => a + b) / times.length);
|
||||||
|
|
||||||
|
const getFps = (frametime: number) => lessPrecise(1000 / frametime);
|
||||||
|
|
||||||
|
export class Debug {
|
||||||
|
public static DEBUG_LOG_TIMES = true;
|
||||||
|
|
||||||
|
private static TIMES_AGGR: Record<string, { t: number; times: number[] }> =
|
||||||
|
{};
|
||||||
|
private static TIMES_AVG: Record<
|
||||||
|
string,
|
||||||
|
{ t: number; times: number[]; avg: number | null }
|
||||||
|
> = {};
|
||||||
|
private static LAST_DEBUG_LOG_CALL = 0;
|
||||||
|
private static DEBUG_LOG_INTERVAL_ID: null | number = null;
|
||||||
|
|
||||||
|
private static setupInterval = () => {
|
||||||
|
if (Debug.DEBUG_LOG_INTERVAL_ID === null) {
|
||||||
|
console.info("%c(starting perf recording)", "color: lime");
|
||||||
|
Debug.DEBUG_LOG_INTERVAL_ID = window.setInterval(Debug.debugLogger, 1000);
|
||||||
|
}
|
||||||
|
Debug.LAST_DEBUG_LOG_CALL = Date.now();
|
||||||
|
};
|
||||||
|
|
||||||
|
private static debugLogger = () => {
|
||||||
|
if (
|
||||||
|
Date.now() - Debug.LAST_DEBUG_LOG_CALL > 600 &&
|
||||||
|
Debug.DEBUG_LOG_INTERVAL_ID !== null
|
||||||
|
) {
|
||||||
|
window.clearInterval(Debug.DEBUG_LOG_INTERVAL_ID);
|
||||||
|
Debug.DEBUG_LOG_INTERVAL_ID = null;
|
||||||
|
for (const [name, { avg }] of Object.entries(Debug.TIMES_AVG)) {
|
||||||
|
if (avg != null) {
|
||||||
|
console.info(
|
||||||
|
`%c${name} run avg: ${avg}ms (${getFps(avg)} fps)`,
|
||||||
|
"color: blue",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.info("%c(stopping perf recording)", "color: red");
|
||||||
|
Debug.TIMES_AGGR = {};
|
||||||
|
Debug.TIMES_AVG = {};
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (Debug.DEBUG_LOG_TIMES) {
|
||||||
|
for (const [name, { t, times }] of Object.entries(Debug.TIMES_AGGR)) {
|
||||||
|
if (times.length) {
|
||||||
|
console.info(
|
||||||
|
name,
|
||||||
|
lessPrecise(times.reduce((a, b) => a + b)),
|
||||||
|
times.sort((a, b) => a - b).map((x) => lessPrecise(x)),
|
||||||
|
);
|
||||||
|
Debug.TIMES_AGGR[name] = { t, times: [] };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const [name, { t, times, avg }] of Object.entries(Debug.TIMES_AVG)) {
|
||||||
|
if (times.length) {
|
||||||
|
const avgFrameTime = getAvgFrameTime(times);
|
||||||
|
console.info(name, `${avgFrameTime}ms (${getFps(avgFrameTime)} fps)`);
|
||||||
|
Debug.TIMES_AVG[name] = {
|
||||||
|
t,
|
||||||
|
times: [],
|
||||||
|
avg:
|
||||||
|
avg != null ? getAvgFrameTime([avg, avgFrameTime]) : avgFrameTime,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
public static logTime = (time?: number, name = "default") => {
|
||||||
|
Debug.setupInterval();
|
||||||
|
const now = performance.now();
|
||||||
|
const { t, times } = (Debug.TIMES_AGGR[name] = Debug.TIMES_AGGR[name] || {
|
||||||
|
t: 0,
|
||||||
|
times: [],
|
||||||
|
});
|
||||||
|
if (t) {
|
||||||
|
times.push(time != null ? time : now - t);
|
||||||
|
}
|
||||||
|
Debug.TIMES_AGGR[name].t = now;
|
||||||
|
};
|
||||||
|
public static logTimeAverage = (time?: number, name = "default") => {
|
||||||
|
Debug.setupInterval();
|
||||||
|
const now = performance.now();
|
||||||
|
const { t, times } = (Debug.TIMES_AVG[name] = Debug.TIMES_AVG[name] || {
|
||||||
|
t: 0,
|
||||||
|
times: [],
|
||||||
|
});
|
||||||
|
if (t) {
|
||||||
|
times.push(time != null ? time : now - t);
|
||||||
|
}
|
||||||
|
Debug.TIMES_AVG[name].t = now;
|
||||||
|
};
|
||||||
|
|
||||||
|
private static logWrapper =
|
||||||
|
(type: "logTime" | "logTimeAverage") =>
|
||||||
|
<T extends any[], R>(fn: (...args: T) => R, name = "default") => {
|
||||||
|
return (...args: T) => {
|
||||||
|
const t0 = performance.now();
|
||||||
|
const ret = fn(...args);
|
||||||
|
Debug.logTime(performance.now() - t0, name);
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
public static logTimeWrap = Debug.logWrapper("logTime");
|
||||||
|
public static logTimeAverageWrap = Debug.logWrapper("logTimeAverage");
|
||||||
|
|
||||||
|
public static perfWrap = <T extends any[], R>(
|
||||||
|
fn: (...args: T) => R,
|
||||||
|
name = "default",
|
||||||
|
) => {
|
||||||
|
return (...args: T) => {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.time(name);
|
||||||
|
const ret = fn(...args);
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.timeEnd(name);
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
window.debug = Debug;
|
||||||
@@ -69,7 +69,10 @@ import {
|
|||||||
} from "./data/localStorage";
|
} from "./data/localStorage";
|
||||||
import CustomStats from "./CustomStats";
|
import CustomStats from "./CustomStats";
|
||||||
import { restore, restoreAppState, RestoredDataState } from "../data/restore";
|
import { restore, restoreAppState, RestoredDataState } from "../data/restore";
|
||||||
import { ExportToExcalidrawPlus } from "./components/ExportToExcalidrawPlus";
|
import {
|
||||||
|
ExportToExcalidrawPlus,
|
||||||
|
exportToExcalidrawPlus,
|
||||||
|
} from "./components/ExportToExcalidrawPlus";
|
||||||
import { updateStaleImageStatuses } from "./data/FileManager";
|
import { updateStaleImageStatuses } from "./data/FileManager";
|
||||||
import { newElementWith } from "../element/mutateElement";
|
import { newElementWith } from "../element/mutateElement";
|
||||||
import { isInitializedImageElement } from "../element/typeChecks";
|
import { isInitializedImageElement } from "../element/typeChecks";
|
||||||
@@ -88,6 +91,10 @@ import { appJotaiStore } from "./app-jotai";
|
|||||||
|
|
||||||
import "./index.scss";
|
import "./index.scss";
|
||||||
import { ResolutionType } from "../utility-types";
|
import { ResolutionType } from "../utility-types";
|
||||||
|
import { ShareableLinkDialog } from "../components/ShareableLinkDialog";
|
||||||
|
import { openConfirmModal } from "../components/OverwriteConfirm/OverwriteConfirmState";
|
||||||
|
import { OverwriteConfirmDialog } from "../components/OverwriteConfirm/OverwriteConfirm";
|
||||||
|
import Trans from "../components/Trans";
|
||||||
|
|
||||||
polyfill();
|
polyfill();
|
||||||
|
|
||||||
@@ -98,6 +105,19 @@ languageDetector.init({
|
|||||||
languageUtils: {},
|
languageUtils: {},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const shareableLinkConfirmDialog = {
|
||||||
|
title: t("overwriteConfirm.modal.shareableLink.title"),
|
||||||
|
description: (
|
||||||
|
<Trans
|
||||||
|
i18nKey="overwriteConfirm.modal.shareableLink.description"
|
||||||
|
bold={(text) => <strong>{text}</strong>}
|
||||||
|
br={() => <br />}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
actionLabel: t("overwriteConfirm.modal.shareableLink.button"),
|
||||||
|
color: "danger",
|
||||||
|
} as const;
|
||||||
|
|
||||||
const initializeScene = async (opts: {
|
const initializeScene = async (opts: {
|
||||||
collabAPI: CollabAPI | null;
|
collabAPI: CollabAPI | null;
|
||||||
excalidrawAPI: ExcalidrawImperativeAPI;
|
excalidrawAPI: ExcalidrawImperativeAPI;
|
||||||
@@ -129,7 +149,7 @@ const initializeScene = async (opts: {
|
|||||||
// don't prompt for collab scenes because we don't override local storage
|
// don't prompt for collab scenes because we don't override local storage
|
||||||
roomLinkData ||
|
roomLinkData ||
|
||||||
// otherwise, prompt whether user wants to override current scene
|
// otherwise, prompt whether user wants to override current scene
|
||||||
window.confirm(t("alerts.loadSceneOverridePrompt"))
|
(await openConfirmModal(shareableLinkConfirmDialog))
|
||||||
) {
|
) {
|
||||||
if (jsonBackendMatch) {
|
if (jsonBackendMatch) {
|
||||||
scene = await loadScene(
|
scene = await loadScene(
|
||||||
@@ -168,7 +188,7 @@ const initializeScene = async (opts: {
|
|||||||
const data = await loadFromBlob(await request.blob(), null, null);
|
const data = await loadFromBlob(await request.blob(), null, null);
|
||||||
if (
|
if (
|
||||||
!scene.elements.length ||
|
!scene.elements.length ||
|
||||||
window.confirm(t("alerts.loadSceneOverridePrompt"))
|
(await openConfirmModal(shareableLinkConfirmDialog))
|
||||||
) {
|
) {
|
||||||
return { scene: data, isExternalScene };
|
return { scene: data, isExternalScene };
|
||||||
}
|
}
|
||||||
@@ -554,6 +574,10 @@ const ExcalidrawWrapper = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const [latestShareableLink, setLatestShareableLink] = useState<string | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
|
||||||
const onExportToBackend = async (
|
const onExportToBackend = async (
|
||||||
exportedElements: readonly NonDeletedExcalidrawElement[],
|
exportedElements: readonly NonDeletedExcalidrawElement[],
|
||||||
appState: Partial<AppState>,
|
appState: Partial<AppState>,
|
||||||
@@ -565,7 +589,7 @@ const ExcalidrawWrapper = () => {
|
|||||||
}
|
}
|
||||||
if (canvas) {
|
if (canvas) {
|
||||||
try {
|
try {
|
||||||
await exportToBackend(
|
const { url, errorMessage } = await exportToBackend(
|
||||||
exportedElements,
|
exportedElements,
|
||||||
{
|
{
|
||||||
...appState,
|
...appState,
|
||||||
@@ -575,6 +599,14 @@ const ExcalidrawWrapper = () => {
|
|||||||
},
|
},
|
||||||
files,
|
files,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (errorMessage) {
|
||||||
|
setErrorMessage(errorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (url) {
|
||||||
|
setLatestShareableLink(url);
|
||||||
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
if (error.name !== "AbortError") {
|
if (error.name !== "AbortError") {
|
||||||
const { width, height } = canvas;
|
const { width, height } = canvas;
|
||||||
@@ -674,21 +706,47 @@ const ExcalidrawWrapper = () => {
|
|||||||
setCollabDialogShown={setCollabDialogShown}
|
setCollabDialogShown={setCollabDialogShown}
|
||||||
isCollabEnabled={!isCollabDisabled}
|
isCollabEnabled={!isCollabDisabled}
|
||||||
/>
|
/>
|
||||||
|
<OverwriteConfirmDialog>
|
||||||
|
<OverwriteConfirmDialog.Actions.ExportToImage />
|
||||||
|
<OverwriteConfirmDialog.Actions.SaveToDisk />
|
||||||
|
{excalidrawAPI && (
|
||||||
|
<OverwriteConfirmDialog.Action
|
||||||
|
title={t("overwriteConfirm.action.excalidrawPlus.title")}
|
||||||
|
actionLabel={t("overwriteConfirm.action.excalidrawPlus.button")}
|
||||||
|
onClick={() => {
|
||||||
|
exportToExcalidrawPlus(
|
||||||
|
excalidrawAPI.getSceneElements(),
|
||||||
|
excalidrawAPI.getAppState(),
|
||||||
|
excalidrawAPI.getFiles(),
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("overwriteConfirm.action.excalidrawPlus.description")}
|
||||||
|
</OverwriteConfirmDialog.Action>
|
||||||
|
)}
|
||||||
|
</OverwriteConfirmDialog>
|
||||||
<AppFooter />
|
<AppFooter />
|
||||||
{isCollaborating && isOffline && (
|
{isCollaborating && isOffline && (
|
||||||
<div className="collab-offline-warning">
|
<div className="collab-offline-warning">
|
||||||
{t("alerts.collabOfflineWarning")}
|
{t("alerts.collabOfflineWarning")}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{latestShareableLink && (
|
||||||
|
<ShareableLinkDialog
|
||||||
|
link={latestShareableLink}
|
||||||
|
onCloseRequest={() => setLatestShareableLink(null)}
|
||||||
|
setErrorMessage={setErrorMessage}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{excalidrawAPI && !isCollabDisabled && (
|
{excalidrawAPI && !isCollabDisabled && (
|
||||||
<Collab excalidrawAPI={excalidrawAPI} />
|
<Collab excalidrawAPI={excalidrawAPI} />
|
||||||
)}
|
)}
|
||||||
|
{errorMessage && (
|
||||||
|
<ErrorDialog onClose={() => setErrorMessage("")}>
|
||||||
|
{errorMessage}
|
||||||
|
</ErrorDialog>
|
||||||
|
)}
|
||||||
</Excalidraw>
|
</Excalidraw>
|
||||||
{errorMessage && (
|
|
||||||
<ErrorDialog onClose={() => setErrorMessage("")}>
|
|
||||||
{errorMessage}
|
|
||||||
</ErrorDialog>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
+12
-4
@@ -16,7 +16,7 @@ import {
|
|||||||
} from "./element/textElement";
|
} from "./element/textElement";
|
||||||
import { arrayToMap, findIndex } from "./utils";
|
import { arrayToMap, findIndex } from "./utils";
|
||||||
import { mutateElement } from "./element/mutateElement";
|
import { mutateElement } from "./element/mutateElement";
|
||||||
import { AppState } from "./types";
|
import { AppClassProperties, AppState } from "./types";
|
||||||
import { getElementsWithinSelection, getSelectedElements } from "./scene";
|
import { getElementsWithinSelection, getSelectedElements } from "./scene";
|
||||||
import { isFrameElement } from "./element";
|
import { isFrameElement } from "./element";
|
||||||
import { moveOneRight } from "./zindex";
|
import { moveOneRight } from "./zindex";
|
||||||
@@ -304,7 +304,7 @@ export const groupsAreCompletelyOutOfFrame = (
|
|||||||
/**
|
/**
|
||||||
* Returns a map of frameId to frame elements. Includes empty frames.
|
* Returns a map of frameId to frame elements. Includes empty frames.
|
||||||
*/
|
*/
|
||||||
export const groupByFrames = (elements: ExcalidrawElementsIncludingDeleted) => {
|
export const groupByFrames = (elements: readonly ExcalidrawElement[]) => {
|
||||||
const frameElementsMap = new Map<
|
const frameElementsMap = new Map<
|
||||||
ExcalidrawElement["id"],
|
ExcalidrawElement["id"],
|
||||||
ExcalidrawElement[]
|
ExcalidrawElement[]
|
||||||
@@ -571,8 +571,13 @@ export const replaceAllElementsInFrame = (
|
|||||||
export const updateFrameMembershipOfSelectedElements = (
|
export const updateFrameMembershipOfSelectedElements = (
|
||||||
allElements: ExcalidrawElementsIncludingDeleted,
|
allElements: ExcalidrawElementsIncludingDeleted,
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
|
app: AppClassProperties,
|
||||||
) => {
|
) => {
|
||||||
const selectedElements = getSelectedElements(allElements, appState);
|
const selectedElements = app.scene.getSelectedElements({
|
||||||
|
selectedElementIds: appState.selectedElementIds,
|
||||||
|
// supplying elements explicitly in case we're passed non-state elements
|
||||||
|
elements: allElements,
|
||||||
|
});
|
||||||
const elementsToFilter = new Set<ExcalidrawElement>(selectedElements);
|
const elementsToFilter = new Set<ExcalidrawElement>(selectedElements);
|
||||||
|
|
||||||
if (appState.editingGroupId) {
|
if (appState.editingGroupId) {
|
||||||
@@ -591,6 +596,7 @@ export const updateFrameMembershipOfSelectedElements = (
|
|||||||
|
|
||||||
elementsToFilter.forEach((element) => {
|
elementsToFilter.forEach((element) => {
|
||||||
if (
|
if (
|
||||||
|
element.frameId &&
|
||||||
!isFrameElement(element) &&
|
!isFrameElement(element) &&
|
||||||
!isElementInFrame(element, allElements, appState)
|
!isElementInFrame(element, allElements, appState)
|
||||||
) {
|
) {
|
||||||
@@ -598,7 +604,9 @@ export const updateFrameMembershipOfSelectedElements = (
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return removeElementsFromFrame(allElements, [...elementsToRemove], appState);
|
return elementsToRemove.size > 0
|
||||||
|
? removeElementsFromFrame(allElements, [...elementsToRemove], appState)
|
||||||
|
: allElements;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+35
-5
@@ -1,7 +1,13 @@
|
|||||||
import { GroupId, ExcalidrawElement, NonDeleted } from "./element/types";
|
import {
|
||||||
import { AppState } from "./types";
|
GroupId,
|
||||||
|
ExcalidrawElement,
|
||||||
|
NonDeleted,
|
||||||
|
NonDeletedExcalidrawElement,
|
||||||
|
} from "./element/types";
|
||||||
|
import { AppClassProperties, AppState } from "./types";
|
||||||
import { getSelectedElements } from "./scene";
|
import { getSelectedElements } from "./scene";
|
||||||
import { getBoundTextElement } from "./element/textElement";
|
import { getBoundTextElement } from "./element/textElement";
|
||||||
|
import { makeNextSelectedElementIds } from "./scene/selection";
|
||||||
|
|
||||||
export const selectGroup = (
|
export const selectGroup = (
|
||||||
groupId: GroupId,
|
groupId: GroupId,
|
||||||
@@ -66,14 +72,33 @@ export const getSelectedGroupIds = (appState: AppState): GroupId[] =>
|
|||||||
*/
|
*/
|
||||||
export const selectGroupsForSelectedElements = (
|
export const selectGroupsForSelectedElements = (
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
elements: readonly NonDeleted<ExcalidrawElement>[],
|
elements: readonly NonDeletedExcalidrawElement[],
|
||||||
|
prevAppState: AppState,
|
||||||
|
/**
|
||||||
|
* supply null in cases where you don't have access to App instance and
|
||||||
|
* you don't care about optimizing selectElements retrieval
|
||||||
|
*/
|
||||||
|
app: AppClassProperties | null,
|
||||||
): AppState => {
|
): AppState => {
|
||||||
let nextAppState: AppState = { ...appState, selectedGroupIds: {} };
|
let nextAppState: AppState = { ...appState, selectedGroupIds: {} };
|
||||||
|
|
||||||
const selectedElements = getSelectedElements(elements, appState);
|
const selectedElements = app
|
||||||
|
? app.scene.getSelectedElements({
|
||||||
|
selectedElementIds: appState.selectedElementIds,
|
||||||
|
// supplying elements explicitly in case we're passed non-state elements
|
||||||
|
elements,
|
||||||
|
})
|
||||||
|
: getSelectedElements(elements, appState);
|
||||||
|
|
||||||
if (!selectedElements.length) {
|
if (!selectedElements.length) {
|
||||||
return { ...nextAppState, editingGroupId: null };
|
return {
|
||||||
|
...nextAppState,
|
||||||
|
editingGroupId: null,
|
||||||
|
selectedElementIds: makeNextSelectedElementIds(
|
||||||
|
nextAppState.selectedElementIds,
|
||||||
|
prevAppState,
|
||||||
|
),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const selectedElement of selectedElements) {
|
for (const selectedElement of selectedElements) {
|
||||||
@@ -91,6 +116,11 @@ export const selectGroupsForSelectedElements = (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
nextAppState.selectedElementIds = makeNextSelectedElementIds(
|
||||||
|
nextAppState.selectedElementIds,
|
||||||
|
prevAppState,
|
||||||
|
);
|
||||||
|
|
||||||
return nextAppState;
|
return nextAppState;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+4
-1
@@ -3,6 +3,7 @@ import percentages from "./locales/percentages.json";
|
|||||||
import { ENV } from "./constants";
|
import { ENV } from "./constants";
|
||||||
import { jotaiScope, jotaiStore } from "./jotai";
|
import { jotaiScope, jotaiStore } from "./jotai";
|
||||||
import { atom, useAtomValue } from "jotai";
|
import { atom, useAtomValue } from "jotai";
|
||||||
|
import { NestedKeyOf } from "./utility-types";
|
||||||
|
|
||||||
const COMPLETION_THRESHOLD = 85;
|
const COMPLETION_THRESHOLD = 85;
|
||||||
|
|
||||||
@@ -12,6 +13,8 @@ export interface Language {
|
|||||||
rtl?: boolean;
|
rtl?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type TranslationKeys = NestedKeyOf<typeof fallbackLangData>;
|
||||||
|
|
||||||
export const defaultLang = { code: "en", label: "English" };
|
export const defaultLang = { code: "en", label: "English" };
|
||||||
|
|
||||||
export const languages: Language[] = [
|
export const languages: Language[] = [
|
||||||
@@ -123,7 +126,7 @@ const findPartsForData = (data: any, parts: string[]) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const t = (
|
export const t = (
|
||||||
path: string,
|
path: NestedKeyOf<typeof fallbackLangData>,
|
||||||
replacement?: { [key: string]: string | number } | null,
|
replacement?: { [key: string]: string | number } | null,
|
||||||
fallback?: string,
|
fallback?: string,
|
||||||
) => {
|
) => {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ export const CODES = {
|
|||||||
BRACKET_LEFT: "BracketLeft",
|
BRACKET_LEFT: "BracketLeft",
|
||||||
ONE: "Digit1",
|
ONE: "Digit1",
|
||||||
TWO: "Digit2",
|
TWO: "Digit2",
|
||||||
|
THREE: "Digit3",
|
||||||
NINE: "Digit9",
|
NINE: "Digit9",
|
||||||
QUOTE: "Quote",
|
QUOTE: "Quote",
|
||||||
ZERO: "Digit0",
|
ZERO: "Digit0",
|
||||||
|
|||||||
@@ -124,6 +124,8 @@
|
|||||||
},
|
},
|
||||||
"statusPublished": "نُشر",
|
"statusPublished": "نُشر",
|
||||||
"sidebarLock": "إبقاء الشريط الجانبي مفتوح",
|
"sidebarLock": "إبقاء الشريط الجانبي مفتوح",
|
||||||
|
"selectAllElementsInFrame": "",
|
||||||
|
"removeAllElementsFromFrame": "",
|
||||||
"eyeDropper": ""
|
"eyeDropper": ""
|
||||||
},
|
},
|
||||||
"library": {
|
"library": {
|
||||||
@@ -221,7 +223,9 @@
|
|||||||
"penMode": "وضع القلم - امنع اللمس",
|
"penMode": "وضع القلم - امنع اللمس",
|
||||||
"link": "إضافة/تحديث الرابط للشكل المحدد",
|
"link": "إضافة/تحديث الرابط للشكل المحدد",
|
||||||
"eraser": "ممحاة",
|
"eraser": "ممحاة",
|
||||||
"hand": ""
|
"frame": "",
|
||||||
|
"hand": "",
|
||||||
|
"extraTools": ""
|
||||||
},
|
},
|
||||||
"headings": {
|
"headings": {
|
||||||
"canvasActions": "إجراءات اللوحة",
|
"canvasActions": "إجراءات اللوحة",
|
||||||
|
|||||||
@@ -124,6 +124,8 @@
|
|||||||
},
|
},
|
||||||
"statusPublished": "",
|
"statusPublished": "",
|
||||||
"sidebarLock": "",
|
"sidebarLock": "",
|
||||||
|
"selectAllElementsInFrame": "",
|
||||||
|
"removeAllElementsFromFrame": "",
|
||||||
"eyeDropper": ""
|
"eyeDropper": ""
|
||||||
},
|
},
|
||||||
"library": {
|
"library": {
|
||||||
@@ -221,7 +223,9 @@
|
|||||||
"penMode": "",
|
"penMode": "",
|
||||||
"link": "",
|
"link": "",
|
||||||
"eraser": "",
|
"eraser": "",
|
||||||
"hand": ""
|
"frame": "",
|
||||||
|
"hand": "",
|
||||||
|
"extraTools": ""
|
||||||
},
|
},
|
||||||
"headings": {
|
"headings": {
|
||||||
"canvasActions": "",
|
"canvasActions": "",
|
||||||
|
|||||||
@@ -124,6 +124,8 @@
|
|||||||
},
|
},
|
||||||
"statusPublished": "",
|
"statusPublished": "",
|
||||||
"sidebarLock": "",
|
"sidebarLock": "",
|
||||||
|
"selectAllElementsInFrame": "",
|
||||||
|
"removeAllElementsFromFrame": "",
|
||||||
"eyeDropper": ""
|
"eyeDropper": ""
|
||||||
},
|
},
|
||||||
"library": {
|
"library": {
|
||||||
@@ -221,7 +223,9 @@
|
|||||||
"penMode": "",
|
"penMode": "",
|
||||||
"link": "",
|
"link": "",
|
||||||
"eraser": "",
|
"eraser": "",
|
||||||
"hand": ""
|
"frame": "",
|
||||||
|
"hand": "",
|
||||||
|
"extraTools": ""
|
||||||
},
|
},
|
||||||
"headings": {
|
"headings": {
|
||||||
"canvasActions": "Действия по платното",
|
"canvasActions": "Действия по платното",
|
||||||
|
|||||||
@@ -124,6 +124,8 @@
|
|||||||
},
|
},
|
||||||
"statusPublished": "প্রকাশিত",
|
"statusPublished": "প্রকাশিত",
|
||||||
"sidebarLock": "লক",
|
"sidebarLock": "লক",
|
||||||
|
"selectAllElementsInFrame": "",
|
||||||
|
"removeAllElementsFromFrame": "",
|
||||||
"eyeDropper": ""
|
"eyeDropper": ""
|
||||||
},
|
},
|
||||||
"library": {
|
"library": {
|
||||||
@@ -221,7 +223,9 @@
|
|||||||
"penMode": "",
|
"penMode": "",
|
||||||
"link": "একটি নির্বাচিত আকৃতির জন্য লিঙ্ক যোগ বা আপডেট করুন",
|
"link": "একটি নির্বাচিত আকৃতির জন্য লিঙ্ক যোগ বা আপডেট করুন",
|
||||||
"eraser": "ঝাড়ন",
|
"eraser": "ঝাড়ন",
|
||||||
"hand": ""
|
"frame": "",
|
||||||
|
"hand": "",
|
||||||
|
"extraTools": ""
|
||||||
},
|
},
|
||||||
"headings": {
|
"headings": {
|
||||||
"canvasActions": "ক্যানভাস কার্যকলাপ",
|
"canvasActions": "ক্যানভাস কার্যকলাপ",
|
||||||
|
|||||||
@@ -124,6 +124,8 @@
|
|||||||
},
|
},
|
||||||
"statusPublished": "Publicat",
|
"statusPublished": "Publicat",
|
||||||
"sidebarLock": "Manté la barra lateral oberta",
|
"sidebarLock": "Manté la barra lateral oberta",
|
||||||
|
"selectAllElementsInFrame": "",
|
||||||
|
"removeAllElementsFromFrame": "",
|
||||||
"eyeDropper": ""
|
"eyeDropper": ""
|
||||||
},
|
},
|
||||||
"library": {
|
"library": {
|
||||||
@@ -221,7 +223,9 @@
|
|||||||
"penMode": "Mode de llapis - evita tocar",
|
"penMode": "Mode de llapis - evita tocar",
|
||||||
"link": "Afegeix / actualitza l'enllaç per a la forma seleccionada",
|
"link": "Afegeix / actualitza l'enllaç per a la forma seleccionada",
|
||||||
"eraser": "Esborrador",
|
"eraser": "Esborrador",
|
||||||
"hand": "Mà (eina de desplaçament)"
|
"frame": "",
|
||||||
|
"hand": "Mà (eina de desplaçament)",
|
||||||
|
"extraTools": ""
|
||||||
},
|
},
|
||||||
"headings": {
|
"headings": {
|
||||||
"canvasActions": "Accions del llenç",
|
"canvasActions": "Accions del llenç",
|
||||||
|
|||||||
@@ -124,6 +124,8 @@
|
|||||||
},
|
},
|
||||||
"statusPublished": "Zveřejněno",
|
"statusPublished": "Zveřejněno",
|
||||||
"sidebarLock": "Ponechat postranní panel otevřený",
|
"sidebarLock": "Ponechat postranní panel otevřený",
|
||||||
|
"selectAllElementsInFrame": "",
|
||||||
|
"removeAllElementsFromFrame": "",
|
||||||
"eyeDropper": "Vyberte barvu z plátna"
|
"eyeDropper": "Vyberte barvu z plátna"
|
||||||
},
|
},
|
||||||
"library": {
|
"library": {
|
||||||
@@ -221,7 +223,9 @@
|
|||||||
"penMode": "Režim Pera - zabránit dotyku",
|
"penMode": "Režim Pera - zabránit dotyku",
|
||||||
"link": "Přidat/aktualizovat odkaz pro vybraný tvar",
|
"link": "Přidat/aktualizovat odkaz pro vybraný tvar",
|
||||||
"eraser": "Guma",
|
"eraser": "Guma",
|
||||||
"hand": "Ruka (nástroj pro posouvání)"
|
"frame": "",
|
||||||
|
"hand": "Ruka (nástroj pro posouvání)",
|
||||||
|
"extraTools": ""
|
||||||
},
|
},
|
||||||
"headings": {
|
"headings": {
|
||||||
"canvasActions": "Akce plátna",
|
"canvasActions": "Akce plátna",
|
||||||
|
|||||||
@@ -124,6 +124,8 @@
|
|||||||
},
|
},
|
||||||
"statusPublished": "",
|
"statusPublished": "",
|
||||||
"sidebarLock": "",
|
"sidebarLock": "",
|
||||||
|
"selectAllElementsInFrame": "",
|
||||||
|
"removeAllElementsFromFrame": "",
|
||||||
"eyeDropper": ""
|
"eyeDropper": ""
|
||||||
},
|
},
|
||||||
"library": {
|
"library": {
|
||||||
@@ -221,7 +223,9 @@
|
|||||||
"penMode": "",
|
"penMode": "",
|
||||||
"link": "",
|
"link": "",
|
||||||
"eraser": "",
|
"eraser": "",
|
||||||
"hand": ""
|
"frame": "",
|
||||||
|
"hand": "",
|
||||||
|
"extraTools": ""
|
||||||
},
|
},
|
||||||
"headings": {
|
"headings": {
|
||||||
"canvasActions": "",
|
"canvasActions": "",
|
||||||
|
|||||||
@@ -124,6 +124,8 @@
|
|||||||
},
|
},
|
||||||
"statusPublished": "Veröffentlicht",
|
"statusPublished": "Veröffentlicht",
|
||||||
"sidebarLock": "Seitenleiste offen lassen",
|
"sidebarLock": "Seitenleiste offen lassen",
|
||||||
|
"selectAllElementsInFrame": "Alle Elemente im Rahmen auswählen",
|
||||||
|
"removeAllElementsFromFrame": "Alle Elemente aus dem Rahmen entfernen",
|
||||||
"eyeDropper": "Farbe von der Zeichenfläche auswählen"
|
"eyeDropper": "Farbe von der Zeichenfläche auswählen"
|
||||||
},
|
},
|
||||||
"library": {
|
"library": {
|
||||||
@@ -221,7 +223,9 @@
|
|||||||
"penMode": "Stift-Modus - Berührung verhindern",
|
"penMode": "Stift-Modus - Berührung verhindern",
|
||||||
"link": "Link für ausgewählte Form hinzufügen / aktualisieren",
|
"link": "Link für ausgewählte Form hinzufügen / aktualisieren",
|
||||||
"eraser": "Radierer",
|
"eraser": "Radierer",
|
||||||
"hand": "Hand (Schwenkwerkzeug)"
|
"frame": "Rahmenwerkzeug",
|
||||||
|
"hand": "Hand (Schwenkwerkzeug)",
|
||||||
|
"extraTools": "Weitere Werkzeuge"
|
||||||
},
|
},
|
||||||
"headings": {
|
"headings": {
|
||||||
"canvasActions": "Aktionen für Zeichenfläche",
|
"canvasActions": "Aktionen für Zeichenfläche",
|
||||||
|
|||||||
@@ -124,6 +124,8 @@
|
|||||||
},
|
},
|
||||||
"statusPublished": "Δημοσιευμένο",
|
"statusPublished": "Δημοσιευμένο",
|
||||||
"sidebarLock": "Κρατήστε την πλαϊνή μπάρα ανοιχτή",
|
"sidebarLock": "Κρατήστε την πλαϊνή μπάρα ανοιχτή",
|
||||||
|
"selectAllElementsInFrame": "",
|
||||||
|
"removeAllElementsFromFrame": "",
|
||||||
"eyeDropper": ""
|
"eyeDropper": ""
|
||||||
},
|
},
|
||||||
"library": {
|
"library": {
|
||||||
@@ -221,7 +223,9 @@
|
|||||||
"penMode": "Λειτουργία μολυβιού - αποτροπή αφής",
|
"penMode": "Λειτουργία μολυβιού - αποτροπή αφής",
|
||||||
"link": "Προσθήκη/ Ενημέρωση συνδέσμου για ένα επιλεγμένο σχήμα",
|
"link": "Προσθήκη/ Ενημέρωση συνδέσμου για ένα επιλεγμένο σχήμα",
|
||||||
"eraser": "Γόμα",
|
"eraser": "Γόμα",
|
||||||
"hand": ""
|
"frame": "",
|
||||||
|
"hand": "",
|
||||||
|
"extraTools": ""
|
||||||
},
|
},
|
||||||
"headings": {
|
"headings": {
|
||||||
"canvasActions": "Ενέργειες καμβά",
|
"canvasActions": "Ενέργειες καμβά",
|
||||||
|
|||||||
@@ -449,5 +449,36 @@
|
|||||||
"shades": "Shades",
|
"shades": "Shades",
|
||||||
"hexCode": "Hex code",
|
"hexCode": "Hex code",
|
||||||
"noShades": "No shades available for this color"
|
"noShades": "No shades available for this color"
|
||||||
|
},
|
||||||
|
"overwriteConfirm": {
|
||||||
|
"action": {
|
||||||
|
"exportToImage": {
|
||||||
|
"title": "Export as image",
|
||||||
|
"button": "Export as image",
|
||||||
|
"description": "Export the scene data as an image from which you can import later."
|
||||||
|
},
|
||||||
|
"saveToDisk": {
|
||||||
|
"title": "Save to disk",
|
||||||
|
"button": "Save to disk",
|
||||||
|
"description": "Export the scene data to a file from which you can import later."
|
||||||
|
},
|
||||||
|
"excalidrawPlus": {
|
||||||
|
"title": "Excalidraw+",
|
||||||
|
"button": "Export to Excalidraw+",
|
||||||
|
"description": "Save the scene to your Excalidraw+ workspace."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"modal": {
|
||||||
|
"loadFromFile": {
|
||||||
|
"title": "Load from file",
|
||||||
|
"button": "Load from file",
|
||||||
|
"description": "Loading from a file will <bold>replace your existing content</bold>.<br></br>You can back up your drawing first using one of the options below."
|
||||||
|
},
|
||||||
|
"shareableLink": {
|
||||||
|
"title": "Load from link",
|
||||||
|
"button": "Replace my content",
|
||||||
|
"description": "Loading external drawing will <bold>replace your existing content</bold>.<br></br>You can back up your drawing first by using one of the options below."
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -124,6 +124,8 @@
|
|||||||
},
|
},
|
||||||
"statusPublished": "Publicado",
|
"statusPublished": "Publicado",
|
||||||
"sidebarLock": "Mantener barra lateral abierta",
|
"sidebarLock": "Mantener barra lateral abierta",
|
||||||
|
"selectAllElementsInFrame": "",
|
||||||
|
"removeAllElementsFromFrame": "",
|
||||||
"eyeDropper": ""
|
"eyeDropper": ""
|
||||||
},
|
},
|
||||||
"library": {
|
"library": {
|
||||||
@@ -221,7 +223,9 @@
|
|||||||
"penMode": "Modo Lápiz - previene toque",
|
"penMode": "Modo Lápiz - previene toque",
|
||||||
"link": "Añadir/Actualizar enlace para una forma seleccionada",
|
"link": "Añadir/Actualizar enlace para una forma seleccionada",
|
||||||
"eraser": "Borrar",
|
"eraser": "Borrar",
|
||||||
"hand": "Mano (herramienta de panoramización)"
|
"frame": "",
|
||||||
|
"hand": "Mano (herramienta de panoramización)",
|
||||||
|
"extraTools": ""
|
||||||
},
|
},
|
||||||
"headings": {
|
"headings": {
|
||||||
"canvasActions": "Acciones del lienzo",
|
"canvasActions": "Acciones del lienzo",
|
||||||
|
|||||||
+39
-35
@@ -124,7 +124,9 @@
|
|||||||
},
|
},
|
||||||
"statusPublished": "Argitaratua",
|
"statusPublished": "Argitaratua",
|
||||||
"sidebarLock": "Mantendu alboko barra irekita",
|
"sidebarLock": "Mantendu alboko barra irekita",
|
||||||
"eyeDropper": ""
|
"selectAllElementsInFrame": "Hautatu markoko elementu guztiak",
|
||||||
|
"removeAllElementsFromFrame": "Kendu markoko elementu guztiak",
|
||||||
|
"eyeDropper": "Aukeratu kolorea oihaletik"
|
||||||
},
|
},
|
||||||
"library": {
|
"library": {
|
||||||
"noItems": "Oraindik ez da elementurik gehitu...",
|
"noItems": "Oraindik ez da elementurik gehitu...",
|
||||||
@@ -221,7 +223,9 @@
|
|||||||
"penMode": "Luma modua - ukipena saihestu",
|
"penMode": "Luma modua - ukipena saihestu",
|
||||||
"link": "Gehitu / Eguneratu esteka hautatutako forma baterako",
|
"link": "Gehitu / Eguneratu esteka hautatutako forma baterako",
|
||||||
"eraser": "Borragoma",
|
"eraser": "Borragoma",
|
||||||
"hand": "Eskua (panoratze tresna)"
|
"frame": "Marko tresna",
|
||||||
|
"hand": "Eskua (panoratze tresna)",
|
||||||
|
"extraTools": "Tresna gehiago"
|
||||||
},
|
},
|
||||||
"headings": {
|
"headings": {
|
||||||
"canvasActions": "Canvas ekintzak",
|
"canvasActions": "Canvas ekintzak",
|
||||||
@@ -356,27 +360,27 @@
|
|||||||
"removeItemsFromLib": "Kendu hautatutako elementuak liburutegitik"
|
"removeItemsFromLib": "Kendu hautatutako elementuak liburutegitik"
|
||||||
},
|
},
|
||||||
"imageExportDialog": {
|
"imageExportDialog": {
|
||||||
"header": "",
|
"header": "Esportatu irudia",
|
||||||
"label": {
|
"label": {
|
||||||
"withBackground": "",
|
"withBackground": "Atzeko planoa",
|
||||||
"onlySelected": "",
|
"onlySelected": "Hautapena soilik",
|
||||||
"darkMode": "",
|
"darkMode": "Modu iluna",
|
||||||
"embedScene": "",
|
"embedScene": "Txertatu eszena",
|
||||||
"scale": "",
|
"scale": "Eskala",
|
||||||
"padding": ""
|
"padding": "Betegarria"
|
||||||
},
|
},
|
||||||
"tooltip": {
|
"tooltip": {
|
||||||
"embedScene": ""
|
"embedScene": "Eszenaren datuak esportatutako PNG/SVG fitxategian gordeko dira, eszena bertatik berrezartzeko.\nEsportatutako fitxategien tamaina handituko da."
|
||||||
},
|
},
|
||||||
"title": {
|
"title": {
|
||||||
"exportToPng": "",
|
"exportToPng": "Esportatu PNG gisa",
|
||||||
"exportToSvg": "",
|
"exportToSvg": "Esportatu SVG gisa",
|
||||||
"copyPngToClipboard": ""
|
"copyPngToClipboard": "Kopiatu PNG arbelera"
|
||||||
},
|
},
|
||||||
"button": {
|
"button": {
|
||||||
"exportToPng": "",
|
"exportToPng": "PNG",
|
||||||
"exportToSvg": "",
|
"exportToSvg": "SVG",
|
||||||
"copyPngToClipboard": ""
|
"copyPngToClipboard": "Kopiatu arbelean"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"encrypted": {
|
"encrypted": {
|
||||||
@@ -411,20 +415,20 @@
|
|||||||
},
|
},
|
||||||
"colors": {
|
"colors": {
|
||||||
"transparent": "Gardena",
|
"transparent": "Gardena",
|
||||||
"black": "",
|
"black": "Beltza",
|
||||||
"white": "",
|
"white": "Zuria",
|
||||||
"red": "",
|
"red": "Gorria",
|
||||||
"pink": "",
|
"pink": "Arrosa",
|
||||||
"grape": "",
|
"grape": "Mahats kolorea",
|
||||||
"violet": "",
|
"violet": "Bioleta",
|
||||||
"gray": "",
|
"gray": "Grisa",
|
||||||
"blue": "",
|
"blue": "Urdina",
|
||||||
"cyan": "",
|
"cyan": "Ziana",
|
||||||
"teal": "",
|
"teal": "Berde urdinxka",
|
||||||
"green": "",
|
"green": "Berdea",
|
||||||
"yellow": "",
|
"yellow": "Horia",
|
||||||
"orange": "",
|
"orange": "Laranja",
|
||||||
"bronze": ""
|
"bronze": "Brontzea"
|
||||||
},
|
},
|
||||||
"welcomeScreen": {
|
"welcomeScreen": {
|
||||||
"app": {
|
"app": {
|
||||||
@@ -440,10 +444,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"colorPicker": {
|
"colorPicker": {
|
||||||
"mostUsedCustomColors": "",
|
"mostUsedCustomColors": "Gehien erabilitako kolore pertsonalizatuak",
|
||||||
"colors": "",
|
"colors": "Koloreak",
|
||||||
"shades": "",
|
"shades": "Ñabardurak",
|
||||||
"hexCode": "",
|
"hexCode": "Hez kodea",
|
||||||
"noShades": ""
|
"noShades": "Kolore honetarako ez dago ñabardurarik eskuragarri"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -124,6 +124,8 @@
|
|||||||
},
|
},
|
||||||
"statusPublished": "منتشر شده",
|
"statusPublished": "منتشر شده",
|
||||||
"sidebarLock": "باز نگه داشتن سایدبار",
|
"sidebarLock": "باز نگه داشتن سایدبار",
|
||||||
|
"selectAllElementsInFrame": "",
|
||||||
|
"removeAllElementsFromFrame": "",
|
||||||
"eyeDropper": ""
|
"eyeDropper": ""
|
||||||
},
|
},
|
||||||
"library": {
|
"library": {
|
||||||
@@ -221,7 +223,9 @@
|
|||||||
"penMode": "حالت قلم - جلوگیری از تماس",
|
"penMode": "حالت قلم - جلوگیری از تماس",
|
||||||
"link": "افزودن/بهروزرسانی پیوند برای شکل انتخابی",
|
"link": "افزودن/بهروزرسانی پیوند برای شکل انتخابی",
|
||||||
"eraser": "پاک کن",
|
"eraser": "پاک کن",
|
||||||
"hand": "دست (ابزار پانینگ)"
|
"frame": "",
|
||||||
|
"hand": "دست (ابزار پانینگ)",
|
||||||
|
"extraTools": ""
|
||||||
},
|
},
|
||||||
"headings": {
|
"headings": {
|
||||||
"canvasActions": "عملیات روی بوم",
|
"canvasActions": "عملیات روی بوم",
|
||||||
|
|||||||
@@ -124,6 +124,8 @@
|
|||||||
},
|
},
|
||||||
"statusPublished": "Julkaistu",
|
"statusPublished": "Julkaistu",
|
||||||
"sidebarLock": "Pidä sivupalkki avoinna",
|
"sidebarLock": "Pidä sivupalkki avoinna",
|
||||||
|
"selectAllElementsInFrame": "",
|
||||||
|
"removeAllElementsFromFrame": "",
|
||||||
"eyeDropper": ""
|
"eyeDropper": ""
|
||||||
},
|
},
|
||||||
"library": {
|
"library": {
|
||||||
@@ -221,7 +223,9 @@
|
|||||||
"penMode": "Kynätila - estä kosketus",
|
"penMode": "Kynätila - estä kosketus",
|
||||||
"link": "Lisää/päivitä linkki valitulle muodolle",
|
"link": "Lisää/päivitä linkki valitulle muodolle",
|
||||||
"eraser": "Poistotyökalu",
|
"eraser": "Poistotyökalu",
|
||||||
"hand": "Käsi (panning-työkalu)"
|
"frame": "",
|
||||||
|
"hand": "Käsi (panning-työkalu)",
|
||||||
|
"extraTools": ""
|
||||||
},
|
},
|
||||||
"headings": {
|
"headings": {
|
||||||
"canvasActions": "Piirtoalueen toiminnot",
|
"canvasActions": "Piirtoalueen toiminnot",
|
||||||
|
|||||||
@@ -124,6 +124,8 @@
|
|||||||
},
|
},
|
||||||
"statusPublished": "Publié",
|
"statusPublished": "Publié",
|
||||||
"sidebarLock": "Maintenir la barre latérale ouverte",
|
"sidebarLock": "Maintenir la barre latérale ouverte",
|
||||||
|
"selectAllElementsInFrame": "",
|
||||||
|
"removeAllElementsFromFrame": "",
|
||||||
"eyeDropper": ""
|
"eyeDropper": ""
|
||||||
},
|
},
|
||||||
"library": {
|
"library": {
|
||||||
@@ -221,7 +223,9 @@
|
|||||||
"penMode": "Mode stylo - évite le toucher",
|
"penMode": "Mode stylo - évite le toucher",
|
||||||
"link": "Ajouter/mettre à jour le lien pour une forme sélectionnée",
|
"link": "Ajouter/mettre à jour le lien pour une forme sélectionnée",
|
||||||
"eraser": "Gomme",
|
"eraser": "Gomme",
|
||||||
"hand": "Mains (outil de déplacement de la vue)"
|
"frame": "",
|
||||||
|
"hand": "Mains (outil de déplacement de la vue)",
|
||||||
|
"extraTools": ""
|
||||||
},
|
},
|
||||||
"headings": {
|
"headings": {
|
||||||
"canvasActions": "Actions du canevas",
|
"canvasActions": "Actions du canevas",
|
||||||
|
|||||||
@@ -124,6 +124,8 @@
|
|||||||
},
|
},
|
||||||
"statusPublished": "Publicado",
|
"statusPublished": "Publicado",
|
||||||
"sidebarLock": "Manter a barra lateral aberta",
|
"sidebarLock": "Manter a barra lateral aberta",
|
||||||
|
"selectAllElementsInFrame": "",
|
||||||
|
"removeAllElementsFromFrame": "",
|
||||||
"eyeDropper": ""
|
"eyeDropper": ""
|
||||||
},
|
},
|
||||||
"library": {
|
"library": {
|
||||||
@@ -221,7 +223,9 @@
|
|||||||
"penMode": "Modo lapis - evitar o contacto",
|
"penMode": "Modo lapis - evitar o contacto",
|
||||||
"link": "Engadir/ Actualizar ligazón para a forma seleccionada",
|
"link": "Engadir/ Actualizar ligazón para a forma seleccionada",
|
||||||
"eraser": "Goma de borrar",
|
"eraser": "Goma de borrar",
|
||||||
"hand": "Man (ferramenta de desprazamento)"
|
"frame": "",
|
||||||
|
"hand": "Man (ferramenta de desprazamento)",
|
||||||
|
"extraTools": ""
|
||||||
},
|
},
|
||||||
"headings": {
|
"headings": {
|
||||||
"canvasActions": "Accións do lenzo",
|
"canvasActions": "Accións do lenzo",
|
||||||
|
|||||||
@@ -124,6 +124,8 @@
|
|||||||
},
|
},
|
||||||
"statusPublished": "פורסם",
|
"statusPublished": "פורסם",
|
||||||
"sidebarLock": "שמור את סרגל הצד פתוח",
|
"sidebarLock": "שמור את סרגל הצד פתוח",
|
||||||
|
"selectAllElementsInFrame": "",
|
||||||
|
"removeAllElementsFromFrame": "",
|
||||||
"eyeDropper": ""
|
"eyeDropper": ""
|
||||||
},
|
},
|
||||||
"library": {
|
"library": {
|
||||||
@@ -221,7 +223,9 @@
|
|||||||
"penMode": "מצב עט - מנע נגיעה",
|
"penMode": "מצב עט - מנע נגיעה",
|
||||||
"link": "הוספה/עדכון קישור של הצורה שנבחרה",
|
"link": "הוספה/עדכון קישור של הצורה שנבחרה",
|
||||||
"eraser": "מחק",
|
"eraser": "מחק",
|
||||||
"hand": "יד (כלי הזזה)"
|
"frame": "",
|
||||||
|
"hand": "יד (כלי הזזה)",
|
||||||
|
"extraTools": ""
|
||||||
},
|
},
|
||||||
"headings": {
|
"headings": {
|
||||||
"canvasActions": "פעולות קנבאס",
|
"canvasActions": "פעולות קנבאס",
|
||||||
|
|||||||
@@ -124,6 +124,8 @@
|
|||||||
},
|
},
|
||||||
"statusPublished": "प्रकाशित",
|
"statusPublished": "प्रकाशित",
|
||||||
"sidebarLock": "साइडबार खुला रखे.",
|
"sidebarLock": "साइडबार खुला रखे.",
|
||||||
|
"selectAllElementsInFrame": "",
|
||||||
|
"removeAllElementsFromFrame": "",
|
||||||
"eyeDropper": "चित्रफलक से रंग चुने"
|
"eyeDropper": "चित्रफलक से रंग चुने"
|
||||||
},
|
},
|
||||||
"library": {
|
"library": {
|
||||||
@@ -221,7 +223,9 @@
|
|||||||
"penMode": "पेन का मोड - स्पर्श टाले",
|
"penMode": "पेन का मोड - स्पर्श टाले",
|
||||||
"link": "",
|
"link": "",
|
||||||
"eraser": "रबड़",
|
"eraser": "रबड़",
|
||||||
"hand": "हाथ ( खिसकाने का औज़ार)"
|
"frame": "",
|
||||||
|
"hand": "हाथ ( खिसकाने का औज़ार)",
|
||||||
|
"extraTools": ""
|
||||||
},
|
},
|
||||||
"headings": {
|
"headings": {
|
||||||
"canvasActions": "कैनवास क्रिया",
|
"canvasActions": "कैनवास क्रिया",
|
||||||
|
|||||||
@@ -124,6 +124,8 @@
|
|||||||
},
|
},
|
||||||
"statusPublished": "",
|
"statusPublished": "",
|
||||||
"sidebarLock": "",
|
"sidebarLock": "",
|
||||||
|
"selectAllElementsInFrame": "",
|
||||||
|
"removeAllElementsFromFrame": "",
|
||||||
"eyeDropper": ""
|
"eyeDropper": ""
|
||||||
},
|
},
|
||||||
"library": {
|
"library": {
|
||||||
@@ -221,7 +223,9 @@
|
|||||||
"penMode": "",
|
"penMode": "",
|
||||||
"link": "Hivatkozás hozzáadása/frissítése a kiválasztott alakzathoz",
|
"link": "Hivatkozás hozzáadása/frissítése a kiválasztott alakzathoz",
|
||||||
"eraser": "",
|
"eraser": "",
|
||||||
"hand": ""
|
"frame": "",
|
||||||
|
"hand": "",
|
||||||
|
"extraTools": ""
|
||||||
},
|
},
|
||||||
"headings": {
|
"headings": {
|
||||||
"canvasActions": "Vászon műveletek",
|
"canvasActions": "Vászon műveletek",
|
||||||
|
|||||||
@@ -124,6 +124,8 @@
|
|||||||
},
|
},
|
||||||
"statusPublished": "Telah terbit",
|
"statusPublished": "Telah terbit",
|
||||||
"sidebarLock": "Biarkan sidebar tetap terbuka",
|
"sidebarLock": "Biarkan sidebar tetap terbuka",
|
||||||
|
"selectAllElementsInFrame": "",
|
||||||
|
"removeAllElementsFromFrame": "",
|
||||||
"eyeDropper": ""
|
"eyeDropper": ""
|
||||||
},
|
},
|
||||||
"library": {
|
"library": {
|
||||||
@@ -221,7 +223,9 @@
|
|||||||
"penMode": "Mode pena - mencegah sentuhan",
|
"penMode": "Mode pena - mencegah sentuhan",
|
||||||
"link": "Tambah/Perbarui tautan untuk bentuk yang dipilih",
|
"link": "Tambah/Perbarui tautan untuk bentuk yang dipilih",
|
||||||
"eraser": "Penghapus",
|
"eraser": "Penghapus",
|
||||||
"hand": "Tangan (alat panning)"
|
"frame": "",
|
||||||
|
"hand": "Tangan (alat panning)",
|
||||||
|
"extraTools": ""
|
||||||
},
|
},
|
||||||
"headings": {
|
"headings": {
|
||||||
"canvasActions": "Opsi Kanvas",
|
"canvasActions": "Opsi Kanvas",
|
||||||
|
|||||||
@@ -124,6 +124,8 @@
|
|||||||
},
|
},
|
||||||
"statusPublished": "Pubblicato",
|
"statusPublished": "Pubblicato",
|
||||||
"sidebarLock": "Mantieni aperta la barra laterale",
|
"sidebarLock": "Mantieni aperta la barra laterale",
|
||||||
|
"selectAllElementsInFrame": "",
|
||||||
|
"removeAllElementsFromFrame": "",
|
||||||
"eyeDropper": ""
|
"eyeDropper": ""
|
||||||
},
|
},
|
||||||
"library": {
|
"library": {
|
||||||
@@ -221,7 +223,9 @@
|
|||||||
"penMode": "Modalità penna - previene il tocco",
|
"penMode": "Modalità penna - previene il tocco",
|
||||||
"link": "Aggiungi/ aggiorna il link per una forma selezionata",
|
"link": "Aggiungi/ aggiorna il link per una forma selezionata",
|
||||||
"eraser": "Gomma",
|
"eraser": "Gomma",
|
||||||
"hand": "Mano (strumento di panoramica)"
|
"frame": "",
|
||||||
|
"hand": "Mano (strumento di panoramica)",
|
||||||
|
"extraTools": "Altri strumenti"
|
||||||
},
|
},
|
||||||
"headings": {
|
"headings": {
|
||||||
"canvasActions": "Azioni sulla Tela",
|
"canvasActions": "Azioni sulla Tela",
|
||||||
@@ -361,12 +365,12 @@
|
|||||||
"withBackground": "Sfondo",
|
"withBackground": "Sfondo",
|
||||||
"onlySelected": "Solo selezionato",
|
"onlySelected": "Solo selezionato",
|
||||||
"darkMode": "Tema scuro",
|
"darkMode": "Tema scuro",
|
||||||
"embedScene": "",
|
"embedScene": "Includi scena",
|
||||||
"scale": "Scala",
|
"scale": "Scala",
|
||||||
"padding": ""
|
"padding": ""
|
||||||
},
|
},
|
||||||
"tooltip": {
|
"tooltip": {
|
||||||
"embedScene": ""
|
"embedScene": "I dati della scena saranno salvati nel file PNG/SVG esportato in modo che la scena possa essere ripristinata da esso.\nQuesto aumenterà la dimensione del file esportato."
|
||||||
},
|
},
|
||||||
"title": {
|
"title": {
|
||||||
"exportToPng": "Esporta come PNG",
|
"exportToPng": "Esporta come PNG",
|
||||||
|
|||||||
@@ -124,6 +124,8 @@
|
|||||||
},
|
},
|
||||||
"statusPublished": "公開済み",
|
"statusPublished": "公開済み",
|
||||||
"sidebarLock": "サイドバーを開いたままにする",
|
"sidebarLock": "サイドバーを開いたままにする",
|
||||||
|
"selectAllElementsInFrame": "",
|
||||||
|
"removeAllElementsFromFrame": "",
|
||||||
"eyeDropper": ""
|
"eyeDropper": ""
|
||||||
},
|
},
|
||||||
"library": {
|
"library": {
|
||||||
@@ -221,7 +223,9 @@
|
|||||||
"penMode": "ペンモード - タッチ防止",
|
"penMode": "ペンモード - タッチ防止",
|
||||||
"link": "選択した図形のリンクを追加/更新",
|
"link": "選択した図形のリンクを追加/更新",
|
||||||
"eraser": "消しゴム",
|
"eraser": "消しゴム",
|
||||||
"hand": "手 (パンニングツール)"
|
"frame": "",
|
||||||
|
"hand": "手 (パンニングツール)",
|
||||||
|
"extraTools": ""
|
||||||
},
|
},
|
||||||
"headings": {
|
"headings": {
|
||||||
"canvasActions": "キャンバス操作",
|
"canvasActions": "キャンバス操作",
|
||||||
|
|||||||
@@ -124,6 +124,8 @@
|
|||||||
},
|
},
|
||||||
"statusPublished": "",
|
"statusPublished": "",
|
||||||
"sidebarLock": "",
|
"sidebarLock": "",
|
||||||
|
"selectAllElementsInFrame": "",
|
||||||
|
"removeAllElementsFromFrame": "",
|
||||||
"eyeDropper": ""
|
"eyeDropper": ""
|
||||||
},
|
},
|
||||||
"library": {
|
"library": {
|
||||||
@@ -221,7 +223,9 @@
|
|||||||
"penMode": "",
|
"penMode": "",
|
||||||
"link": "",
|
"link": "",
|
||||||
"eraser": "Óshirgish",
|
"eraser": "Óshirgish",
|
||||||
"hand": ""
|
"frame": "",
|
||||||
|
"hand": "",
|
||||||
|
"extraTools": ""
|
||||||
},
|
},
|
||||||
"headings": {
|
"headings": {
|
||||||
"canvasActions": "",
|
"canvasActions": "",
|
||||||
|
|||||||
@@ -124,6 +124,8 @@
|
|||||||
},
|
},
|
||||||
"statusPublished": "Yeffeɣ-d",
|
"statusPublished": "Yeffeɣ-d",
|
||||||
"sidebarLock": "Eǧǧ afeggag n yidis yeldi",
|
"sidebarLock": "Eǧǧ afeggag n yidis yeldi",
|
||||||
|
"selectAllElementsInFrame": "",
|
||||||
|
"removeAllElementsFromFrame": "",
|
||||||
"eyeDropper": ""
|
"eyeDropper": ""
|
||||||
},
|
},
|
||||||
"library": {
|
"library": {
|
||||||
@@ -221,7 +223,9 @@
|
|||||||
"penMode": "Askar n yimru - gdel tanalit",
|
"penMode": "Askar n yimru - gdel tanalit",
|
||||||
"link": "Rnu/leqqem aseɣwen i talɣa yettwafernen",
|
"link": "Rnu/leqqem aseɣwen i talɣa yettwafernen",
|
||||||
"eraser": "Sfeḍ",
|
"eraser": "Sfeḍ",
|
||||||
"hand": "Afus (afecku n usmutti n tmuɣli)"
|
"frame": "",
|
||||||
|
"hand": "Afus (afecku n usmutti n tmuɣli)",
|
||||||
|
"extraTools": ""
|
||||||
},
|
},
|
||||||
"headings": {
|
"headings": {
|
||||||
"canvasActions": "Tigawin n teɣzut n usuneɣ",
|
"canvasActions": "Tigawin n teɣzut n usuneɣ",
|
||||||
|
|||||||
@@ -124,6 +124,8 @@
|
|||||||
},
|
},
|
||||||
"statusPublished": "",
|
"statusPublished": "",
|
||||||
"sidebarLock": "",
|
"sidebarLock": "",
|
||||||
|
"selectAllElementsInFrame": "",
|
||||||
|
"removeAllElementsFromFrame": "",
|
||||||
"eyeDropper": ""
|
"eyeDropper": ""
|
||||||
},
|
},
|
||||||
"library": {
|
"library": {
|
||||||
@@ -221,7 +223,9 @@
|
|||||||
"penMode": "",
|
"penMode": "",
|
||||||
"link": "",
|
"link": "",
|
||||||
"eraser": "",
|
"eraser": "",
|
||||||
"hand": ""
|
"frame": "",
|
||||||
|
"hand": "",
|
||||||
|
"extraTools": ""
|
||||||
},
|
},
|
||||||
"headings": {
|
"headings": {
|
||||||
"canvasActions": "",
|
"canvasActions": "",
|
||||||
|
|||||||
@@ -124,6 +124,8 @@
|
|||||||
},
|
},
|
||||||
"statusPublished": "ត្រូវបានបោះពុម្ពផ្សាយ",
|
"statusPublished": "ត្រូវបានបោះពុម្ពផ្សាយ",
|
||||||
"sidebarLock": "ទុករបារចំហៀងបើក",
|
"sidebarLock": "ទុករបារចំហៀងបើក",
|
||||||
|
"selectAllElementsInFrame": "",
|
||||||
|
"removeAllElementsFromFrame": "",
|
||||||
"eyeDropper": ""
|
"eyeDropper": ""
|
||||||
},
|
},
|
||||||
"library": {
|
"library": {
|
||||||
@@ -221,7 +223,9 @@
|
|||||||
"penMode": "របៀបប៊ិច - ជៀសវាងការប៉ះ",
|
"penMode": "របៀបប៊ិច - ជៀសវាងការប៉ះ",
|
||||||
"link": "បន្ថែម/ធ្វើបច្ចុប្បន្នភាពតំណភ្ជាប់សម្រាប់រូបរាងដែលបានជ្រើសរើស",
|
"link": "បន្ថែម/ធ្វើបច្ចុប្បន្នភាពតំណភ្ជាប់សម្រាប់រូបរាងដែលបានជ្រើសរើស",
|
||||||
"eraser": "ជ័រលុប",
|
"eraser": "ជ័រលុប",
|
||||||
"hand": "ដៃ (panning tool)"
|
"frame": "",
|
||||||
|
"hand": "ដៃ (panning tool)",
|
||||||
|
"extraTools": ""
|
||||||
},
|
},
|
||||||
"headings": {
|
"headings": {
|
||||||
"canvasActions": "សកម្មភាពបាវ",
|
"canvasActions": "សកម្មភាពបាវ",
|
||||||
|
|||||||
@@ -124,6 +124,8 @@
|
|||||||
},
|
},
|
||||||
"statusPublished": "게시됨",
|
"statusPublished": "게시됨",
|
||||||
"sidebarLock": "사이드바 유지",
|
"sidebarLock": "사이드바 유지",
|
||||||
|
"selectAllElementsInFrame": "프레임의 모든 요소 선택",
|
||||||
|
"removeAllElementsFromFrame": "프레임의 모든 요소 삭제",
|
||||||
"eyeDropper": "캔버스에서 색상 고르기"
|
"eyeDropper": "캔버스에서 색상 고르기"
|
||||||
},
|
},
|
||||||
"library": {
|
"library": {
|
||||||
@@ -221,7 +223,9 @@
|
|||||||
"penMode": "펜 모드 - 터치 방지",
|
"penMode": "펜 모드 - 터치 방지",
|
||||||
"link": "선택한 도형에 대해서 링크를 추가/업데이트",
|
"link": "선택한 도형에 대해서 링크를 추가/업데이트",
|
||||||
"eraser": "지우개",
|
"eraser": "지우개",
|
||||||
"hand": "손 (패닝 도구)"
|
"frame": "프레임 도구",
|
||||||
|
"hand": "손 (패닝 도구)",
|
||||||
|
"extraTools": "다른 도구"
|
||||||
},
|
},
|
||||||
"headings": {
|
"headings": {
|
||||||
"canvasActions": "캔버스 동작",
|
"canvasActions": "캔버스 동작",
|
||||||
|
|||||||
+40
-36
@@ -71,7 +71,7 @@
|
|||||||
"language": "زمان",
|
"language": "زمان",
|
||||||
"liveCollaboration": "هاوکاریکردنی زیندو...",
|
"liveCollaboration": "هاوکاریکردنی زیندو...",
|
||||||
"duplicateSelection": "لەبەرگرتنەوە",
|
"duplicateSelection": "لەبەرگرتنەوە",
|
||||||
"untitled": "بێ-ناو",
|
"untitled": "بێ ناونیشان",
|
||||||
"name": "ناو",
|
"name": "ناو",
|
||||||
"yourName": "ناوەکەت",
|
"yourName": "ناوەکەت",
|
||||||
"madeWithExcalidraw": "دروستکراوە بە Excalidraw",
|
"madeWithExcalidraw": "دروستکراوە بە Excalidraw",
|
||||||
@@ -124,7 +124,9 @@
|
|||||||
},
|
},
|
||||||
"statusPublished": "بڵاوکراوەتەوە",
|
"statusPublished": "بڵاوکراوەتەوە",
|
||||||
"sidebarLock": "هێشتنەوەی شریتی لا بە کراوەیی",
|
"sidebarLock": "هێشتنەوەی شریتی لا بە کراوەیی",
|
||||||
"eyeDropper": ""
|
"selectAllElementsInFrame": "هەموو توخمەکانی ناو چوارچێوەکە دیاری بکە",
|
||||||
|
"removeAllElementsFromFrame": "هەموو توخمەکانی ناو چوارچێوەکە لابەرە",
|
||||||
|
"eyeDropper": "ڕەنگێک لەسەر تابلۆکە هەڵبژێرە"
|
||||||
},
|
},
|
||||||
"library": {
|
"library": {
|
||||||
"noItems": "هێشتا هیچ بڕگەیەک زیاد نەکراوە...",
|
"noItems": "هێشتا هیچ بڕگەیەک زیاد نەکراوە...",
|
||||||
@@ -221,7 +223,9 @@
|
|||||||
"penMode": "شێوازی قەڵەم - دەست لێدان ڕابگرە",
|
"penMode": "شێوازی قەڵەم - دەست لێدان ڕابگرە",
|
||||||
"link": "زیادکردن/ نوێکردنەوەی لینک بۆ شێوەی دیاریکراو",
|
"link": "زیادکردن/ نوێکردنەوەی لینک بۆ شێوەی دیاریکراو",
|
||||||
"eraser": "سڕەر",
|
"eraser": "سڕەر",
|
||||||
"hand": "دەست (ئامرازی پانکردن)"
|
"frame": "ئامرازی چوارچێوە",
|
||||||
|
"hand": "دەست (ئامرازی پانکردن)",
|
||||||
|
"extraTools": "ئامرازی زیاتر"
|
||||||
},
|
},
|
||||||
"headings": {
|
"headings": {
|
||||||
"canvasActions": "کردارەکانی تابلۆ",
|
"canvasActions": "کردارەکانی تابلۆ",
|
||||||
@@ -356,27 +360,27 @@
|
|||||||
"removeItemsFromLib": "لابردنی ئایتمە دیاریکراوەکان لە کتێبخانە"
|
"removeItemsFromLib": "لابردنی ئایتمە دیاریکراوەکان لە کتێبخانە"
|
||||||
},
|
},
|
||||||
"imageExportDialog": {
|
"imageExportDialog": {
|
||||||
"header": "",
|
"header": "وێنە هەناردە بکە",
|
||||||
"label": {
|
"label": {
|
||||||
"withBackground": "",
|
"withBackground": "پاشبنەما",
|
||||||
"onlySelected": "",
|
"onlySelected": "تەنها دیاریکراوەکان",
|
||||||
"darkMode": "",
|
"darkMode": "دۆخی تاریک",
|
||||||
"embedScene": "",
|
"embedScene": "دیمەنەکە بەکاربهێنەرەوە",
|
||||||
"scale": "",
|
"scale": "قەبارە",
|
||||||
"padding": ""
|
"padding": "بۆشایی"
|
||||||
},
|
},
|
||||||
"tooltip": {
|
"tooltip": {
|
||||||
"embedScene": ""
|
"embedScene": "داتاکانی دیمەنەکە لە فایلە هەناردەکراوەکەی PNG/SVG هەڵدەگیرێن بۆ ئەوەی دیمەنەکە لێیەوە بگەڕێتەوە.\nقەبارەی پەڕگەی هەناردەکراو زیاد دەکات."
|
||||||
},
|
},
|
||||||
"title": {
|
"title": {
|
||||||
"exportToPng": "",
|
"exportToPng": "هەناردە بکە وەک PNG",
|
||||||
"exportToSvg": "",
|
"exportToSvg": "هەناردە بکە وەک SVG",
|
||||||
"copyPngToClipboard": ""
|
"copyPngToClipboard": "لەبەربگرەوە بۆ سەر تەختەنوس"
|
||||||
},
|
},
|
||||||
"button": {
|
"button": {
|
||||||
"exportToPng": "",
|
"exportToPng": "PNG",
|
||||||
"exportToSvg": "",
|
"exportToSvg": "SVG",
|
||||||
"copyPngToClipboard": ""
|
"copyPngToClipboard": "لهبهری بگرهوه بۆ تهختهنووس"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"encrypted": {
|
"encrypted": {
|
||||||
@@ -411,20 +415,20 @@
|
|||||||
},
|
},
|
||||||
"colors": {
|
"colors": {
|
||||||
"transparent": "ڕوون",
|
"transparent": "ڕوون",
|
||||||
"black": "",
|
"black": "ڕەش",
|
||||||
"white": "",
|
"white": "سپی",
|
||||||
"red": "",
|
"red": "سور",
|
||||||
"pink": "",
|
"pink": "پەمەیی",
|
||||||
"grape": "",
|
"grape": "مێوژی",
|
||||||
"violet": "",
|
"violet": "مۆری کاڵ",
|
||||||
"gray": "",
|
"gray": "خۆڵەمێشی",
|
||||||
"blue": "",
|
"blue": "شین",
|
||||||
"cyan": "",
|
"cyan": "شینی ئاسمانی",
|
||||||
"teal": "",
|
"teal": "شەدری",
|
||||||
"green": "",
|
"green": "سهوز",
|
||||||
"yellow": "",
|
"yellow": "زەرد",
|
||||||
"orange": "",
|
"orange": "پرتەقاڵی",
|
||||||
"bronze": ""
|
"bronze": "برۆنزی"
|
||||||
},
|
},
|
||||||
"welcomeScreen": {
|
"welcomeScreen": {
|
||||||
"app": {
|
"app": {
|
||||||
@@ -440,10 +444,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"colorPicker": {
|
"colorPicker": {
|
||||||
"mostUsedCustomColors": "",
|
"mostUsedCustomColors": "زۆرترین ڕەنگە باوە بەکارهاتووەکان",
|
||||||
"colors": "",
|
"colors": "ڕەنگەکان",
|
||||||
"shades": "",
|
"shades": "سێبەرەکان",
|
||||||
"hexCode": "",
|
"hexCode": "کۆدی هێکس",
|
||||||
"noShades": ""
|
"noShades": "هیچ سێبەرێک بۆ ئەم ڕەنگە بەردەست نییە"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user