Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3ad69c661f | |||
| 56ea3c5bd0 |
+5
-1
@@ -20,10 +20,14 @@ 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
|
||||||
|
|||||||
+10
-1
@@ -11,5 +11,14 @@ 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=
|
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
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,35 +165,3 @@ 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,32 +306,30 @@ This is the history API. history.clear() will clear the history.
|
|||||||
|
|
||||||
## scrollToContent
|
## scrollToContent
|
||||||
|
|
||||||
```tsx
|
<pre>
|
||||||
(
|
(<br />
|
||||||
target?: ExcalidrawElement | ExcalidrawElement[],
|
{" "}
|
||||||
opts?:
|
target?:{" "}
|
||||||
| {
|
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L115">
|
||||||
fitToContent?: boolean;
|
ExcalidrawElement
|
||||||
animate?: boolean;
|
</a>{" "}
|
||||||
duration?: number;
|
|{" "}
|
||||||
}
|
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L115">
|
||||||
| {
|
ExcalidrawElement
|
||||||
fitToViewport?: boolean;
|
</a>
|
||||||
viewportZoomFactor?: number;
|
[],
|
||||||
animate?: boolean;
|
<br />
|
||||||
duration?: number;
|
{" "}opts?: { fitToContent?: boolean; animate?: boolean; duration?: number
|
||||||
}
|
}
|
||||||
) => void
|
<br />) => 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 | [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. |
|
| target | <code>ExcalidrawElement | ExcalidrawElement[]</code> | 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. Note that the zoom range is between 10%-100%. |
|
| opts.fitToContent | boolean | false | Whether to fit the elements to viewport by automatically changing zoom as needed. |
|
||||||
| 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,7 +19,6 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"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",
|
||||||
|
|||||||
+38
-33
@@ -148,6 +148,33 @@
|
|||||||
// 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>
|
||||||
@@ -200,39 +227,17 @@
|
|||||||
<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
|
||||||
// need to load this script dynamically bcs. of iframe embed tracking
|
async
|
||||||
var scriptEle = document.createElement("script");
|
defer
|
||||||
scriptEle.setAttribute(
|
src="https://scripts.simpleanalyticscdn.com/latest.js"
|
||||||
"src",
|
></script>
|
||||||
"https://scripts.simpleanalyticscdn.com/latest.js",
|
<noscript
|
||||||
);
|
><img
|
||||||
scriptEle.setAttribute("type", "text/javascript");
|
src="https://queue.simpleanalyticscdn.com/noscript.gif"
|
||||||
scriptEle.setAttribute("defer", true);
|
alt=""
|
||||||
scriptEle.setAttribute("async", true);
|
referrerpolicy="no-referrer-when-downgrade"
|
||||||
// if iframe
|
/></noscript>
|
||||||
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,4 +1,6 @@
|
|||||||
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";
|
||||||
@@ -7,11 +9,14 @@ export const actionAddToLibrary = register({
|
|||||||
name: "addToLibrary",
|
name: "addToLibrary",
|
||||||
trackEvent: { category: "element" },
|
trackEvent: { category: "element" },
|
||||||
perform: (elements, appState, _, app) => {
|
perform: (elements, appState, _, app) => {
|
||||||
const selectedElements = app.scene.getSelectedElements({
|
const selectedElements = getSelectedElements(
|
||||||
selectedElementIds: appState.selectedElementIds,
|
getNonDeletedElements(elements),
|
||||||
includeBoundTextElement: true,
|
appState,
|
||||||
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,
|
||||||
|
|||||||
+34
-32
@@ -13,18 +13,19 @@ 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 { isSomeElementSelected } from "../scene";
|
import { getSelectedElements, isSomeElementSelected } from "../scene";
|
||||||
import { AppClassProperties, AppState } from "../types";
|
import { 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 = app.scene.getSelectedElements(appState);
|
const selectedElements = getSelectedElements(
|
||||||
|
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
|
||||||
@@ -35,10 +36,12 @@ 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 = app.scene.getSelectedElements(appState);
|
const selectedElements = getSelectedElements(
|
||||||
|
getNonDeletedElements(elements),
|
||||||
|
appState,
|
||||||
|
);
|
||||||
|
|
||||||
const updatedElements = alignElements(selectedElements, alignment);
|
const updatedElements = alignElements(selectedElements, alignment);
|
||||||
|
|
||||||
@@ -47,7 +50,6 @@ 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,
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -55,10 +57,10 @@ export const actionAlignTop = register({
|
|||||||
name: "alignTop",
|
name: "alignTop",
|
||||||
trackEvent: { category: "element" },
|
trackEvent: { category: "element" },
|
||||||
predicate: alignActionsPredicate,
|
predicate: alignActionsPredicate,
|
||||||
perform: (elements, appState, _, app) => {
|
perform: (elements, appState) => {
|
||||||
return {
|
return {
|
||||||
appState,
|
appState,
|
||||||
elements: alignSelectedElements(elements, appState, app, {
|
elements: alignSelectedElements(elements, appState, {
|
||||||
position: "start",
|
position: "start",
|
||||||
axis: "y",
|
axis: "y",
|
||||||
}),
|
}),
|
||||||
@@ -67,9 +69,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, app }) => (
|
PanelComponent: ({ elements, appState, updateData }) => (
|
||||||
<ToolButton
|
<ToolButton
|
||||||
hidden={!alignActionsPredicate(elements, appState, null, app)}
|
hidden={!alignActionsPredicate(elements, appState)}
|
||||||
type="button"
|
type="button"
|
||||||
icon={AlignTopIcon}
|
icon={AlignTopIcon}
|
||||||
onClick={() => updateData(null)}
|
onClick={() => updateData(null)}
|
||||||
@@ -86,10 +88,10 @@ export const actionAlignBottom = register({
|
|||||||
name: "alignBottom",
|
name: "alignBottom",
|
||||||
trackEvent: { category: "element" },
|
trackEvent: { category: "element" },
|
||||||
predicate: alignActionsPredicate,
|
predicate: alignActionsPredicate,
|
||||||
perform: (elements, appState, _, app) => {
|
perform: (elements, appState) => {
|
||||||
return {
|
return {
|
||||||
appState,
|
appState,
|
||||||
elements: alignSelectedElements(elements, appState, app, {
|
elements: alignSelectedElements(elements, appState, {
|
||||||
position: "end",
|
position: "end",
|
||||||
axis: "y",
|
axis: "y",
|
||||||
}),
|
}),
|
||||||
@@ -98,9 +100,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, app }) => (
|
PanelComponent: ({ elements, appState, updateData }) => (
|
||||||
<ToolButton
|
<ToolButton
|
||||||
hidden={!alignActionsPredicate(elements, appState, null, app)}
|
hidden={!alignActionsPredicate(elements, appState)}
|
||||||
type="button"
|
type="button"
|
||||||
icon={AlignBottomIcon}
|
icon={AlignBottomIcon}
|
||||||
onClick={() => updateData(null)}
|
onClick={() => updateData(null)}
|
||||||
@@ -117,10 +119,10 @@ export const actionAlignLeft = register({
|
|||||||
name: "alignLeft",
|
name: "alignLeft",
|
||||||
trackEvent: { category: "element" },
|
trackEvent: { category: "element" },
|
||||||
predicate: alignActionsPredicate,
|
predicate: alignActionsPredicate,
|
||||||
perform: (elements, appState, _, app) => {
|
perform: (elements, appState) => {
|
||||||
return {
|
return {
|
||||||
appState,
|
appState,
|
||||||
elements: alignSelectedElements(elements, appState, app, {
|
elements: alignSelectedElements(elements, appState, {
|
||||||
position: "start",
|
position: "start",
|
||||||
axis: "x",
|
axis: "x",
|
||||||
}),
|
}),
|
||||||
@@ -129,9 +131,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, app }) => (
|
PanelComponent: ({ elements, appState, updateData }) => (
|
||||||
<ToolButton
|
<ToolButton
|
||||||
hidden={!alignActionsPredicate(elements, appState, null, app)}
|
hidden={!alignActionsPredicate(elements, appState)}
|
||||||
type="button"
|
type="button"
|
||||||
icon={AlignLeftIcon}
|
icon={AlignLeftIcon}
|
||||||
onClick={() => updateData(null)}
|
onClick={() => updateData(null)}
|
||||||
@@ -148,10 +150,10 @@ export const actionAlignRight = register({
|
|||||||
name: "alignRight",
|
name: "alignRight",
|
||||||
trackEvent: { category: "element" },
|
trackEvent: { category: "element" },
|
||||||
predicate: alignActionsPredicate,
|
predicate: alignActionsPredicate,
|
||||||
perform: (elements, appState, _, app) => {
|
perform: (elements, appState) => {
|
||||||
return {
|
return {
|
||||||
appState,
|
appState,
|
||||||
elements: alignSelectedElements(elements, appState, app, {
|
elements: alignSelectedElements(elements, appState, {
|
||||||
position: "end",
|
position: "end",
|
||||||
axis: "x",
|
axis: "x",
|
||||||
}),
|
}),
|
||||||
@@ -160,9 +162,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, app }) => (
|
PanelComponent: ({ elements, appState, updateData }) => (
|
||||||
<ToolButton
|
<ToolButton
|
||||||
hidden={!alignActionsPredicate(elements, appState, null, app)}
|
hidden={!alignActionsPredicate(elements, appState)}
|
||||||
type="button"
|
type="button"
|
||||||
icon={AlignRightIcon}
|
icon={AlignRightIcon}
|
||||||
onClick={() => updateData(null)}
|
onClick={() => updateData(null)}
|
||||||
@@ -179,19 +181,19 @@ export const actionAlignVerticallyCentered = register({
|
|||||||
name: "alignVerticallyCentered",
|
name: "alignVerticallyCentered",
|
||||||
trackEvent: { category: "element" },
|
trackEvent: { category: "element" },
|
||||||
predicate: alignActionsPredicate,
|
predicate: alignActionsPredicate,
|
||||||
perform: (elements, appState, _, app) => {
|
perform: (elements, appState) => {
|
||||||
return {
|
return {
|
||||||
appState,
|
appState,
|
||||||
elements: alignSelectedElements(elements, appState, app, {
|
elements: alignSelectedElements(elements, appState, {
|
||||||
position: "center",
|
position: "center",
|
||||||
axis: "y",
|
axis: "y",
|
||||||
}),
|
}),
|
||||||
commitToHistory: true,
|
commitToHistory: true,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
PanelComponent: ({ elements, appState, updateData, app }) => (
|
PanelComponent: ({ elements, appState, updateData }) => (
|
||||||
<ToolButton
|
<ToolButton
|
||||||
hidden={!alignActionsPredicate(elements, appState, null, app)}
|
hidden={!alignActionsPredicate(elements, appState)}
|
||||||
type="button"
|
type="button"
|
||||||
icon={CenterVerticallyIcon}
|
icon={CenterVerticallyIcon}
|
||||||
onClick={() => updateData(null)}
|
onClick={() => updateData(null)}
|
||||||
@@ -206,19 +208,19 @@ export const actionAlignHorizontallyCentered = register({
|
|||||||
name: "alignHorizontallyCentered",
|
name: "alignHorizontallyCentered",
|
||||||
trackEvent: { category: "element" },
|
trackEvent: { category: "element" },
|
||||||
predicate: alignActionsPredicate,
|
predicate: alignActionsPredicate,
|
||||||
perform: (elements, appState, _, app) => {
|
perform: (elements, appState) => {
|
||||||
return {
|
return {
|
||||||
appState,
|
appState,
|
||||||
elements: alignSelectedElements(elements, appState, app, {
|
elements: alignSelectedElements(elements, appState, {
|
||||||
position: "center",
|
position: "center",
|
||||||
axis: "x",
|
axis: "x",
|
||||||
}),
|
}),
|
||||||
commitToHistory: true,
|
commitToHistory: true,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
PanelComponent: ({ elements, appState, updateData, app }) => (
|
PanelComponent: ({ elements, appState, updateData }) => (
|
||||||
<ToolButton
|
<ToolButton
|
||||||
hidden={!alignActionsPredicate(elements, appState, null, app)}
|
hidden={!alignActionsPredicate(elements, appState)}
|
||||||
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 { isTextElement, newElement } from "../element";
|
import { getNonDeletedElements, 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,13 +38,16 @@ export const actionUnbindText = register({
|
|||||||
name: "unbindText",
|
name: "unbindText",
|
||||||
contextItemLabel: "labels.unbindText",
|
contextItemLabel: "labels.unbindText",
|
||||||
trackEvent: { category: "element" },
|
trackEvent: { category: "element" },
|
||||||
predicate: (elements, appState, _, app) => {
|
predicate: (elements, appState) => {
|
||||||
const selectedElements = app.scene.getSelectedElements(appState);
|
const selectedElements = getSelectedElements(elements, appState);
|
||||||
|
|
||||||
return selectedElements.some((element) => hasBoundTextElement(element));
|
return selectedElements.some((element) => hasBoundTextElement(element));
|
||||||
},
|
},
|
||||||
perform: (elements, appState, _, app) => {
|
perform: (elements, appState) => {
|
||||||
const selectedElements = app.scene.getSelectedElements(appState);
|
const selectedElements = getSelectedElements(
|
||||||
|
getNonDeletedElements(elements),
|
||||||
|
appState,
|
||||||
|
);
|
||||||
selectedElements.forEach((element) => {
|
selectedElements.forEach((element) => {
|
||||||
const boundTextElement = getBoundTextElement(element);
|
const boundTextElement = getBoundTextElement(element);
|
||||||
if (boundTextElement) {
|
if (boundTextElement) {
|
||||||
@@ -89,8 +92,8 @@ export const actionBindText = register({
|
|||||||
name: "bindText",
|
name: "bindText",
|
||||||
contextItemLabel: "labels.bindText",
|
contextItemLabel: "labels.bindText",
|
||||||
trackEvent: { category: "element" },
|
trackEvent: { category: "element" },
|
||||||
predicate: (elements, appState, _, app) => {
|
predicate: (elements, appState) => {
|
||||||
const selectedElements = app.scene.getSelectedElements(appState);
|
const selectedElements = getSelectedElements(elements, appState);
|
||||||
|
|
||||||
if (selectedElements.length === 2) {
|
if (selectedElements.length === 2) {
|
||||||
const textElement =
|
const textElement =
|
||||||
@@ -113,8 +116,11 @@ export const actionBindText = register({
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
perform: (elements, appState, _, app) => {
|
perform: (elements, appState) => {
|
||||||
const selectedElements = app.scene.getSelectedElements(appState);
|
const selectedElements = getSelectedElements(
|
||||||
|
getNonDeletedElements(elements),
|
||||||
|
appState,
|
||||||
|
);
|
||||||
|
|
||||||
let textElement: ExcalidrawTextElement;
|
let textElement: ExcalidrawTextElement;
|
||||||
let container: ExcalidrawTextContainer;
|
let container: ExcalidrawTextContainer;
|
||||||
@@ -194,15 +200,18 @@ export const actionWrapTextInContainer = register({
|
|||||||
name: "wrapTextInContainer",
|
name: "wrapTextInContainer",
|
||||||
contextItemLabel: "labels.createContainerFromText",
|
contextItemLabel: "labels.createContainerFromText",
|
||||||
trackEvent: { category: "element" },
|
trackEvent: { category: "element" },
|
||||||
predicate: (elements, appState, _, app) => {
|
predicate: (elements, appState) => {
|
||||||
const selectedElements = app.scene.getSelectedElements(appState);
|
const selectedElements = getSelectedElements(elements, 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, _, app) => {
|
perform: (elements, appState) => {
|
||||||
const selectedElements = app.scene.getSelectedElements(appState);
|
const selectedElements = getSelectedElements(
|
||||||
|
getNonDeletedElements(elements),
|
||||||
|
appState,
|
||||||
|
);
|
||||||
let updatedElements: readonly ExcalidrawElement[] = elements.slice();
|
let updatedElements: readonly ExcalidrawElement[] = elements.slice();
|
||||||
const containerIds: Mutable<AppState["selectedElementIds"]> = {};
|
const containerIds: 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 } from "../scene";
|
import { getNormalizedZoom, getSelectedElements } 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,6 +20,7 @@ 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({
|
||||||
@@ -225,93 +226,52 @@ const zoomValueToFitBoundsOnViewport = (
|
|||||||
return clampedZoomValueToFitElements as NormalizedZoomValue;
|
return clampedZoomValueToFitElements as NormalizedZoomValue;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const zoomToFit = ({
|
export const zoomToFitElements = (
|
||||||
targetElements,
|
elements: readonly ExcalidrawElement[],
|
||||||
appState,
|
appState: Readonly<AppState>,
|
||||||
fitToViewport = false,
|
zoomToSelection: boolean,
|
||||||
viewportZoomFactor = 0.7,
|
) => {
|
||||||
}: {
|
const nonDeletedElements = getNonDeletedElements(elements);
|
||||||
targetElements: readonly ExcalidrawElement[];
|
const selectedElements = getSelectedElements(nonDeletedElements, appState);
|
||||||
appState: Readonly<AppState>;
|
|
||||||
/** whether to fit content to viewport (beyond >100%) */
|
const commonBounds =
|
||||||
fitToViewport: boolean;
|
zoomToSelection && selectedElements.length > 0
|
||||||
/** zoom content to cover X of the viewport, when fitToViewport=true */
|
? getCommonBounds(excludeElementsInFramesFromSelection(selectedElements))
|
||||||
viewportZoomFactor?: number;
|
: getCommonBounds(
|
||||||
}) => {
|
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,
|
||||||
scrollX,
|
...centerScrollOn({
|
||||||
scrollY,
|
scenePoint: { x: centerX, y: centerY },
|
||||||
zoom: { value: newZoomValue },
|
viewportDimensions: {
|
||||||
|
width: appState.width,
|
||||||
|
height: appState.height,
|
||||||
|
},
|
||||||
|
zoom: newZoom,
|
||||||
|
}),
|
||||||
|
zoom: newZoom,
|
||||||
},
|
},
|
||||||
commitToHistory: false,
|
commitToHistory: false,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// Note, this action differs from actionZoomToFitSelection in that it doesn't
|
export const actionZoomToSelected = register({
|
||||||
// zoom beyond 100%. In other words, if the content is smaller than viewport
|
name: "zoomToSelection",
|
||||||
// size, it won't be zoomed in.
|
|
||||||
export const actionZoomToFitSelectionInViewport = register({
|
|
||||||
name: "zoomToFitSelectionInViewport",
|
|
||||||
trackEvent: { category: "canvas" },
|
trackEvent: { category: "canvas" },
|
||||||
perform: (elements, appState, _, app) => {
|
perform: (elements, appState) => zoomToFitElements(elements, appState, true),
|
||||||
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 &&
|
||||||
@@ -319,31 +279,11 @@ export const actionZoomToFitSelectionInViewport = 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) =>
|
perform: (elements, appState) => zoomToFitElements(elements, appState, false),
|
||||||
zoomToFit({ targetElements: elements, appState, fitToViewport: false }),
|
|
||||||
keyTest: (event) =>
|
keyTest: (event) =>
|
||||||
event.code === CODES.ONE &&
|
event.code === CODES.ONE &&
|
||||||
event.shiftKey &&
|
event.shiftKey &&
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ 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";
|
||||||
@@ -15,8 +16,7 @@ export const actionCopy = register({
|
|||||||
name: "copy",
|
name: "copy",
|
||||||
trackEvent: { category: "element" },
|
trackEvent: { category: "element" },
|
||||||
perform: (elements, appState, _, app) => {
|
perform: (elements, appState, _, app) => {
|
||||||
const elementsToCopy = app.scene.getSelectedElements({
|
const elementsToCopy = getSelectedElements(elements, appState, {
|
||||||
selectedElementIds: appState.selectedElementIds,
|
|
||||||
includeBoundTextElement: true,
|
includeBoundTextElement: true,
|
||||||
includeElementsInFrames: true,
|
includeElementsInFrames: true,
|
||||||
});
|
});
|
||||||
@@ -75,11 +75,14 @@ export const actionCopyAsSvg = register({
|
|||||||
commitToHistory: false,
|
commitToHistory: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const selectedElements = app.scene.getSelectedElements({
|
const selectedElements = getSelectedElements(
|
||||||
selectedElementIds: appState.selectedElementIds,
|
getNonDeletedElements(elements),
|
||||||
includeBoundTextElement: true,
|
appState,
|
||||||
includeElementsInFrames: true,
|
{
|
||||||
});
|
includeBoundTextElement: true,
|
||||||
|
includeElementsInFrames: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
try {
|
try {
|
||||||
await exportCanvas(
|
await exportCanvas(
|
||||||
"clipboard-svg",
|
"clipboard-svg",
|
||||||
@@ -119,11 +122,14 @@ export const actionCopyAsPng = register({
|
|||||||
commitToHistory: false,
|
commitToHistory: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const selectedElements = app.scene.getSelectedElements({
|
const selectedElements = getSelectedElements(
|
||||||
selectedElementIds: appState.selectedElementIds,
|
getNonDeletedElements(elements),
|
||||||
includeBoundTextElement: true,
|
appState,
|
||||||
includeElementsInFrames: true,
|
{
|
||||||
});
|
includeBoundTextElement: true,
|
||||||
|
includeElementsInFrames: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
try {
|
try {
|
||||||
await exportCanvas(
|
await exportCanvas(
|
||||||
"clipboard",
|
"clipboard",
|
||||||
@@ -171,11 +177,14 @@ 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, _, app) => {
|
perform: (elements, appState) => {
|
||||||
const selectedElements = app.scene.getSelectedElements({
|
const selectedElements = getSelectedElements(
|
||||||
selectedElementIds: appState.selectedElementIds,
|
getNonDeletedElements(elements),
|
||||||
includeBoundTextElement: true,
|
appState,
|
||||||
});
|
{
|
||||||
|
includeBoundTextElement: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const text = selectedElements
|
const text = selectedElements
|
||||||
.reduce((acc: string[], element) => {
|
.reduce((acc: string[], element) => {
|
||||||
@@ -190,15 +199,12 @@ export const copyText = register({
|
|||||||
commitToHistory: false,
|
commitToHistory: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
predicate: (elements, appState, _, app) => {
|
predicate: (elements, appState) => {
|
||||||
return (
|
return (
|
||||||
probablySupportsClipboardWriteText &&
|
probablySupportsClipboardWriteText &&
|
||||||
app.scene
|
getSelectedElements(elements, appState, {
|
||||||
.getSelectedElements({
|
includeBoundTextElement: true,
|
||||||
selectedElementIds: appState.selectedElementIds,
|
}).some(isTextElement)
|
||||||
includeBoundTextElement: true,
|
|
||||||
})
|
|
||||||
.some(isTextElement)
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
contextItemLabel: "labels.copyText",
|
contextItemLabel: "labels.copyText",
|
||||||
|
|||||||
@@ -9,13 +9,19 @@ 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 { isSomeElementSelected } from "../scene";
|
import { getSelectedElements, isSomeElementSelected } from "../scene";
|
||||||
import { AppClassProperties, AppState } from "../types";
|
import { AppState } from "../types";
|
||||||
import { arrayToMap, getShortcutKey } from "../utils";
|
import { arrayToMap, getShortcutKey } from "../utils";
|
||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
|
|
||||||
const enableActionGroup = (appState: AppState, app: AppClassProperties) => {
|
const enableActionGroup = (
|
||||||
const selectedElements = app.scene.getSelectedElements(appState);
|
elements: readonly ExcalidrawElement[],
|
||||||
|
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
|
||||||
@@ -26,10 +32,12 @@ const enableActionGroup = (appState: AppState, app: AppClassProperties) => {
|
|||||||
const distributeSelectedElements = (
|
const distributeSelectedElements = (
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly ExcalidrawElement[],
|
||||||
appState: Readonly<AppState>,
|
appState: Readonly<AppState>,
|
||||||
app: AppClassProperties,
|
|
||||||
distribution: Distribution,
|
distribution: Distribution,
|
||||||
) => {
|
) => {
|
||||||
const selectedElements = app.scene.getSelectedElements(appState);
|
const selectedElements = getSelectedElements(
|
||||||
|
getNonDeletedElements(elements),
|
||||||
|
appState,
|
||||||
|
);
|
||||||
|
|
||||||
const updatedElements = distributeElements(selectedElements, distribution);
|
const updatedElements = distributeElements(selectedElements, distribution);
|
||||||
|
|
||||||
@@ -38,17 +46,16 @@ 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, _, app) => {
|
perform: (elements, appState) => {
|
||||||
return {
|
return {
|
||||||
appState,
|
appState,
|
||||||
elements: distributeSelectedElements(elements, appState, app, {
|
elements: distributeSelectedElements(elements, appState, {
|
||||||
space: "between",
|
space: "between",
|
||||||
axis: "x",
|
axis: "x",
|
||||||
}),
|
}),
|
||||||
@@ -57,9 +64,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, app }) => (
|
PanelComponent: ({ elements, appState, updateData }) => (
|
||||||
<ToolButton
|
<ToolButton
|
||||||
hidden={!enableActionGroup(appState, app)}
|
hidden={!enableActionGroup(elements, appState)}
|
||||||
type="button"
|
type="button"
|
||||||
icon={DistributeHorizontallyIcon}
|
icon={DistributeHorizontallyIcon}
|
||||||
onClick={() => updateData(null)}
|
onClick={() => updateData(null)}
|
||||||
@@ -75,10 +82,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, _, app) => {
|
perform: (elements, appState) => {
|
||||||
return {
|
return {
|
||||||
appState,
|
appState,
|
||||||
elements: distributeSelectedElements(elements, appState, app, {
|
elements: distributeSelectedElements(elements, appState, {
|
||||||
space: "between",
|
space: "between",
|
||||||
axis: "y",
|
axis: "y",
|
||||||
}),
|
}),
|
||||||
@@ -87,9 +94,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, app }) => (
|
PanelComponent: ({ elements, appState, updateData }) => (
|
||||||
<ToolButton
|
<ToolButton
|
||||||
hidden={!enableActionGroup(appState, app)}
|
hidden={!enableActionGroup(elements, appState)}
|
||||||
type="button"
|
type="button"
|
||||||
icon={DistributeVerticallyIcon}
|
icon={DistributeVerticallyIcon}
|
||||||
onClick={() => updateData(null)}
|
onClick={() => updateData(null)}
|
||||||
|
|||||||
@@ -274,8 +274,6 @@ const duplicateElements = (
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
getNonDeletedElements(finalElements),
|
getNonDeletedElements(finalElements),
|
||||||
appState,
|
|
||||||
null,
|
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
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";
|
||||||
|
|
||||||
@@ -10,15 +11,14 @@ 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, _, app) => {
|
predicate: (elements, appState) => {
|
||||||
const selectedElements = app.scene.getSelectedElements(appState);
|
const selectedElements = getSelectedElements(elements, appState);
|
||||||
return !selectedElements.some(
|
return !selectedElements.some(
|
||||||
(element) => element.locked && element.frameId,
|
(element) => element.locked && element.frameId,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
perform: (elements, appState, _, app) => {
|
perform: (elements, appState) => {
|
||||||
const selectedElements = app.scene.getSelectedElements({
|
const selectedElements = getSelectedElements(elements, appState, {
|
||||||
selectedElementIds: appState.selectedElementIds,
|
|
||||||
includeBoundTextElement: true,
|
includeBoundTextElement: true,
|
||||||
includeElementsInFrames: true,
|
includeElementsInFrames: true,
|
||||||
});
|
});
|
||||||
@@ -46,9 +46,8 @@ export const actionToggleElementLock = register({
|
|||||||
commitToHistory: true,
|
commitToHistory: true,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
contextItemLabel: (elements, appState, app) => {
|
contextItemLabel: (elements, appState) => {
|
||||||
const selected = app.scene.getSelectedElements({
|
const selected = getSelectedElements(elements, appState, {
|
||||||
selectedElementIds: appState.selectedElementIds,
|
|
||||||
includeBoundTextElement: false,
|
includeBoundTextElement: false,
|
||||||
});
|
});
|
||||||
if (selected.length === 1 && selected[0].type !== "frame") {
|
if (selected.length === 1 && selected[0].type !== "frame") {
|
||||||
@@ -61,13 +60,12 @@ export const actionToggleElementLock = register({
|
|||||||
? "labels.elementLock.lockAll"
|
? "labels.elementLock.lockAll"
|
||||||
: "labels.elementLock.unlockAll";
|
: "labels.elementLock.unlockAll";
|
||||||
},
|
},
|
||||||
keyTest: (event, appState, elements, app) => {
|
keyTest: (event, appState, elements) => {
|
||||||
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 &&
|
||||||
app.scene.getSelectedElements({
|
getSelectedElements(elements, appState, {
|
||||||
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(
|
||||||
"imageExportDialog.label.scale",
|
"buttons.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("imageExportDialog.label.withBackground")}
|
{t("labels.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("imageExportDialog.label.embedScene")}
|
{t("labels.exportEmbedScene")}
|
||||||
<Tooltip label={t("imageExportDialog.tooltip.embedScene")} long={true}>
|
<Tooltip label={t("labels.exportEmbedScene_details")} 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("imageExportDialog.label.darkMode")}
|
title={t("labels.toggleExportColorScheme")}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -125,6 +125,13 @@ export const actionFinalize = register({
|
|||||||
{ x, y },
|
{ x, y },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!appState.activeTool.locked &&
|
||||||
|
appState.activeTool.type !== "freedraw"
|
||||||
|
) {
|
||||||
|
appState.selectedElementIds[multiPointElement.id] = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
|||||||
@@ -17,12 +17,11 @@ 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, _, app) => {
|
perform: (elements, appState) => {
|
||||||
return {
|
return {
|
||||||
elements: updateFrameMembershipOfSelectedElements(
|
elements: updateFrameMembershipOfSelectedElements(
|
||||||
flipSelectedElements(elements, appState, "horizontal"),
|
flipSelectedElements(elements, appState, "horizontal"),
|
||||||
appState,
|
appState,
|
||||||
app,
|
|
||||||
),
|
),
|
||||||
appState,
|
appState,
|
||||||
commitToHistory: true,
|
commitToHistory: true,
|
||||||
@@ -35,12 +34,11 @@ 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, _, app) => {
|
perform: (elements, appState) => {
|
||||||
return {
|
return {
|
||||||
elements: updateFrameMembershipOfSelectedElements(
|
elements: updateFrameMembershipOfSelectedElements(
|
||||||
flipSelectedElements(elements, appState, "vertical"),
|
flipSelectedElements(elements, appState, "vertical"),
|
||||||
appState,
|
appState,
|
||||||
app,
|
|
||||||
),
|
),
|
||||||
appState,
|
appState,
|
||||||
commitToHistory: true,
|
commitToHistory: true,
|
||||||
|
|||||||
+27
-19
@@ -3,12 +3,19 @@ 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 { AppClassProperties, AppState } from "../types";
|
import { getSelectedElements } from "../scene";
|
||||||
|
import { AppState } from "../types";
|
||||||
import { setCursorForShape, updateActiveTool } from "../utils";
|
import { setCursorForShape, updateActiveTool } from "../utils";
|
||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
|
|
||||||
const isSingleFrameSelected = (appState: AppState, app: AppClassProperties) => {
|
const isSingleFrameSelected = (
|
||||||
const selectedElements = app.scene.getSelectedElements(appState);
|
elements: readonly ExcalidrawElement[],
|
||||||
|
appState: AppState,
|
||||||
|
) => {
|
||||||
|
const selectedElements = getSelectedElements(
|
||||||
|
getNonDeletedElements(elements),
|
||||||
|
appState,
|
||||||
|
);
|
||||||
|
|
||||||
return selectedElements.length === 1 && selectedElements[0].type === "frame";
|
return selectedElements.length === 1 && selectedElements[0].type === "frame";
|
||||||
};
|
};
|
||||||
@@ -16,8 +23,11 @@ const isSingleFrameSelected = (appState: AppState, app: AppClassProperties) => {
|
|||||||
export const actionSelectAllElementsInFrame = register({
|
export const actionSelectAllElementsInFrame = register({
|
||||||
name: "selectAllElementsInFrame",
|
name: "selectAllElementsInFrame",
|
||||||
trackEvent: { category: "canvas" },
|
trackEvent: { category: "canvas" },
|
||||||
perform: (elements, appState, _, app) => {
|
perform: (elements, appState) => {
|
||||||
const selectedFrame = app.scene.getSelectedElements(appState)[0];
|
const selectedFrame = getSelectedElements(
|
||||||
|
getNonDeletedElements(elements),
|
||||||
|
appState,
|
||||||
|
)[0];
|
||||||
|
|
||||||
if (selectedFrame && selectedFrame.type === "frame") {
|
if (selectedFrame && selectedFrame.type === "frame") {
|
||||||
const elementsInFrame = getFrameElements(
|
const elementsInFrame = getFrameElements(
|
||||||
@@ -45,15 +55,17 @@ export const actionSelectAllElementsInFrame = register({
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
contextItemLabel: "labels.selectAllElementsInFrame",
|
contextItemLabel: "labels.selectAllElementsInFrame",
|
||||||
predicate: (elements, appState, _, app) =>
|
predicate: (elements, appState) => isSingleFrameSelected(elements, appState),
|
||||||
isSingleFrameSelected(appState, app),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const actionRemoveAllElementsFromFrame = register({
|
export const actionRemoveAllElementsFromFrame = register({
|
||||||
name: "removeAllElementsFromFrame",
|
name: "removeAllElementsFromFrame",
|
||||||
trackEvent: { category: "history" },
|
trackEvent: { category: "history" },
|
||||||
perform: (elements, appState, _, app) => {
|
perform: (elements, appState) => {
|
||||||
const selectedFrame = app.scene.getSelectedElements(appState)[0];
|
const selectedFrame = getSelectedElements(
|
||||||
|
getNonDeletedElements(elements),
|
||||||
|
appState,
|
||||||
|
)[0];
|
||||||
|
|
||||||
if (selectedFrame && selectedFrame.type === "frame") {
|
if (selectedFrame && selectedFrame.type === "frame") {
|
||||||
return {
|
return {
|
||||||
@@ -75,12 +87,11 @@ export const actionRemoveAllElementsFromFrame = register({
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
contextItemLabel: "labels.removeAllElementsFromFrame",
|
contextItemLabel: "labels.removeAllElementsFromFrame",
|
||||||
predicate: (elements, appState, _, app) =>
|
predicate: (elements, appState) => isSingleFrameSelected(elements, appState),
|
||||||
isSingleFrameSelected(appState, app),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const actionupdateFrameRendering = register({
|
export const actionToggleFrameRendering = register({
|
||||||
name: "updateFrameRendering",
|
name: "toggleFrameRendering",
|
||||||
viewMode: true,
|
viewMode: true,
|
||||||
trackEvent: { category: "canvas" },
|
trackEvent: { category: "canvas" },
|
||||||
perform: (elements, appState) => {
|
perform: (elements, appState) => {
|
||||||
@@ -88,16 +99,13 @@ export const actionupdateFrameRendering = register({
|
|||||||
elements,
|
elements,
|
||||||
appState: {
|
appState: {
|
||||||
...appState,
|
...appState,
|
||||||
frameRendering: {
|
shouldRenderFrames: !appState.shouldRenderFrames,
|
||||||
...appState.frameRendering,
|
|
||||||
enabled: !appState.frameRendering.enabled,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
commitToHistory: false,
|
commitToHistory: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
contextItemLabel: "labels.updateFrameRendering",
|
contextItemLabel: "labels.toggleFrameRendering",
|
||||||
checked: (appState: AppState) => appState.frameRendering.enabled,
|
checked: (appState: AppState) => appState.shouldRenderFrames,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const actionSetFrameAsActiveTool = register({
|
export const actionSetFrameAsActiveTool = register({
|
||||||
|
|||||||
+22
-29
@@ -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 { isSomeElementSelected } from "../scene";
|
import { getSelectedElements, 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 { AppClassProperties, AppState } from "../types";
|
import { AppState } from "../types";
|
||||||
import { isBoundToContainer } from "../element/typeChecks";
|
import { isBoundToContainer } from "../element/typeChecks";
|
||||||
import {
|
import {
|
||||||
getElementsInResizingFrame,
|
getElementsInResizingFrame,
|
||||||
@@ -51,12 +51,14 @@ const allElementsInSameGroup = (elements: readonly ExcalidrawElement[]) => {
|
|||||||
const enableActionGroup = (
|
const enableActionGroup = (
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly ExcalidrawElement[],
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
app: AppClassProperties,
|
|
||||||
) => {
|
) => {
|
||||||
const selectedElements = app.scene.getSelectedElements({
|
const selectedElements = getSelectedElements(
|
||||||
selectedElementIds: appState.selectedElementIds,
|
getNonDeletedElements(elements),
|
||||||
includeBoundTextElement: true,
|
appState,
|
||||||
});
|
{
|
||||||
|
includeBoundTextElement: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
return (
|
return (
|
||||||
selectedElements.length >= 2 && !allElementsInSameGroup(selectedElements)
|
selectedElements.length >= 2 && !allElementsInSameGroup(selectedElements)
|
||||||
);
|
);
|
||||||
@@ -66,10 +68,13 @@ export const actionGroup = register({
|
|||||||
name: "group",
|
name: "group",
|
||||||
trackEvent: { category: "element" },
|
trackEvent: { category: "element" },
|
||||||
perform: (elements, appState, _, app) => {
|
perform: (elements, appState, _, app) => {
|
||||||
const selectedElements = app.scene.getSelectedElements({
|
const selectedElements = getSelectedElements(
|
||||||
selectedElementIds: appState.selectedElementIds,
|
getNonDeletedElements(elements),
|
||||||
includeBoundTextElement: true,
|
appState,
|
||||||
});
|
{
|
||||||
|
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 };
|
||||||
@@ -159,13 +164,12 @@ export const actionGroup = register({
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
contextItemLabel: "labels.group",
|
contextItemLabel: "labels.group",
|
||||||
predicate: (elements, appState, _, app) =>
|
predicate: (elements, appState) => enableActionGroup(elements, appState),
|
||||||
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, app }) => (
|
PanelComponent: ({ elements, appState, updateData }) => (
|
||||||
<ToolButton
|
<ToolButton
|
||||||
hidden={!enableActionGroup(elements, appState, app)}
|
hidden={!enableActionGroup(elements, appState)}
|
||||||
type="button"
|
type="button"
|
||||||
icon={<GroupIcon theme={appState.theme} />}
|
icon={<GroupIcon theme={appState.theme} />}
|
||||||
onClick={() => updateData(null)}
|
onClick={() => updateData(null)}
|
||||||
@@ -187,7 +191,7 @@ export const actionUngroup = register({
|
|||||||
|
|
||||||
let nextElements = [...elements];
|
let nextElements = [...elements];
|
||||||
|
|
||||||
const selectedElements = app.scene.getSelectedElements(appState);
|
const selectedElements = getSelectedElements(nextElements, appState);
|
||||||
const frames = selectedElements
|
const frames = selectedElements
|
||||||
.filter((element) => element.frameId)
|
.filter((element) => element.frameId)
|
||||||
.map((element) =>
|
.map((element) =>
|
||||||
@@ -214,8 +218,6 @@ 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) => {
|
||||||
@@ -230,18 +232,9 @@ export const actionUngroup = register({
|
|||||||
});
|
});
|
||||||
|
|
||||||
// remove binded text elements from selection
|
// remove binded text elements from selection
|
||||||
updateAppState.selectedElementIds = Object.entries(
|
boundTextElementIds.forEach(
|
||||||
updateAppState.selectedElementIds,
|
(id) => (updateAppState.selectedElementIds[id] = false),
|
||||||
).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,6 +1,8 @@
|
|||||||
|
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({
|
||||||
@@ -8,18 +10,21 @@ export const actionToggleLinearEditor = register({
|
|||||||
trackEvent: {
|
trackEvent: {
|
||||||
category: "element",
|
category: "element",
|
||||||
},
|
},
|
||||||
predicate: (elements, appState, _, app) => {
|
predicate: (elements, appState) => {
|
||||||
const selectedElements = app.scene.getSelectedElements(appState);
|
const selectedElements = getSelectedElements(elements, 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 = app.scene.getSelectedElements({
|
const selectedElement = getSelectedElements(
|
||||||
selectedElementIds: appState.selectedElementIds,
|
getNonDeletedElements(elements),
|
||||||
includeBoundTextElement: true,
|
appState,
|
||||||
})[0] as ExcalidrawLinearElement;
|
{
|
||||||
|
includeBoundTextElement: true,
|
||||||
|
},
|
||||||
|
)[0] as ExcalidrawLinearElement;
|
||||||
|
|
||||||
const editingLinearElement =
|
const editingLinearElement =
|
||||||
appState.editingLinearElement?.elementId === selectedElement.id
|
appState.editingLinearElement?.elementId === selectedElement.id
|
||||||
@@ -33,11 +38,14 @@ export const actionToggleLinearEditor = register({
|
|||||||
commitToHistory: false,
|
commitToHistory: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
contextItemLabel: (elements, appState, app) => {
|
contextItemLabel: (elements, appState) => {
|
||||||
const selectedElement = app.scene.getSelectedElements({
|
const selectedElement = getSelectedElements(
|
||||||
selectedElementIds: appState.selectedElementIds,
|
getNonDeletedElements(elements),
|
||||||
includeBoundTextElement: true,
|
appState,
|
||||||
})[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,8 +41,6 @@ export const actionSelectAll = register({
|
|||||||
selectedElementIds,
|
selectedElementIds,
|
||||||
},
|
},
|
||||||
getNonDeletedElements(elements),
|
getNonDeletedElements(elements),
|
||||||
appState,
|
|
||||||
app,
|
|
||||||
),
|
),
|
||||||
commitToHistory: true,
|
commitToHistory: true,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -90,7 +90,6 @@ export class ActionManager {
|
|||||||
event,
|
event,
|
||||||
this.getAppState(),
|
this.getAppState(),
|
||||||
this.getElementsIncludingDeleted(),
|
this.getElementsIncludingDeleted(),
|
||||||
this.app,
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -169,7 +168,6 @@ 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,8 +82,7 @@ export type ActionName =
|
|||||||
| "zoomOut"
|
| "zoomOut"
|
||||||
| "resetZoom"
|
| "resetZoom"
|
||||||
| "zoomToFit"
|
| "zoomToFit"
|
||||||
| "zoomToFitSelection"
|
| "zoomToSelection"
|
||||||
| "zoomToFitSelectionInViewport"
|
|
||||||
| "changeFontFamily"
|
| "changeFontFamily"
|
||||||
| "changeTextAlign"
|
| "changeTextAlign"
|
||||||
| "changeVerticalAlign"
|
| "changeVerticalAlign"
|
||||||
@@ -119,7 +118,7 @@ export type ActionName =
|
|||||||
| "toggleHandTool"
|
| "toggleHandTool"
|
||||||
| "selectAllElementsInFrame"
|
| "selectAllElementsInFrame"
|
||||||
| "removeAllElementsFromFrame"
|
| "removeAllElementsFromFrame"
|
||||||
| "updateFrameRendering"
|
| "toggleFrameRendering"
|
||||||
| "setFrameAsActiveTool"
|
| "setFrameAsActiveTool"
|
||||||
| "createContainerFromText"
|
| "createContainerFromText"
|
||||||
| "wrapTextInContainer";
|
| "wrapTextInContainer";
|
||||||
@@ -130,7 +129,6 @@ 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 {
|
||||||
@@ -142,14 +140,12 @@ 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[],
|
||||||
|
|||||||
+14
-5
@@ -5,9 +5,6 @@ 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 });
|
||||||
|
|
||||||
@@ -15,8 +12,12 @@ export const trackEvent = (
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ALLOWED_CATEGORIES_TO_TRACK.includes(category)) {
|
if (process.env.REACT_APP_GOOGLE_ANALYTICS_ID && window.gtag) {
|
||||||
return;
|
window.gtag("event", action, {
|
||||||
|
event_category: category,
|
||||||
|
event_label: label,
|
||||||
|
value,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (window.sa_event) {
|
if (window.sa_event) {
|
||||||
@@ -26,6 +27,14 @@ 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: [],
|
||||||
frameRendering: { enabled: true, clip: true, name: true, outline: true },
|
shouldRenderFrames: 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 },
|
||||||
frameRendering: { browser: false, export: false, server: false },
|
shouldRenderFrames: { 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 getChartDimensions = (spreadsheet: Spreadsheet) => {
|
const getChartDimentions = (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 } = getChartDimensions(spreadsheet);
|
const { chartWidth, chartHeight } = getChartDimentions(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 } = getChartDimensions(spreadsheet);
|
const { chartWidth, chartHeight } = getChartDimentions(spreadsheet);
|
||||||
|
|
||||||
const title = spreadsheet.title
|
const title = spreadsheet.title
|
||||||
? newTextElement({
|
? newTextElement({
|
||||||
|
|||||||
+247
-360
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 { TranslationKeys, t } from "../../i18n";
|
import { t } from "../../i18n";
|
||||||
|
|
||||||
interface PickerColorListProps {
|
interface PickerColorListProps {
|
||||||
palette: ColorPaletteCustom;
|
palette: ColorPaletteCustom;
|
||||||
@@ -48,11 +48,7 @@ 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(
|
const label = t(`colors.${key.replace(/\d+/, "")}`, null, "");
|
||||||
`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, TranslationKeys } from "../i18n";
|
import { t } from "../i18n";
|
||||||
|
|
||||||
import "./ContextMenu.scss";
|
import "./ContextMenu.scss";
|
||||||
import {
|
import {
|
||||||
@@ -82,15 +82,9 @@ 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(
|
label = t(item.contextItemLabel(elements, appState));
|
||||||
item.contextItemLabel(
|
|
||||||
elements,
|
|
||||||
appState,
|
|
||||||
actionManager.app,
|
|
||||||
) as unknown as TranslationKeys,
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
label = t(item.contextItemLabel as unknown as TranslationKeys);
|
label = t(item.contextItemLabel);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,34 +17,16 @@ 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?: DialogSize;
|
size?: "small" | "regular" | "wide";
|
||||||
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);
|
||||||
@@ -103,7 +85,9 @@ export const Dialog = (props: DialogProps) => {
|
|||||||
<Modal
|
<Modal
|
||||||
className={clsx("Dialog", props.className)}
|
className={clsx("Dialog", props.className)}
|
||||||
labelledBy="dialog-title"
|
labelledBy="dialog-title"
|
||||||
maxWidth={getDialogSize(props.size)}
|
maxWidth={
|
||||||
|
props.size === "wide" ? 1024 : props.size === "small" ? 550 : 800
|
||||||
|
}
|
||||||
onCloseRequest={onClose}
|
onCloseRequest={onClose}
|
||||||
closeOnClickOutside={props.closeOnClickOutside}
|
closeOnClickOutside={props.closeOnClickOutside}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -2,140 +2,20 @@
|
|||||||
|
|
||||||
.excalidraw {
|
.excalidraw {
|
||||||
.ExcButton {
|
.ExcButton {
|
||||||
--text-color: transparent;
|
|
||||||
--border-color: transparent;
|
|
||||||
--back-color: transparent;
|
|
||||||
|
|
||||||
color: var(--text-color);
|
|
||||||
background-color: var(--back-color);
|
|
||||||
border-color: var(--border-color);
|
|
||||||
|
|
||||||
&--color-primary {
|
&--color-primary {
|
||||||
&.ExcButton--variant-filled {
|
color: var(--input-bg-color);
|
||||||
--text-color: var(--input-bg-color);
|
|
||||||
--back-color: var(--color-primary);
|
|
||||||
|
|
||||||
&:hover {
|
--accent-color: var(--color-primary);
|
||||||
--back-color: var(--color-primary-darker);
|
--accent-color-hover: var(--color-primary-darker);
|
||||||
}
|
--accent-color-active: var(--color-primary-darkest);
|
||||||
|
|
||||||
&: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 {
|
||||||
&.ExcButton--variant-filled {
|
color: var(--input-bg-color);
|
||||||
--text-color: var(--color-danger-text);
|
|
||||||
--back-color: var(--color-danger-dark);
|
|
||||||
|
|
||||||
&:hover {
|
--accent-color: var(--color-danger);
|
||||||
--back-color: var(--color-danger-darker);
|
--accent-color-hover: #d65550;
|
||||||
}
|
--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;
|
||||||
@@ -145,8 +25,6 @@
|
|||||||
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";
|
||||||
|
|
||||||
@@ -155,9 +33,9 @@
|
|||||||
transition: all 150ms ease-out;
|
transition: all 150ms ease-out;
|
||||||
|
|
||||||
&--size-large {
|
&--size-large {
|
||||||
font-weight: 600;
|
font-weight: 400;
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
min-height: 3rem;
|
height: 3rem;
|
||||||
padding: 0.5rem 1.5rem;
|
padding: 0.5rem 1.5rem;
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
|
|
||||||
@@ -167,22 +45,48 @@
|
|||||||
&--size-medium {
|
&--size-medium {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
min-height: 2.5rem;
|
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" | "warning" | "muted";
|
export type ButtonColor = "primary" | "danger";
|
||||||
export type ButtonSize = "medium" | "large";
|
export type ButtonSize = "medium" | "large";
|
||||||
|
|
||||||
export type FilledButtonProps = {
|
export type FilledButtonProps = {
|
||||||
@@ -17,7 +17,6 @@ export type FilledButtonProps = {
|
|||||||
color?: ButtonColor;
|
color?: ButtonColor;
|
||||||
size?: ButtonSize;
|
size?: ButtonSize;
|
||||||
className?: string;
|
className?: string;
|
||||||
fullWidth?: boolean;
|
|
||||||
|
|
||||||
startIcon?: React.ReactNode;
|
startIcon?: React.ReactNode;
|
||||||
};
|
};
|
||||||
@@ -32,7 +31,6 @@ export const FilledButton = forwardRef<HTMLButtonElement, FilledButtonProps>(
|
|||||||
variant = "filled",
|
variant = "filled",
|
||||||
color = "primary",
|
color = "primary",
|
||||||
size = "medium",
|
size = "medium",
|
||||||
fullWidth,
|
|
||||||
className,
|
className,
|
||||||
},
|
},
|
||||||
ref,
|
ref,
|
||||||
@@ -44,7 +42,6 @@ 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,5 +1,7 @@
|
|||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { AppClassProperties, Device, UIAppState } from "../types";
|
import { NonDeletedExcalidrawElement } from "../element/types";
|
||||||
|
import { getSelectedElements } from "../scene";
|
||||||
|
import { Device, UIAppState } from "../types";
|
||||||
import {
|
import {
|
||||||
isImageElement,
|
isImageElement,
|
||||||
isLinearElement,
|
isLinearElement,
|
||||||
@@ -13,12 +15,17 @@ 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 = ({ appState, isMobile, device, app }: HintViewerProps) => {
|
const getHints = ({
|
||||||
|
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;
|
||||||
|
|
||||||
@@ -48,7 +55,7 @@ const getHints = ({ appState, isMobile, device, app }: HintViewerProps) => {
|
|||||||
return t("hints.placeImage");
|
return t("hints.placeImage");
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectedElements = app.scene.getSelectedElements(appState);
|
const selectedElements = getSelectedElements(elements, appState);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
isResizing &&
|
isResizing &&
|
||||||
@@ -108,15 +115,15 @@ const getHints = ({ appState, isMobile, device, app }: HintViewerProps) => {
|
|||||||
|
|
||||||
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,7 +41,6 @@ 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";
|
||||||
@@ -72,7 +71,6 @@ 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<{
|
||||||
@@ -101,15 +99,6 @@ const DefaultMainMenu: React.FC<{
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const DefaultOverwriteConfirmDialog = () => {
|
|
||||||
return (
|
|
||||||
<OverwriteConfirmDialog __fallback>
|
|
||||||
<OverwriteConfirmDialog.Actions.SaveToDisk />
|
|
||||||
<OverwriteConfirmDialog.Actions.ExportToImage />
|
|
||||||
</OverwriteConfirmDialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const LayerUI = ({
|
const LayerUI = ({
|
||||||
actionManager,
|
actionManager,
|
||||||
appState,
|
appState,
|
||||||
@@ -128,7 +117,6 @@ const LayerUI = ({
|
|||||||
onExportImage,
|
onExportImage,
|
||||||
renderWelcomeScreen,
|
renderWelcomeScreen,
|
||||||
children,
|
children,
|
||||||
app,
|
|
||||||
}: LayerUIProps) => {
|
}: LayerUIProps) => {
|
||||||
const device = useDevice();
|
const device = useDevice();
|
||||||
const tunnels = useInitializeTunnels();
|
const tunnels = useInitializeTunnels();
|
||||||
@@ -242,9 +230,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}>
|
||||||
@@ -355,7 +343,6 @@ const LayerUI = ({
|
|||||||
>
|
>
|
||||||
{t("toolBar.library")}
|
{t("toolBar.library")}
|
||||||
</DefaultSidebar.Trigger>
|
</DefaultSidebar.Trigger>
|
||||||
<DefaultOverwriteConfirmDialog />
|
|
||||||
{/* ------------------------------------------------------------------ */}
|
{/* ------------------------------------------------------------------ */}
|
||||||
|
|
||||||
{appState.isLoading && <LoadingMessage delay={250} />}
|
{appState.isLoading && <LoadingMessage delay={250} />}
|
||||||
@@ -387,7 +374,6 @@ const LayerUI = ({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<ActiveConfirmDialog />
|
<ActiveConfirmDialog />
|
||||||
<tunnels.OverwriteConfirmDialogTunnel.Out />
|
|
||||||
{renderImageExportDialog()}
|
{renderImageExportDialog()}
|
||||||
{renderJSONExportDialog()}
|
{renderJSONExportDialog()}
|
||||||
{appState.pasteDialog.shown && (
|
{appState.pasteDialog.shown && (
|
||||||
@@ -401,9 +387,8 @@ const LayerUI = ({
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{device.isMobile && (
|
{device.isMobile && !eyeDropperState && (
|
||||||
<MobileMenu
|
<MobileMenu
|
||||||
app={app}
|
|
||||||
appState={appState}
|
appState={appState}
|
||||||
elements={elements}
|
elements={elements}
|
||||||
actionManager={actionManager}
|
actionManager={actionManager}
|
||||||
|
|||||||
@@ -1,11 +1,5 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import {
|
import { AppState, Device, ExcalidrawProps, UIAppState } from "../types";
|
||||||
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";
|
||||||
@@ -47,7 +41,6 @@ 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 = ({
|
||||||
@@ -65,7 +58,6 @@ export const MobileMenu = ({
|
|||||||
renderSidebars,
|
renderSidebars,
|
||||||
device,
|
device,
|
||||||
renderWelcomeScreen,
|
renderWelcomeScreen,
|
||||||
app,
|
|
||||||
}: MobileMenuProps) => {
|
}: MobileMenuProps) => {
|
||||||
const {
|
const {
|
||||||
WelcomeScreenCenterTunnel,
|
WelcomeScreenCenterTunnel,
|
||||||
@@ -127,9 +119,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: var(--zIndex-modal);
|
z-index: 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
.Modal {
|
.Modal {
|
||||||
|
|||||||
@@ -1,126 +0,0 @@
|
|||||||
@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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
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 };
|
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
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 };
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
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: "canvasActions" | "selectedShapeActions" | "shapes";
|
heading: string;
|
||||||
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 }) => {
|
||||||
|
|||||||
@@ -1,91 +0,0 @@
|
|||||||
@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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
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,10 +1,4 @@
|
|||||||
import {
|
import { forwardRef, useRef, useImperativeHandle, KeyboardEvent } from "react";
|
||||||
forwardRef,
|
|
||||||
useRef,
|
|
||||||
useImperativeHandle,
|
|
||||||
KeyboardEvent,
|
|
||||||
useLayoutEffect,
|
|
||||||
} from "react";
|
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
|
|
||||||
import "./TextField.scss";
|
import "./TextField.scss";
|
||||||
@@ -18,7 +12,6 @@ export type TextFieldProps = {
|
|||||||
|
|
||||||
readonly?: boolean;
|
readonly?: boolean;
|
||||||
fullWidth?: boolean;
|
fullWidth?: boolean;
|
||||||
selectOnRender?: boolean;
|
|
||||||
|
|
||||||
label?: string;
|
label?: string;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
@@ -26,28 +19,13 @@ 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: var(--zIndex-popup);
|
z-index: 1000;
|
||||||
|
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ 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", () => {
|
||||||
@@ -19,27 +18,24 @@ describe("Test <Trans/>", () => {
|
|||||||
const { getByTestId } = render(
|
const { getByTestId } = render(
|
||||||
<>
|
<>
|
||||||
<div data-testid="test1">
|
<div data-testid="test1">
|
||||||
<Trans
|
<Trans i18nKey="transTest.key1" audience="world" />
|
||||||
i18nKey={"transTest.key1" as unknown as TranslationKeys}
|
|
||||||
audience="world"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div data-testid="test2">
|
<div data-testid="test2">
|
||||||
<Trans
|
<Trans
|
||||||
i18nKey={"transTest.key2" as unknown as TranslationKeys}
|
i18nKey="transTest.key2"
|
||||||
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" as unknown as TranslationKeys}
|
i18nKey="transTest.key3"
|
||||||
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" as unknown as TranslationKeys}
|
i18nKey="transTest.key4"
|
||||||
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>}
|
||||||
@@ -47,7 +43,7 @@ describe("Test <Trans/>", () => {
|
|||||||
</div>
|
</div>
|
||||||
<div data-testid="test5">
|
<div data-testid="test5">
|
||||||
<Trans
|
<Trans
|
||||||
i18nKey={"transTest.key5" as unknown as TranslationKeys}
|
i18nKey="transTest.key5"
|
||||||
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 { TranslationKeys, useI18n } from "../i18n";
|
import { 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: TranslationKeys;
|
i18nKey: string;
|
||||||
[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,16 +1608,6 @@ 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,10 +1,6 @@
|
|||||||
import { getShortcutFromShortcutName } from "../../actions/shortcuts";
|
import { getShortcutFromShortcutName } from "../../actions/shortcuts";
|
||||||
import { useI18n } from "../../i18n";
|
import { useI18n } from "../../i18n";
|
||||||
import {
|
import { useExcalidrawSetAppState, useExcalidrawActionManager } from "../App";
|
||||||
useExcalidrawSetAppState,
|
|
||||||
useExcalidrawActionManager,
|
|
||||||
useExcalidrawElements,
|
|
||||||
} from "../App";
|
|
||||||
import {
|
import {
|
||||||
ExportIcon,
|
ExportIcon,
|
||||||
ExportImageIcon,
|
ExportImageIcon,
|
||||||
@@ -33,42 +29,19 @@ 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={handleSelect}
|
onSelect={() => actionManager.executeAction(actionLoadScene)}
|
||||||
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,7 +12,6 @@ type TunnelsContextValue = {
|
|||||||
FooterCenterTunnel: Tunnel;
|
FooterCenterTunnel: Tunnel;
|
||||||
DefaultSidebarTriggerTunnel: Tunnel;
|
DefaultSidebarTriggerTunnel: Tunnel;
|
||||||
DefaultSidebarTabTriggersTunnel: Tunnel;
|
DefaultSidebarTabTriggersTunnel: Tunnel;
|
||||||
OverwriteConfirmDialogTunnel: Tunnel;
|
|
||||||
jotaiScope: symbol;
|
jotaiScope: symbol;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -31,7 +30,6 @@ export const useInitializeTunnels = () => {
|
|||||||
FooterCenterTunnel: tunnel(),
|
FooterCenterTunnel: tunnel(),
|
||||||
DefaultSidebarTriggerTunnel: tunnel(),
|
DefaultSidebarTriggerTunnel: tunnel(),
|
||||||
DefaultSidebarTabTriggersTunnel: tunnel(),
|
DefaultSidebarTabTriggersTunnel: tunnel(),
|
||||||
OverwriteConfirmDialogTunnel: tunnel(),
|
|
||||||
jotaiScope: Symbol(),
|
jotaiScope: Symbol(),
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|||||||
@@ -5,15 +5,6 @@
|
|||||||
--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 {
|
||||||
|
|||||||
+4
-45
@@ -27,6 +27,10 @@
|
|||||||
--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),
|
||||||
@@ -95,33 +99,9 @@
|
|||||||
--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;
|
||||||
@@ -197,27 +177,6 @@
|
|||||||
--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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-24
@@ -41,7 +41,6 @@ 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,
|
||||||
@@ -143,7 +142,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 ? normalizeLink(element.link) : null,
|
link: element.link ?? null,
|
||||||
locked: element.locked ?? false,
|
locked: element.locked ?? false,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -371,24 +370,6 @@ 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 */
|
||||||
@@ -429,10 +410,6 @@ 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) {
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
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>");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
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,7 +29,6 @@ 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";
|
||||||
@@ -167,7 +166,7 @@ export const Hyperlink = ({
|
|||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<a
|
<a
|
||||||
href={normalizeLink(element.link || "")}
|
href={element.link || ""}
|
||||||
className={clsx("excalidraw-hyperlinkContainer-link", {
|
className={clsx("excalidraw-hyperlinkContainer-link", {
|
||||||
"d-none": isEditing,
|
"d-none": isEditing,
|
||||||
})}
|
})}
|
||||||
@@ -178,13 +177,7 @@ export const Hyperlink = ({
|
|||||||
EVENT.EXCALIDRAW_LINK,
|
EVENT.EXCALIDRAW_LINK,
|
||||||
event.nativeEvent,
|
event.nativeEvent,
|
||||||
);
|
);
|
||||||
onLinkOpen(
|
onLinkOpen(element, customEvent);
|
||||||
{
|
|
||||||
...element,
|
|
||||||
link: normalizeLink(element.link),
|
|
||||||
},
|
|
||||||
customEvent,
|
|
||||||
);
|
|
||||||
if (customEvent.defaultPrevented) {
|
if (customEvent.defaultPrevented) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}
|
}
|
||||||
@@ -238,6 +231,21 @@ 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,6 +862,7 @@ const VALID_CONTAINER_TYPES = new Set([
|
|||||||
"rectangle",
|
"rectangle",
|
||||||
"ellipse",
|
"ellipse",
|
||||||
"diamond",
|
"diamond",
|
||||||
|
"image",
|
||||||
"arrow",
|
"arrow",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@@ -1459,54 +1459,5 @@ 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,6 +30,15 @@ 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", () => {
|
||||||
@@ -53,14 +62,5 @@ describe("Test TypeChecks", () => {
|
|||||||
),
|
),
|
||||||
).toBeFalsy();
|
).toBeFalsy();
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(
|
|
||||||
hasBoundTextElement(
|
|
||||||
API.createElement({
|
|
||||||
type: "image",
|
|
||||||
boundElements: [{ type: "text", id: "text-id" }],
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
).toBeFalsy();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -126,6 +126,7 @@ 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,6 +162,7 @@ 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";
|
||||||
|
|
||||||
export const exportToExcalidrawPlus = async (
|
const exportToExcalidrawPlus = async (
|
||||||
elements: readonly NonDeletedExcalidrawElement[],
|
elements: readonly NonDeletedExcalidrawElement[],
|
||||||
appState: Partial<AppState>,
|
appState: Partial<AppState>,
|
||||||
files: BinaryFiles,
|
files: BinaryFiles,
|
||||||
|
|||||||
@@ -282,15 +282,11 @@ 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(
|
||||||
@@ -331,18 +327,14 @@ export const exportToBackend = async (
|
|||||||
files: filesToUpload,
|
files: filesToUpload,
|
||||||
});
|
});
|
||||||
|
|
||||||
return { url: urlString, errorMessage: null };
|
window.prompt(`🔒${t("alerts.uploadedSecurly")}`, urlString);
|
||||||
} else if (json.error_class === "RequestTooLargeError") {
|
} else if (json.error_class === "RequestTooLargeError") {
|
||||||
return {
|
window.alert(t("alerts.couldNotCreateShareableLinkTooBig"));
|
||||||
url: null,
|
} else {
|
||||||
errorMessage: t("alerts.couldNotCreateShareableLinkTooBig"),
|
window.alert(t("alerts.couldNotCreateShareableLink"));
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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") };
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,135 +0,0 @@
|
|||||||
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,10 +69,7 @@ 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 {
|
import { ExportToExcalidrawPlus } from "./components/ExportToExcalidrawPlus";
|
||||||
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";
|
||||||
@@ -91,10 +88,6 @@ 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();
|
||||||
|
|
||||||
@@ -105,19 +98,6 @@ 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;
|
||||||
@@ -149,7 +129,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
|
||||||
(await openConfirmModal(shareableLinkConfirmDialog))
|
window.confirm(t("alerts.loadSceneOverridePrompt"))
|
||||||
) {
|
) {
|
||||||
if (jsonBackendMatch) {
|
if (jsonBackendMatch) {
|
||||||
scene = await loadScene(
|
scene = await loadScene(
|
||||||
@@ -188,7 +168,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 ||
|
||||||
(await openConfirmModal(shareableLinkConfirmDialog))
|
window.confirm(t("alerts.loadSceneOverridePrompt"))
|
||||||
) {
|
) {
|
||||||
return { scene: data, isExternalScene };
|
return { scene: data, isExternalScene };
|
||||||
}
|
}
|
||||||
@@ -574,10 +554,6 @@ 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>,
|
||||||
@@ -589,7 +565,7 @@ const ExcalidrawWrapper = () => {
|
|||||||
}
|
}
|
||||||
if (canvas) {
|
if (canvas) {
|
||||||
try {
|
try {
|
||||||
const { url, errorMessage } = await exportToBackend(
|
await exportToBackend(
|
||||||
exportedElements,
|
exportedElements,
|
||||||
{
|
{
|
||||||
...appState,
|
...appState,
|
||||||
@@ -599,14 +575,6 @@ 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;
|
||||||
@@ -706,47 +674,21 @@ 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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
+4
-12
@@ -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 { AppClassProperties, AppState } from "./types";
|
import { 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: readonly ExcalidrawElement[]) => {
|
export const groupByFrames = (elements: ExcalidrawElementsIncludingDeleted) => {
|
||||||
const frameElementsMap = new Map<
|
const frameElementsMap = new Map<
|
||||||
ExcalidrawElement["id"],
|
ExcalidrawElement["id"],
|
||||||
ExcalidrawElement[]
|
ExcalidrawElement[]
|
||||||
@@ -571,13 +571,8 @@ export const replaceAllElementsInFrame = (
|
|||||||
export const updateFrameMembershipOfSelectedElements = (
|
export const updateFrameMembershipOfSelectedElements = (
|
||||||
allElements: ExcalidrawElementsIncludingDeleted,
|
allElements: ExcalidrawElementsIncludingDeleted,
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
app: AppClassProperties,
|
|
||||||
) => {
|
) => {
|
||||||
const selectedElements = app.scene.getSelectedElements({
|
const selectedElements = getSelectedElements(allElements, appState);
|
||||||
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) {
|
||||||
@@ -596,7 +591,6 @@ 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)
|
||||||
) {
|
) {
|
||||||
@@ -604,9 +598,7 @@ export const updateFrameMembershipOfSelectedElements = (
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return elementsToRemove.size > 0
|
return removeElementsFromFrame(allElements, [...elementsToRemove], appState);
|
||||||
? removeElementsFromFrame(allElements, [...elementsToRemove], appState)
|
|
||||||
: allElements;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+5
-35
@@ -1,13 +1,7 @@
|
|||||||
import {
|
import { GroupId, ExcalidrawElement, NonDeleted } from "./element/types";
|
||||||
GroupId,
|
import { AppState } from "./types";
|
||||||
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,
|
||||||
@@ -72,33 +66,14 @@ export const getSelectedGroupIds = (appState: AppState): GroupId[] =>
|
|||||||
*/
|
*/
|
||||||
export const selectGroupsForSelectedElements = (
|
export const selectGroupsForSelectedElements = (
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
elements: readonly NonDeletedExcalidrawElement[],
|
elements: readonly NonDeleted<ExcalidrawElement>[],
|
||||||
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 = app
|
const selectedElements = getSelectedElements(elements, appState);
|
||||||
? 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 {
|
return { ...nextAppState, editingGroupId: null };
|
||||||
...nextAppState,
|
|
||||||
editingGroupId: null,
|
|
||||||
selectedElementIds: makeNextSelectedElementIds(
|
|
||||||
nextAppState.selectedElementIds,
|
|
||||||
prevAppState,
|
|
||||||
),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const selectedElement of selectedElements) {
|
for (const selectedElement of selectedElements) {
|
||||||
@@ -116,11 +91,6 @@ export const selectGroupsForSelectedElements = (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
nextAppState.selectedElementIds = makeNextSelectedElementIds(
|
|
||||||
nextAppState.selectedElementIds,
|
|
||||||
prevAppState,
|
|
||||||
);
|
|
||||||
|
|
||||||
return nextAppState;
|
return nextAppState;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+1
-4
@@ -3,7 +3,6 @@ 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;
|
||||||
|
|
||||||
@@ -13,8 +12,6 @@ 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[] = [
|
||||||
@@ -126,7 +123,7 @@ const findPartsForData = (data: any, parts: string[]) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const t = (
|
export const t = (
|
||||||
path: NestedKeyOf<typeof fallbackLangData>,
|
path: string,
|
||||||
replacement?: { [key: string]: string | number } | null,
|
replacement?: { [key: string]: string | number } | null,
|
||||||
fallback?: string,
|
fallback?: string,
|
||||||
) => {
|
) => {
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ 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,8 +124,6 @@
|
|||||||
},
|
},
|
||||||
"statusPublished": "نُشر",
|
"statusPublished": "نُشر",
|
||||||
"sidebarLock": "إبقاء الشريط الجانبي مفتوح",
|
"sidebarLock": "إبقاء الشريط الجانبي مفتوح",
|
||||||
"selectAllElementsInFrame": "",
|
|
||||||
"removeAllElementsFromFrame": "",
|
|
||||||
"eyeDropper": ""
|
"eyeDropper": ""
|
||||||
},
|
},
|
||||||
"library": {
|
"library": {
|
||||||
@@ -223,9 +221,7 @@
|
|||||||
"penMode": "وضع القلم - امنع اللمس",
|
"penMode": "وضع القلم - امنع اللمس",
|
||||||
"link": "إضافة/تحديث الرابط للشكل المحدد",
|
"link": "إضافة/تحديث الرابط للشكل المحدد",
|
||||||
"eraser": "ممحاة",
|
"eraser": "ممحاة",
|
||||||
"frame": "",
|
"hand": ""
|
||||||
"hand": "",
|
|
||||||
"extraTools": ""
|
|
||||||
},
|
},
|
||||||
"headings": {
|
"headings": {
|
||||||
"canvasActions": "إجراءات اللوحة",
|
"canvasActions": "إجراءات اللوحة",
|
||||||
|
|||||||
@@ -124,8 +124,6 @@
|
|||||||
},
|
},
|
||||||
"statusPublished": "",
|
"statusPublished": "",
|
||||||
"sidebarLock": "",
|
"sidebarLock": "",
|
||||||
"selectAllElementsInFrame": "",
|
|
||||||
"removeAllElementsFromFrame": "",
|
|
||||||
"eyeDropper": ""
|
"eyeDropper": ""
|
||||||
},
|
},
|
||||||
"library": {
|
"library": {
|
||||||
@@ -223,9 +221,7 @@
|
|||||||
"penMode": "",
|
"penMode": "",
|
||||||
"link": "",
|
"link": "",
|
||||||
"eraser": "",
|
"eraser": "",
|
||||||
"frame": "",
|
"hand": ""
|
||||||
"hand": "",
|
|
||||||
"extraTools": ""
|
|
||||||
},
|
},
|
||||||
"headings": {
|
"headings": {
|
||||||
"canvasActions": "",
|
"canvasActions": "",
|
||||||
|
|||||||
@@ -124,8 +124,6 @@
|
|||||||
},
|
},
|
||||||
"statusPublished": "",
|
"statusPublished": "",
|
||||||
"sidebarLock": "",
|
"sidebarLock": "",
|
||||||
"selectAllElementsInFrame": "",
|
|
||||||
"removeAllElementsFromFrame": "",
|
|
||||||
"eyeDropper": ""
|
"eyeDropper": ""
|
||||||
},
|
},
|
||||||
"library": {
|
"library": {
|
||||||
@@ -223,9 +221,7 @@
|
|||||||
"penMode": "",
|
"penMode": "",
|
||||||
"link": "",
|
"link": "",
|
||||||
"eraser": "",
|
"eraser": "",
|
||||||
"frame": "",
|
"hand": ""
|
||||||
"hand": "",
|
|
||||||
"extraTools": ""
|
|
||||||
},
|
},
|
||||||
"headings": {
|
"headings": {
|
||||||
"canvasActions": "Действия по платното",
|
"canvasActions": "Действия по платното",
|
||||||
|
|||||||
@@ -124,8 +124,6 @@
|
|||||||
},
|
},
|
||||||
"statusPublished": "প্রকাশিত",
|
"statusPublished": "প্রকাশিত",
|
||||||
"sidebarLock": "লক",
|
"sidebarLock": "লক",
|
||||||
"selectAllElementsInFrame": "",
|
|
||||||
"removeAllElementsFromFrame": "",
|
|
||||||
"eyeDropper": ""
|
"eyeDropper": ""
|
||||||
},
|
},
|
||||||
"library": {
|
"library": {
|
||||||
@@ -223,9 +221,7 @@
|
|||||||
"penMode": "",
|
"penMode": "",
|
||||||
"link": "একটি নির্বাচিত আকৃতির জন্য লিঙ্ক যোগ বা আপডেট করুন",
|
"link": "একটি নির্বাচিত আকৃতির জন্য লিঙ্ক যোগ বা আপডেট করুন",
|
||||||
"eraser": "ঝাড়ন",
|
"eraser": "ঝাড়ন",
|
||||||
"frame": "",
|
"hand": ""
|
||||||
"hand": "",
|
|
||||||
"extraTools": ""
|
|
||||||
},
|
},
|
||||||
"headings": {
|
"headings": {
|
||||||
"canvasActions": "ক্যানভাস কার্যকলাপ",
|
"canvasActions": "ক্যানভাস কার্যকলাপ",
|
||||||
|
|||||||
@@ -124,8 +124,6 @@
|
|||||||
},
|
},
|
||||||
"statusPublished": "Publicat",
|
"statusPublished": "Publicat",
|
||||||
"sidebarLock": "Manté la barra lateral oberta",
|
"sidebarLock": "Manté la barra lateral oberta",
|
||||||
"selectAllElementsInFrame": "",
|
|
||||||
"removeAllElementsFromFrame": "",
|
|
||||||
"eyeDropper": ""
|
"eyeDropper": ""
|
||||||
},
|
},
|
||||||
"library": {
|
"library": {
|
||||||
@@ -223,9 +221,7 @@
|
|||||||
"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",
|
||||||
"frame": "",
|
"hand": "Mà (eina de desplaçament)"
|
||||||
"hand": "Mà (eina de desplaçament)",
|
|
||||||
"extraTools": ""
|
|
||||||
},
|
},
|
||||||
"headings": {
|
"headings": {
|
||||||
"canvasActions": "Accions del llenç",
|
"canvasActions": "Accions del llenç",
|
||||||
|
|||||||
@@ -124,8 +124,6 @@
|
|||||||
},
|
},
|
||||||
"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": {
|
||||||
@@ -223,9 +221,7 @@
|
|||||||
"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",
|
||||||
"frame": "",
|
"hand": "Ruka (nástroj pro posouvání)"
|
||||||
"hand": "Ruka (nástroj pro posouvání)",
|
|
||||||
"extraTools": ""
|
|
||||||
},
|
},
|
||||||
"headings": {
|
"headings": {
|
||||||
"canvasActions": "Akce plátna",
|
"canvasActions": "Akce plátna",
|
||||||
|
|||||||
@@ -124,8 +124,6 @@
|
|||||||
},
|
},
|
||||||
"statusPublished": "",
|
"statusPublished": "",
|
||||||
"sidebarLock": "",
|
"sidebarLock": "",
|
||||||
"selectAllElementsInFrame": "",
|
|
||||||
"removeAllElementsFromFrame": "",
|
|
||||||
"eyeDropper": ""
|
"eyeDropper": ""
|
||||||
},
|
},
|
||||||
"library": {
|
"library": {
|
||||||
@@ -223,9 +221,7 @@
|
|||||||
"penMode": "",
|
"penMode": "",
|
||||||
"link": "",
|
"link": "",
|
||||||
"eraser": "",
|
"eraser": "",
|
||||||
"frame": "",
|
"hand": ""
|
||||||
"hand": "",
|
|
||||||
"extraTools": ""
|
|
||||||
},
|
},
|
||||||
"headings": {
|
"headings": {
|
||||||
"canvasActions": "",
|
"canvasActions": "",
|
||||||
|
|||||||
@@ -124,8 +124,6 @@
|
|||||||
},
|
},
|
||||||
"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": {
|
||||||
@@ -223,9 +221,7 @@
|
|||||||
"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",
|
||||||
"frame": "Rahmenwerkzeug",
|
"hand": "Hand (Schwenkwerkzeug)"
|
||||||
"hand": "Hand (Schwenkwerkzeug)",
|
|
||||||
"extraTools": "Weitere Werkzeuge"
|
|
||||||
},
|
},
|
||||||
"headings": {
|
"headings": {
|
||||||
"canvasActions": "Aktionen für Zeichenfläche",
|
"canvasActions": "Aktionen für Zeichenfläche",
|
||||||
|
|||||||
@@ -124,8 +124,6 @@
|
|||||||
},
|
},
|
||||||
"statusPublished": "Δημοσιευμένο",
|
"statusPublished": "Δημοσιευμένο",
|
||||||
"sidebarLock": "Κρατήστε την πλαϊνή μπάρα ανοιχτή",
|
"sidebarLock": "Κρατήστε την πλαϊνή μπάρα ανοιχτή",
|
||||||
"selectAllElementsInFrame": "",
|
|
||||||
"removeAllElementsFromFrame": "",
|
|
||||||
"eyeDropper": ""
|
"eyeDropper": ""
|
||||||
},
|
},
|
||||||
"library": {
|
"library": {
|
||||||
@@ -223,9 +221,7 @@
|
|||||||
"penMode": "Λειτουργία μολυβιού - αποτροπή αφής",
|
"penMode": "Λειτουργία μολυβιού - αποτροπή αφής",
|
||||||
"link": "Προσθήκη/ Ενημέρωση συνδέσμου για ένα επιλεγμένο σχήμα",
|
"link": "Προσθήκη/ Ενημέρωση συνδέσμου για ένα επιλεγμένο σχήμα",
|
||||||
"eraser": "Γόμα",
|
"eraser": "Γόμα",
|
||||||
"frame": "",
|
"hand": ""
|
||||||
"hand": "",
|
|
||||||
"extraTools": ""
|
|
||||||
},
|
},
|
||||||
"headings": {
|
"headings": {
|
||||||
"canvasActions": "Ενέργειες καμβά",
|
"canvasActions": "Ενέργειες καμβά",
|
||||||
|
|||||||
@@ -449,36 +449,5 @@
|
|||||||
"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,8 +124,6 @@
|
|||||||
},
|
},
|
||||||
"statusPublished": "Publicado",
|
"statusPublished": "Publicado",
|
||||||
"sidebarLock": "Mantener barra lateral abierta",
|
"sidebarLock": "Mantener barra lateral abierta",
|
||||||
"selectAllElementsInFrame": "",
|
|
||||||
"removeAllElementsFromFrame": "",
|
|
||||||
"eyeDropper": ""
|
"eyeDropper": ""
|
||||||
},
|
},
|
||||||
"library": {
|
"library": {
|
||||||
@@ -223,9 +221,7 @@
|
|||||||
"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",
|
||||||
"frame": "",
|
"hand": "Mano (herramienta de panoramización)"
|
||||||
"hand": "Mano (herramienta de panoramización)",
|
|
||||||
"extraTools": ""
|
|
||||||
},
|
},
|
||||||
"headings": {
|
"headings": {
|
||||||
"canvasActions": "Acciones del lienzo",
|
"canvasActions": "Acciones del lienzo",
|
||||||
|
|||||||
+35
-39
@@ -124,9 +124,7 @@
|
|||||||
},
|
},
|
||||||
"statusPublished": "Argitaratua",
|
"statusPublished": "Argitaratua",
|
||||||
"sidebarLock": "Mantendu alboko barra irekita",
|
"sidebarLock": "Mantendu alboko barra irekita",
|
||||||
"selectAllElementsInFrame": "Hautatu markoko elementu guztiak",
|
"eyeDropper": ""
|
||||||
"removeAllElementsFromFrame": "Kendu markoko elementu guztiak",
|
|
||||||
"eyeDropper": "Aukeratu kolorea oihaletik"
|
|
||||||
},
|
},
|
||||||
"library": {
|
"library": {
|
||||||
"noItems": "Oraindik ez da elementurik gehitu...",
|
"noItems": "Oraindik ez da elementurik gehitu...",
|
||||||
@@ -223,9 +221,7 @@
|
|||||||
"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",
|
||||||
"frame": "Marko tresna",
|
"hand": "Eskua (panoratze tresna)"
|
||||||
"hand": "Eskua (panoratze tresna)",
|
|
||||||
"extraTools": "Tresna gehiago"
|
|
||||||
},
|
},
|
||||||
"headings": {
|
"headings": {
|
||||||
"canvasActions": "Canvas ekintzak",
|
"canvasActions": "Canvas ekintzak",
|
||||||
@@ -360,27 +356,27 @@
|
|||||||
"removeItemsFromLib": "Kendu hautatutako elementuak liburutegitik"
|
"removeItemsFromLib": "Kendu hautatutako elementuak liburutegitik"
|
||||||
},
|
},
|
||||||
"imageExportDialog": {
|
"imageExportDialog": {
|
||||||
"header": "Esportatu irudia",
|
"header": "",
|
||||||
"label": {
|
"label": {
|
||||||
"withBackground": "Atzeko planoa",
|
"withBackground": "",
|
||||||
"onlySelected": "Hautapena soilik",
|
"onlySelected": "",
|
||||||
"darkMode": "Modu iluna",
|
"darkMode": "",
|
||||||
"embedScene": "Txertatu eszena",
|
"embedScene": "",
|
||||||
"scale": "Eskala",
|
"scale": "",
|
||||||
"padding": "Betegarria"
|
"padding": ""
|
||||||
},
|
},
|
||||||
"tooltip": {
|
"tooltip": {
|
||||||
"embedScene": "Eszenaren datuak esportatutako PNG/SVG fitxategian gordeko dira, eszena bertatik berrezartzeko.\nEsportatutako fitxategien tamaina handituko da."
|
"embedScene": ""
|
||||||
},
|
},
|
||||||
"title": {
|
"title": {
|
||||||
"exportToPng": "Esportatu PNG gisa",
|
"exportToPng": "",
|
||||||
"exportToSvg": "Esportatu SVG gisa",
|
"exportToSvg": "",
|
||||||
"copyPngToClipboard": "Kopiatu PNG arbelera"
|
"copyPngToClipboard": ""
|
||||||
},
|
},
|
||||||
"button": {
|
"button": {
|
||||||
"exportToPng": "PNG",
|
"exportToPng": "",
|
||||||
"exportToSvg": "SVG",
|
"exportToSvg": "",
|
||||||
"copyPngToClipboard": "Kopiatu arbelean"
|
"copyPngToClipboard": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"encrypted": {
|
"encrypted": {
|
||||||
@@ -415,20 +411,20 @@
|
|||||||
},
|
},
|
||||||
"colors": {
|
"colors": {
|
||||||
"transparent": "Gardena",
|
"transparent": "Gardena",
|
||||||
"black": "Beltza",
|
"black": "",
|
||||||
"white": "Zuria",
|
"white": "",
|
||||||
"red": "Gorria",
|
"red": "",
|
||||||
"pink": "Arrosa",
|
"pink": "",
|
||||||
"grape": "Mahats kolorea",
|
"grape": "",
|
||||||
"violet": "Bioleta",
|
"violet": "",
|
||||||
"gray": "Grisa",
|
"gray": "",
|
||||||
"blue": "Urdina",
|
"blue": "",
|
||||||
"cyan": "Ziana",
|
"cyan": "",
|
||||||
"teal": "Berde urdinxka",
|
"teal": "",
|
||||||
"green": "Berdea",
|
"green": "",
|
||||||
"yellow": "Horia",
|
"yellow": "",
|
||||||
"orange": "Laranja",
|
"orange": "",
|
||||||
"bronze": "Brontzea"
|
"bronze": ""
|
||||||
},
|
},
|
||||||
"welcomeScreen": {
|
"welcomeScreen": {
|
||||||
"app": {
|
"app": {
|
||||||
@@ -444,10 +440,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"colorPicker": {
|
"colorPicker": {
|
||||||
"mostUsedCustomColors": "Gehien erabilitako kolore pertsonalizatuak",
|
"mostUsedCustomColors": "",
|
||||||
"colors": "Koloreak",
|
"colors": "",
|
||||||
"shades": "Ñabardurak",
|
"shades": "",
|
||||||
"hexCode": "Hez kodea",
|
"hexCode": "",
|
||||||
"noShades": "Kolore honetarako ez dago ñabardurarik eskuragarri"
|
"noShades": ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -124,8 +124,6 @@
|
|||||||
},
|
},
|
||||||
"statusPublished": "منتشر شده",
|
"statusPublished": "منتشر شده",
|
||||||
"sidebarLock": "باز نگه داشتن سایدبار",
|
"sidebarLock": "باز نگه داشتن سایدبار",
|
||||||
"selectAllElementsInFrame": "",
|
|
||||||
"removeAllElementsFromFrame": "",
|
|
||||||
"eyeDropper": ""
|
"eyeDropper": ""
|
||||||
},
|
},
|
||||||
"library": {
|
"library": {
|
||||||
@@ -223,9 +221,7 @@
|
|||||||
"penMode": "حالت قلم - جلوگیری از تماس",
|
"penMode": "حالت قلم - جلوگیری از تماس",
|
||||||
"link": "افزودن/بهروزرسانی پیوند برای شکل انتخابی",
|
"link": "افزودن/بهروزرسانی پیوند برای شکل انتخابی",
|
||||||
"eraser": "پاک کن",
|
"eraser": "پاک کن",
|
||||||
"frame": "",
|
"hand": "دست (ابزار پانینگ)"
|
||||||
"hand": "دست (ابزار پانینگ)",
|
|
||||||
"extraTools": ""
|
|
||||||
},
|
},
|
||||||
"headings": {
|
"headings": {
|
||||||
"canvasActions": "عملیات روی بوم",
|
"canvasActions": "عملیات روی بوم",
|
||||||
|
|||||||
@@ -124,8 +124,6 @@
|
|||||||
},
|
},
|
||||||
"statusPublished": "Julkaistu",
|
"statusPublished": "Julkaistu",
|
||||||
"sidebarLock": "Pidä sivupalkki avoinna",
|
"sidebarLock": "Pidä sivupalkki avoinna",
|
||||||
"selectAllElementsInFrame": "",
|
|
||||||
"removeAllElementsFromFrame": "",
|
|
||||||
"eyeDropper": ""
|
"eyeDropper": ""
|
||||||
},
|
},
|
||||||
"library": {
|
"library": {
|
||||||
@@ -223,9 +221,7 @@
|
|||||||
"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",
|
||||||
"frame": "",
|
"hand": "Käsi (panning-työkalu)"
|
||||||
"hand": "Käsi (panning-työkalu)",
|
|
||||||
"extraTools": ""
|
|
||||||
},
|
},
|
||||||
"headings": {
|
"headings": {
|
||||||
"canvasActions": "Piirtoalueen toiminnot",
|
"canvasActions": "Piirtoalueen toiminnot",
|
||||||
|
|||||||
@@ -124,8 +124,6 @@
|
|||||||
},
|
},
|
||||||
"statusPublished": "Publié",
|
"statusPublished": "Publié",
|
||||||
"sidebarLock": "Maintenir la barre latérale ouverte",
|
"sidebarLock": "Maintenir la barre latérale ouverte",
|
||||||
"selectAllElementsInFrame": "",
|
|
||||||
"removeAllElementsFromFrame": "",
|
|
||||||
"eyeDropper": ""
|
"eyeDropper": ""
|
||||||
},
|
},
|
||||||
"library": {
|
"library": {
|
||||||
@@ -223,9 +221,7 @@
|
|||||||
"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",
|
||||||
"frame": "",
|
"hand": "Mains (outil de déplacement de la vue)"
|
||||||
"hand": "Mains (outil de déplacement de la vue)",
|
|
||||||
"extraTools": ""
|
|
||||||
},
|
},
|
||||||
"headings": {
|
"headings": {
|
||||||
"canvasActions": "Actions du canevas",
|
"canvasActions": "Actions du canevas",
|
||||||
|
|||||||
@@ -124,8 +124,6 @@
|
|||||||
},
|
},
|
||||||
"statusPublished": "Publicado",
|
"statusPublished": "Publicado",
|
||||||
"sidebarLock": "Manter a barra lateral aberta",
|
"sidebarLock": "Manter a barra lateral aberta",
|
||||||
"selectAllElementsInFrame": "",
|
|
||||||
"removeAllElementsFromFrame": "",
|
|
||||||
"eyeDropper": ""
|
"eyeDropper": ""
|
||||||
},
|
},
|
||||||
"library": {
|
"library": {
|
||||||
@@ -223,9 +221,7 @@
|
|||||||
"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",
|
||||||
"frame": "",
|
"hand": "Man (ferramenta de desprazamento)"
|
||||||
"hand": "Man (ferramenta de desprazamento)",
|
|
||||||
"extraTools": ""
|
|
||||||
},
|
},
|
||||||
"headings": {
|
"headings": {
|
||||||
"canvasActions": "Accións do lenzo",
|
"canvasActions": "Accións do lenzo",
|
||||||
|
|||||||
@@ -124,8 +124,6 @@
|
|||||||
},
|
},
|
||||||
"statusPublished": "פורסם",
|
"statusPublished": "פורסם",
|
||||||
"sidebarLock": "שמור את סרגל הצד פתוח",
|
"sidebarLock": "שמור את סרגל הצד פתוח",
|
||||||
"selectAllElementsInFrame": "",
|
|
||||||
"removeAllElementsFromFrame": "",
|
|
||||||
"eyeDropper": ""
|
"eyeDropper": ""
|
||||||
},
|
},
|
||||||
"library": {
|
"library": {
|
||||||
@@ -223,9 +221,7 @@
|
|||||||
"penMode": "מצב עט - מנע נגיעה",
|
"penMode": "מצב עט - מנע נגיעה",
|
||||||
"link": "הוספה/עדכון קישור של הצורה שנבחרה",
|
"link": "הוספה/עדכון קישור של הצורה שנבחרה",
|
||||||
"eraser": "מחק",
|
"eraser": "מחק",
|
||||||
"frame": "",
|
"hand": "יד (כלי הזזה)"
|
||||||
"hand": "יד (כלי הזזה)",
|
|
||||||
"extraTools": ""
|
|
||||||
},
|
},
|
||||||
"headings": {
|
"headings": {
|
||||||
"canvasActions": "פעולות קנבאס",
|
"canvasActions": "פעולות קנבאס",
|
||||||
|
|||||||
@@ -124,8 +124,6 @@
|
|||||||
},
|
},
|
||||||
"statusPublished": "प्रकाशित",
|
"statusPublished": "प्रकाशित",
|
||||||
"sidebarLock": "साइडबार खुला रखे.",
|
"sidebarLock": "साइडबार खुला रखे.",
|
||||||
"selectAllElementsInFrame": "",
|
|
||||||
"removeAllElementsFromFrame": "",
|
|
||||||
"eyeDropper": "चित्रफलक से रंग चुने"
|
"eyeDropper": "चित्रफलक से रंग चुने"
|
||||||
},
|
},
|
||||||
"library": {
|
"library": {
|
||||||
@@ -223,9 +221,7 @@
|
|||||||
"penMode": "पेन का मोड - स्पर्श टाले",
|
"penMode": "पेन का मोड - स्पर्श टाले",
|
||||||
"link": "",
|
"link": "",
|
||||||
"eraser": "रबड़",
|
"eraser": "रबड़",
|
||||||
"frame": "",
|
"hand": "हाथ ( खिसकाने का औज़ार)"
|
||||||
"hand": "हाथ ( खिसकाने का औज़ार)",
|
|
||||||
"extraTools": ""
|
|
||||||
},
|
},
|
||||||
"headings": {
|
"headings": {
|
||||||
"canvasActions": "कैनवास क्रिया",
|
"canvasActions": "कैनवास क्रिया",
|
||||||
|
|||||||
@@ -124,8 +124,6 @@
|
|||||||
},
|
},
|
||||||
"statusPublished": "",
|
"statusPublished": "",
|
||||||
"sidebarLock": "",
|
"sidebarLock": "",
|
||||||
"selectAllElementsInFrame": "",
|
|
||||||
"removeAllElementsFromFrame": "",
|
|
||||||
"eyeDropper": ""
|
"eyeDropper": ""
|
||||||
},
|
},
|
||||||
"library": {
|
"library": {
|
||||||
@@ -223,9 +221,7 @@
|
|||||||
"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": "",
|
||||||
"frame": "",
|
"hand": ""
|
||||||
"hand": "",
|
|
||||||
"extraTools": ""
|
|
||||||
},
|
},
|
||||||
"headings": {
|
"headings": {
|
||||||
"canvasActions": "Vászon műveletek",
|
"canvasActions": "Vászon műveletek",
|
||||||
|
|||||||
@@ -124,8 +124,6 @@
|
|||||||
},
|
},
|
||||||
"statusPublished": "Telah terbit",
|
"statusPublished": "Telah terbit",
|
||||||
"sidebarLock": "Biarkan sidebar tetap terbuka",
|
"sidebarLock": "Biarkan sidebar tetap terbuka",
|
||||||
"selectAllElementsInFrame": "",
|
|
||||||
"removeAllElementsFromFrame": "",
|
|
||||||
"eyeDropper": ""
|
"eyeDropper": ""
|
||||||
},
|
},
|
||||||
"library": {
|
"library": {
|
||||||
@@ -223,9 +221,7 @@
|
|||||||
"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",
|
||||||
"frame": "",
|
"hand": "Tangan (alat panning)"
|
||||||
"hand": "Tangan (alat panning)",
|
|
||||||
"extraTools": ""
|
|
||||||
},
|
},
|
||||||
"headings": {
|
"headings": {
|
||||||
"canvasActions": "Opsi Kanvas",
|
"canvasActions": "Opsi Kanvas",
|
||||||
|
|||||||
@@ -124,8 +124,6 @@
|
|||||||
},
|
},
|
||||||
"statusPublished": "Pubblicato",
|
"statusPublished": "Pubblicato",
|
||||||
"sidebarLock": "Mantieni aperta la barra laterale",
|
"sidebarLock": "Mantieni aperta la barra laterale",
|
||||||
"selectAllElementsInFrame": "",
|
|
||||||
"removeAllElementsFromFrame": "",
|
|
||||||
"eyeDropper": ""
|
"eyeDropper": ""
|
||||||
},
|
},
|
||||||
"library": {
|
"library": {
|
||||||
@@ -223,9 +221,7 @@
|
|||||||
"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",
|
||||||
"frame": "",
|
"hand": "Mano (strumento di panoramica)"
|
||||||
"hand": "Mano (strumento di panoramica)",
|
|
||||||
"extraTools": "Altri strumenti"
|
|
||||||
},
|
},
|
||||||
"headings": {
|
"headings": {
|
||||||
"canvasActions": "Azioni sulla Tela",
|
"canvasActions": "Azioni sulla Tela",
|
||||||
@@ -365,12 +361,12 @@
|
|||||||
"withBackground": "Sfondo",
|
"withBackground": "Sfondo",
|
||||||
"onlySelected": "Solo selezionato",
|
"onlySelected": "Solo selezionato",
|
||||||
"darkMode": "Tema scuro",
|
"darkMode": "Tema scuro",
|
||||||
"embedScene": "Includi scena",
|
"embedScene": "",
|
||||||
"scale": "Scala",
|
"scale": "Scala",
|
||||||
"padding": ""
|
"padding": ""
|
||||||
},
|
},
|
||||||
"tooltip": {
|
"tooltip": {
|
||||||
"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."
|
"embedScene": ""
|
||||||
},
|
},
|
||||||
"title": {
|
"title": {
|
||||||
"exportToPng": "Esporta come PNG",
|
"exportToPng": "Esporta come PNG",
|
||||||
|
|||||||
@@ -124,8 +124,6 @@
|
|||||||
},
|
},
|
||||||
"statusPublished": "公開済み",
|
"statusPublished": "公開済み",
|
||||||
"sidebarLock": "サイドバーを開いたままにする",
|
"sidebarLock": "サイドバーを開いたままにする",
|
||||||
"selectAllElementsInFrame": "",
|
|
||||||
"removeAllElementsFromFrame": "",
|
|
||||||
"eyeDropper": ""
|
"eyeDropper": ""
|
||||||
},
|
},
|
||||||
"library": {
|
"library": {
|
||||||
@@ -223,9 +221,7 @@
|
|||||||
"penMode": "ペンモード - タッチ防止",
|
"penMode": "ペンモード - タッチ防止",
|
||||||
"link": "選択した図形のリンクを追加/更新",
|
"link": "選択した図形のリンクを追加/更新",
|
||||||
"eraser": "消しゴム",
|
"eraser": "消しゴム",
|
||||||
"frame": "",
|
"hand": "手 (パンニングツール)"
|
||||||
"hand": "手 (パンニングツール)",
|
|
||||||
"extraTools": ""
|
|
||||||
},
|
},
|
||||||
"headings": {
|
"headings": {
|
||||||
"canvasActions": "キャンバス操作",
|
"canvasActions": "キャンバス操作",
|
||||||
|
|||||||
@@ -124,8 +124,6 @@
|
|||||||
},
|
},
|
||||||
"statusPublished": "",
|
"statusPublished": "",
|
||||||
"sidebarLock": "",
|
"sidebarLock": "",
|
||||||
"selectAllElementsInFrame": "",
|
|
||||||
"removeAllElementsFromFrame": "",
|
|
||||||
"eyeDropper": ""
|
"eyeDropper": ""
|
||||||
},
|
},
|
||||||
"library": {
|
"library": {
|
||||||
@@ -223,9 +221,7 @@
|
|||||||
"penMode": "",
|
"penMode": "",
|
||||||
"link": "",
|
"link": "",
|
||||||
"eraser": "Óshirgish",
|
"eraser": "Óshirgish",
|
||||||
"frame": "",
|
"hand": ""
|
||||||
"hand": "",
|
|
||||||
"extraTools": ""
|
|
||||||
},
|
},
|
||||||
"headings": {
|
"headings": {
|
||||||
"canvasActions": "",
|
"canvasActions": "",
|
||||||
|
|||||||
@@ -124,8 +124,6 @@
|
|||||||
},
|
},
|
||||||
"statusPublished": "Yeffeɣ-d",
|
"statusPublished": "Yeffeɣ-d",
|
||||||
"sidebarLock": "Eǧǧ afeggag n yidis yeldi",
|
"sidebarLock": "Eǧǧ afeggag n yidis yeldi",
|
||||||
"selectAllElementsInFrame": "",
|
|
||||||
"removeAllElementsFromFrame": "",
|
|
||||||
"eyeDropper": ""
|
"eyeDropper": ""
|
||||||
},
|
},
|
||||||
"library": {
|
"library": {
|
||||||
@@ -223,9 +221,7 @@
|
|||||||
"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ḍ",
|
||||||
"frame": "",
|
"hand": "Afus (afecku n usmutti n tmuɣli)"
|
||||||
"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,8 +124,6 @@
|
|||||||
},
|
},
|
||||||
"statusPublished": "",
|
"statusPublished": "",
|
||||||
"sidebarLock": "",
|
"sidebarLock": "",
|
||||||
"selectAllElementsInFrame": "",
|
|
||||||
"removeAllElementsFromFrame": "",
|
|
||||||
"eyeDropper": ""
|
"eyeDropper": ""
|
||||||
},
|
},
|
||||||
"library": {
|
"library": {
|
||||||
@@ -223,9 +221,7 @@
|
|||||||
"penMode": "",
|
"penMode": "",
|
||||||
"link": "",
|
"link": "",
|
||||||
"eraser": "",
|
"eraser": "",
|
||||||
"frame": "",
|
"hand": ""
|
||||||
"hand": "",
|
|
||||||
"extraTools": ""
|
|
||||||
},
|
},
|
||||||
"headings": {
|
"headings": {
|
||||||
"canvasActions": "",
|
"canvasActions": "",
|
||||||
|
|||||||
@@ -124,8 +124,6 @@
|
|||||||
},
|
},
|
||||||
"statusPublished": "ត្រូវបានបោះពុម្ពផ្សាយ",
|
"statusPublished": "ត្រូវបានបោះពុម្ពផ្សាយ",
|
||||||
"sidebarLock": "ទុករបារចំហៀងបើក",
|
"sidebarLock": "ទុករបារចំហៀងបើក",
|
||||||
"selectAllElementsInFrame": "",
|
|
||||||
"removeAllElementsFromFrame": "",
|
|
||||||
"eyeDropper": ""
|
"eyeDropper": ""
|
||||||
},
|
},
|
||||||
"library": {
|
"library": {
|
||||||
@@ -223,9 +221,7 @@
|
|||||||
"penMode": "របៀបប៊ិច - ជៀសវាងការប៉ះ",
|
"penMode": "របៀបប៊ិច - ជៀសវាងការប៉ះ",
|
||||||
"link": "បន្ថែម/ធ្វើបច្ចុប្បន្នភាពតំណភ្ជាប់សម្រាប់រូបរាងដែលបានជ្រើសរើស",
|
"link": "បន្ថែម/ធ្វើបច្ចុប្បន្នភាពតំណភ្ជាប់សម្រាប់រូបរាងដែលបានជ្រើសរើស",
|
||||||
"eraser": "ជ័រលុប",
|
"eraser": "ជ័រលុប",
|
||||||
"frame": "",
|
"hand": "ដៃ (panning tool)"
|
||||||
"hand": "ដៃ (panning tool)",
|
|
||||||
"extraTools": ""
|
|
||||||
},
|
},
|
||||||
"headings": {
|
"headings": {
|
||||||
"canvasActions": "សកម្មភាពបាវ",
|
"canvasActions": "សកម្មភាពបាវ",
|
||||||
|
|||||||
@@ -124,8 +124,6 @@
|
|||||||
},
|
},
|
||||||
"statusPublished": "게시됨",
|
"statusPublished": "게시됨",
|
||||||
"sidebarLock": "사이드바 유지",
|
"sidebarLock": "사이드바 유지",
|
||||||
"selectAllElementsInFrame": "프레임의 모든 요소 선택",
|
|
||||||
"removeAllElementsFromFrame": "프레임의 모든 요소 삭제",
|
|
||||||
"eyeDropper": "캔버스에서 색상 고르기"
|
"eyeDropper": "캔버스에서 색상 고르기"
|
||||||
},
|
},
|
||||||
"library": {
|
"library": {
|
||||||
@@ -223,9 +221,7 @@
|
|||||||
"penMode": "펜 모드 - 터치 방지",
|
"penMode": "펜 모드 - 터치 방지",
|
||||||
"link": "선택한 도형에 대해서 링크를 추가/업데이트",
|
"link": "선택한 도형에 대해서 링크를 추가/업데이트",
|
||||||
"eraser": "지우개",
|
"eraser": "지우개",
|
||||||
"frame": "프레임 도구",
|
"hand": "손 (패닝 도구)"
|
||||||
"hand": "손 (패닝 도구)",
|
|
||||||
"extraTools": "다른 도구"
|
|
||||||
},
|
},
|
||||||
"headings": {
|
"headings": {
|
||||||
"canvasActions": "캔버스 동작",
|
"canvasActions": "캔버스 동작",
|
||||||
|
|||||||
+36
-40
@@ -71,7 +71,7 @@
|
|||||||
"language": "زمان",
|
"language": "زمان",
|
||||||
"liveCollaboration": "هاوکاریکردنی زیندو...",
|
"liveCollaboration": "هاوکاریکردنی زیندو...",
|
||||||
"duplicateSelection": "لەبەرگرتنەوە",
|
"duplicateSelection": "لەبەرگرتنەوە",
|
||||||
"untitled": "بێ ناونیشان",
|
"untitled": "بێ-ناو",
|
||||||
"name": "ناو",
|
"name": "ناو",
|
||||||
"yourName": "ناوەکەت",
|
"yourName": "ناوەکەت",
|
||||||
"madeWithExcalidraw": "دروستکراوە بە Excalidraw",
|
"madeWithExcalidraw": "دروستکراوە بە Excalidraw",
|
||||||
@@ -124,9 +124,7 @@
|
|||||||
},
|
},
|
||||||
"statusPublished": "بڵاوکراوەتەوە",
|
"statusPublished": "بڵاوکراوەتەوە",
|
||||||
"sidebarLock": "هێشتنەوەی شریتی لا بە کراوەیی",
|
"sidebarLock": "هێشتنەوەی شریتی لا بە کراوەیی",
|
||||||
"selectAllElementsInFrame": "هەموو توخمەکانی ناو چوارچێوەکە دیاری بکە",
|
"eyeDropper": ""
|
||||||
"removeAllElementsFromFrame": "هەموو توخمەکانی ناو چوارچێوەکە لابەرە",
|
|
||||||
"eyeDropper": "ڕەنگێک لەسەر تابلۆکە هەڵبژێرە"
|
|
||||||
},
|
},
|
||||||
"library": {
|
"library": {
|
||||||
"noItems": "هێشتا هیچ بڕگەیەک زیاد نەکراوە...",
|
"noItems": "هێشتا هیچ بڕگەیەک زیاد نەکراوە...",
|
||||||
@@ -223,9 +221,7 @@
|
|||||||
"penMode": "شێوازی قەڵەم - دەست لێدان ڕابگرە",
|
"penMode": "شێوازی قەڵەم - دەست لێدان ڕابگرە",
|
||||||
"link": "زیادکردن/ نوێکردنەوەی لینک بۆ شێوەی دیاریکراو",
|
"link": "زیادکردن/ نوێکردنەوەی لینک بۆ شێوەی دیاریکراو",
|
||||||
"eraser": "سڕەر",
|
"eraser": "سڕەر",
|
||||||
"frame": "ئامرازی چوارچێوە",
|
"hand": "دەست (ئامرازی پانکردن)"
|
||||||
"hand": "دەست (ئامرازی پانکردن)",
|
|
||||||
"extraTools": "ئامرازی زیاتر"
|
|
||||||
},
|
},
|
||||||
"headings": {
|
"headings": {
|
||||||
"canvasActions": "کردارەکانی تابلۆ",
|
"canvasActions": "کردارەکانی تابلۆ",
|
||||||
@@ -360,27 +356,27 @@
|
|||||||
"removeItemsFromLib": "لابردنی ئایتمە دیاریکراوەکان لە کتێبخانە"
|
"removeItemsFromLib": "لابردنی ئایتمە دیاریکراوەکان لە کتێبخانە"
|
||||||
},
|
},
|
||||||
"imageExportDialog": {
|
"imageExportDialog": {
|
||||||
"header": "وێنە هەناردە بکە",
|
"header": "",
|
||||||
"label": {
|
"label": {
|
||||||
"withBackground": "پاشبنەما",
|
"withBackground": "",
|
||||||
"onlySelected": "تەنها دیاریکراوەکان",
|
"onlySelected": "",
|
||||||
"darkMode": "دۆخی تاریک",
|
"darkMode": "",
|
||||||
"embedScene": "دیمەنەکە بەکاربهێنەرەوە",
|
"embedScene": "",
|
||||||
"scale": "قەبارە",
|
"scale": "",
|
||||||
"padding": "بۆشایی"
|
"padding": ""
|
||||||
},
|
},
|
||||||
"tooltip": {
|
"tooltip": {
|
||||||
"embedScene": "داتاکانی دیمەنەکە لە فایلە هەناردەکراوەکەی PNG/SVG هەڵدەگیرێن بۆ ئەوەی دیمەنەکە لێیەوە بگەڕێتەوە.\nقەبارەی پەڕگەی هەناردەکراو زیاد دەکات."
|
"embedScene": ""
|
||||||
},
|
},
|
||||||
"title": {
|
"title": {
|
||||||
"exportToPng": "هەناردە بکە وەک PNG",
|
"exportToPng": "",
|
||||||
"exportToSvg": "هەناردە بکە وەک SVG",
|
"exportToSvg": "",
|
||||||
"copyPngToClipboard": "لەبەربگرەوە بۆ سەر تەختەنوس"
|
"copyPngToClipboard": ""
|
||||||
},
|
},
|
||||||
"button": {
|
"button": {
|
||||||
"exportToPng": "PNG",
|
"exportToPng": "",
|
||||||
"exportToSvg": "SVG",
|
"exportToSvg": "",
|
||||||
"copyPngToClipboard": "لهبهری بگرهوه بۆ تهختهنووس"
|
"copyPngToClipboard": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"encrypted": {
|
"encrypted": {
|
||||||
@@ -415,20 +411,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": {
|
||||||
@@ -444,10 +440,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"colorPicker": {
|
"colorPicker": {
|
||||||
"mostUsedCustomColors": "زۆرترین ڕەنگە باوە بەکارهاتووەکان",
|
"mostUsedCustomColors": "",
|
||||||
"colors": "ڕەنگەکان",
|
"colors": "",
|
||||||
"shades": "سێبەرەکان",
|
"shades": "",
|
||||||
"hexCode": "کۆدی هێکس",
|
"hexCode": "",
|
||||||
"noShades": "هیچ سێبەرێک بۆ ئەم ڕەنگە بەردەست نییە"
|
"noShades": ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -124,8 +124,6 @@
|
|||||||
},
|
},
|
||||||
"statusPublished": "",
|
"statusPublished": "",
|
||||||
"sidebarLock": "",
|
"sidebarLock": "",
|
||||||
"selectAllElementsInFrame": "",
|
|
||||||
"removeAllElementsFromFrame": "",
|
|
||||||
"eyeDropper": ""
|
"eyeDropper": ""
|
||||||
},
|
},
|
||||||
"library": {
|
"library": {
|
||||||
@@ -223,9 +221,7 @@
|
|||||||
"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",
|
||||||
"frame": "",
|
"hand": ""
|
||||||
"hand": "",
|
|
||||||
"extraTools": ""
|
|
||||||
},
|
},
|
||||||
"headings": {
|
"headings": {
|
||||||
"canvasActions": "Veiksmai su drobe",
|
"canvasActions": "Veiksmai su drobe",
|
||||||
|
|||||||
@@ -124,8 +124,6 @@
|
|||||||
},
|
},
|
||||||
"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": {
|
||||||
@@ -223,9 +221,7 @@
|
|||||||
"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",
|
||||||
"frame": "",
|
"hand": "Roka (panoramēšanas rīks)"
|
||||||
"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