Compare commits
23 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 |
+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`. |
|
||||||
|
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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": "هیچ سێبەرێک بۆ ئەم ڕەنگە بەردەست نییە"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -124,6 +124,8 @@
|
|||||||
},
|
},
|
||||||
"statusPublished": "",
|
"statusPublished": "",
|
||||||
"sidebarLock": "",
|
"sidebarLock": "",
|
||||||
|
"selectAllElementsInFrame": "",
|
||||||
|
"removeAllElementsFromFrame": "",
|
||||||
"eyeDropper": ""
|
"eyeDropper": ""
|
||||||
},
|
},
|
||||||
"library": {
|
"library": {
|
||||||
@@ -221,7 +223,9 @@
|
|||||||
"penMode": "Rašyklio režimas - neleisti prisilietimų",
|
"penMode": "Rašyklio režimas - neleisti prisilietimų",
|
||||||
"link": "Pridėti / Atnaujinti pasirinktos figūros nuorodą",
|
"link": "Pridėti / Atnaujinti pasirinktos figūros nuorodą",
|
||||||
"eraser": "Trintukas",
|
"eraser": "Trintukas",
|
||||||
"hand": ""
|
"frame": "",
|
||||||
|
"hand": "",
|
||||||
|
"extraTools": ""
|
||||||
},
|
},
|
||||||
"headings": {
|
"headings": {
|
||||||
"canvasActions": "Veiksmai su drobe",
|
"canvasActions": "Veiksmai su drobe",
|
||||||
|
|||||||
@@ -124,6 +124,8 @@
|
|||||||
},
|
},
|
||||||
"statusPublished": "Publicēts",
|
"statusPublished": "Publicēts",
|
||||||
"sidebarLock": "Paturēt atvērtu sānjoslu",
|
"sidebarLock": "Paturēt atvērtu sānjoslu",
|
||||||
|
"selectAllElementsInFrame": "",
|
||||||
|
"removeAllElementsFromFrame": "",
|
||||||
"eyeDropper": ""
|
"eyeDropper": ""
|
||||||
},
|
},
|
||||||
"library": {
|
"library": {
|
||||||
@@ -221,7 +223,9 @@
|
|||||||
"penMode": "Pildspalvas režīms – novērst pieskaršanos",
|
"penMode": "Pildspalvas režīms – novērst pieskaršanos",
|
||||||
"link": "Pievienot/rediģēt atlasītās figūras saiti",
|
"link": "Pievienot/rediģēt atlasītās figūras saiti",
|
||||||
"eraser": "Dzēšgumija",
|
"eraser": "Dzēšgumija",
|
||||||
"hand": "Roka (panoramēšanas rīks)"
|
"frame": "",
|
||||||
|
"hand": "Roka (panoramēšanas rīks)",
|
||||||
|
"extraTools": ""
|
||||||
},
|
},
|
||||||
"headings": {
|
"headings": {
|
||||||
"canvasActions": "Tāfeles darbības",
|
"canvasActions": "Tāfeles darbības",
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user