diff --git a/.dockerignore b/.dockerignore index 7a0150947..1f38a978c 100644 --- a/.dockerignore +++ b/.dockerignore @@ -6,6 +6,6 @@ !.prettierrc !package.json !public/ -!src/ +!packages/ !tsconfig.json !yarn.lock diff --git a/.env.development b/.env.development index 44955884f..95e21ff87 100644 --- a/.env.development +++ b/.env.development @@ -7,9 +7,6 @@ VITE_APP_LIBRARY_BACKEND=https://us-central1-excalidraw-room-persistence.cloudfu # collaboration WebSocket server (https://github.com/excalidraw/excalidraw-room) VITE_APP_WS_SERVER_URL=http://localhost:3002 -# set this only if using the collaboration workflow we use on excalidraw.com -VITE_APP_PORTAL_URL= - VITE_APP_PLUS_LP=https://plus.excalidraw.com VITE_APP_PLUS_APP=https://app.excalidraw.com diff --git a/.env.production b/.env.production index 26b46a52a..0c715854a 100644 --- a/.env.production +++ b/.env.production @@ -4,16 +4,13 @@ VITE_APP_BACKEND_V2_POST_URL=https://json.excalidraw.com/api/v2/post/ VITE_APP_LIBRARY_URL=https://libraries.excalidraw.com VITE_APP_LIBRARY_BACKEND=https://us-central1-excalidraw-room-persistence.cloudfunctions.net/libraries -VITE_APP_PORTAL_URL=https://portal.excalidraw.com - VITE_APP_PLUS_LP=https://plus.excalidraw.com VITE_APP_PLUS_APP=https://app.excalidraw.com VITE_APP_AI_BACKEND=https://oss-ai.excalidraw.com -# Fill to set socket server URL used for collaboration. -# Meant for forks only: excalidraw.com uses custom VITE_APP_PORTAL_URL flow -VITE_APP_WS_SERVER_URL= +# socket server URL used for collaboration +VITE_APP_WS_SERVER_URL=https://oss-collab.excalidraw.com VITE_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"}' diff --git a/.eslintignore b/.eslintignore index b238ce5f7..ab3aa6c76 100644 --- a/.eslintignore +++ b/.eslintignore @@ -5,4 +5,4 @@ package-lock.json firebase/ dist/ public/workbox -src/packages/excalidraw/types +packages/excalidraw/types diff --git a/.github/workflows/autorelease-excalidraw.yml b/.github/workflows/autorelease-excalidraw.yml index 4eaeb11f1..5ff5690eb 100644 --- a/.github/workflows/autorelease-excalidraw.yml +++ b/.github/workflows/autorelease-excalidraw.yml @@ -23,5 +23,5 @@ jobs: NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - name: Auto release run: | - yarn add @actions/core + yarn add @actions/core -W yarn autorelease diff --git a/.github/workflows/autorelease-preview.yml b/.github/workflows/autorelease-preview.yml index bcd501880..a40ed3c43 100644 --- a/.github/workflows/autorelease-preview.yml +++ b/.github/workflows/autorelease-preview.yml @@ -44,7 +44,7 @@ jobs: - name: Auto release preview id: "autorelease" run: | - yarn add @actions/core + yarn add @actions/core -W yarn autorelease preview ${{ github.event.issue.number }} - name: Post comment post release if: always() diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index d42f8f632..82f826361 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -16,7 +16,7 @@ jobs: - name: Install and lint run: | - yarn --frozen-lockfile + yarn install yarn test:other yarn test:code yarn test:typecheck diff --git a/.github/workflows/locales-coverage.yml b/.github/workflows/locales-coverage.yml index 822af06e1..3cce93e04 100644 --- a/.github/workflows/locales-coverage.yml +++ b/.github/workflows/locales-coverage.yml @@ -22,11 +22,11 @@ jobs: - name: Create report file run: | yarn locales-coverage - FILE_CHANGED=$(git diff src/locales/percentages.json) + FILE_CHANGED=$(git diff packages/excalidraw/locales/percentages.json) if [ ! -z "${FILE_CHANGED}" ]; then git config --global user.name 'Excalidraw Bot' git config --global user.email 'bot@excalidraw.com' - git add src/locales/percentages.json + git add packages/excalidraw/locales/percentages.json git commit -am "Auto commit: Calculate translation coverage" git push fi diff --git a/.github/workflows/size-limit.yml b/.github/workflows/size-limit.yml index 8ced8ee03..5bd3c0d92 100644 --- a/.github/workflows/size-limit.yml +++ b/.github/workflows/size-limit.yml @@ -15,16 +15,14 @@ jobs: 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 + - name: Install in packages/excalidraw + run: yarn + working-directory: packages/excalidraw env: CI: true - uses: andresz1/size-limit-action@v1 with: github_token: ${{ secrets.GITHUB_TOKEN }} - build_script: build:umd + build_script: build:esm skip_step: install - directory: src/packages/excalidraw + directory: packages/excalidraw diff --git a/.github/workflows/test-coverage-pr.yml b/.github/workflows/test-coverage-pr.yml index 76c818298..7ff40ad5d 100644 --- a/.github/workflows/test-coverage-pr.yml +++ b/.github/workflows/test-coverage-pr.yml @@ -16,7 +16,7 @@ jobs: with: node-version: "18.x" - name: "Install Deps" - run: yarn --frozen-lockfile + run: yarn install - name: "Test Coverage" run: yarn test:coverage - name: "Report Coverage" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5c4584e82..2c458a810 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,5 +13,5 @@ jobs: node-version: 18.x - name: Install and test run: | - yarn --frozen-lockfile + yarn install yarn test:app diff --git a/.gitignore b/.gitignore index 6e430f4ed..21d2730a2 100644 --- a/.gitignore +++ b/.gitignore @@ -21,10 +21,8 @@ npm-debug.log* package-lock.json yarn-debug.log* yarn-error.log* -src/packages/excalidraw/types -src/packages/excalidraw/example/public/bundle.js -src/packages/excalidraw/example/public/excalidraw-assets-dev -src/packages/excalidraw/example/public/excalidraw.development.js +packages/excalidraw/types coverage dev-dist html +examples/**/bundle.* \ No newline at end of file diff --git a/README.md b/README.md index 2a8a3f908..e8cd3b06f 100644 --- a/README.md +++ b/README.md @@ -85,7 +85,7 @@ We'll be adding these features as drop-in plugins for the npm package in the fut ## Quick start -Install the [Excalidraw npm package](https://www.npmjs.com/package/@excalidraw/excalidraw): +**Note:** following instructions are for installing the Excalidraw [npm package](https://www.npmjs.com/package/@excalidraw/excalidraw) when integrating Excalidraw into your own app. To run the repository locally for development, please refer to our [Development Guide](https://docs.excalidraw.com/docs/introduction/development). ``` npm install react react-dom @excalidraw/excalidraw @@ -97,7 +97,7 @@ or via yarn yarn add react react-dom @excalidraw/excalidraw ``` -Don't forget to check out our [Documentation](https://docs.excalidraw.com)! +Check out our [documentation](https://docs.excalidraw.com/docs/@excalidraw/excalidraw/installation) for more details! ## Contributing diff --git a/crowdin.yml b/crowdin.yml index a08b939c6..ccb8a17aa 100644 --- a/crowdin.yml +++ b/crowdin.yml @@ -1,3 +1,3 @@ files: - - source: /src/locales/en.json - translation: /src/locales/%locale%.json + - source: /packages/excalidraw/locales/en.json + translation: /packages/excalidraw/locales/%locale%.json diff --git a/dev-docs/docs/@excalidraw/excalidraw/api/children-components/main-menu.mdx b/dev-docs/docs/@excalidraw/excalidraw/api/children-components/main-menu.mdx index 77c746b84..e601a2471 100644 --- a/dev-docs/docs/@excalidraw/excalidraw/api/children-components/main-menu.mdx +++ b/dev-docs/docs/@excalidraw/excalidraw/api/children-components/main-menu.mdx @@ -133,7 +133,7 @@ function App() { } ``` -Here is a [complete list](https://github.com/excalidraw/excalidraw/blob/master/src/components/mainMenu/DefaultItems.tsx) of the default items. +Here is a [complete list](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/components/mainMenu/DefaultItems.tsx) of the default items. ### MainMenu.Group diff --git a/dev-docs/docs/@excalidraw/excalidraw/api/constants.mdx b/dev-docs/docs/@excalidraw/excalidraw/api/constants.mdx index a6c95ab2a..029f5b05c 100644 --- a/dev-docs/docs/@excalidraw/excalidraw/api/constants.mdx +++ b/dev-docs/docs/@excalidraw/excalidraw/api/constants.mdx @@ -37,7 +37,7 @@ Defaults to `THEME.LIGHT` unless passed in `initialData.appState.theme` ### MIME_TYPES -[`MIME_TYPES`](https://github.com/excalidraw/excalidraw/blob/master/src/constants.ts#L101) contains all the mime types supported by `Excalidraw`. +[`MIME_TYPES`](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/constants.ts#L101) contains all the mime types supported by `Excalidraw`. **How to use ** diff --git a/dev-docs/docs/@excalidraw/excalidraw/api/excalidraw-element-skeleton.mdx b/dev-docs/docs/@excalidraw/excalidraw/api/excalidraw-element-skeleton.mdx index 838896c42..b633236aa 100644 --- a/dev-docs/docs/@excalidraw/excalidraw/api/excalidraw-element-skeleton.mdx +++ b/dev-docs/docs/@excalidraw/excalidraw/api/excalidraw-element-skeleton.mdx @@ -2,9 +2,9 @@ We support a simplified API to make it easier to generate Excalidraw elements programmatically. This API is in beta and subject to change before stable. You can check the [PR](https://github.com/excalidraw/excalidraw/pull/6546) for more details. -For this purpose we introduced a new type [`ExcalidrawElementSkeleton`](https://github.com/excalidraw/excalidraw/blob/master/src/data/transform.ts#L133). This is the simplified version of [`ExcalidrawElement`](https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L134) type with the minimum possible attributes so that creating elements programmatically is much easier (especially for cases like binding arrows or creating text containers). +For this purpose we introduced a new type [`ExcalidrawElementSkeleton`](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/data/transform.ts#L133). This is the simplified version of [`ExcalidrawElement`](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L134) type with the minimum possible attributes so that creating elements programmatically is much easier (especially for cases like binding arrows or creating text containers). -The [`ExcalidrawElementSkeleton`](https://github.com/excalidraw/excalidraw/blob/master/src/data/transform.ts#L133) can be converted to fully qualified Excalidraw elements by using [`convertToExcalidrawElements`](/docs/@excalidraw/excalidraw/api/excalidraw-element-skeleton#converttoexcalidrawelements). +The [`ExcalidrawElementSkeleton`](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/data/transform.ts#L133) can be converted to fully qualified Excalidraw elements by using [`convertToExcalidrawElements`](/docs/@excalidraw/excalidraw/api/excalidraw-element-skeleton#converttoexcalidrawelements). ## convertToExcalidrawElements @@ -19,7 +19,7 @@ convertToExcalidrawElements( | Name | Type | Default | Description | | --- | --- | --- | --- | -| `elements` | [ExcalidrawElementSkeleton](https://github.com/excalidraw/excalidraw/blob/master/src/data/transform.ts#L137) | | The Excalidraw element Skeleton which needs to be converted to Excalidraw elements. | +| `elements` | [ExcalidrawElementSkeleton](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/data/transform.ts#L137) | | The Excalidraw element Skeleton which needs to be converted to Excalidraw elements. | | `opts` | `{ regenerateIds: boolean }` | ` {regenerateIds: true}` | By default `id` will be regenerated for all the elements irrespective of whether you pass the `id` so if you don't want the ids to regenerated, you can set this attribute to `false`. | **_How to use_** @@ -71,7 +71,7 @@ function App() { } ``` -You can pass additional [`properties`](https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L27) as well to decorate the shapes. +You can pass additional [`properties`](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L27) as well to decorate the shapes. :::info @@ -192,7 +192,7 @@ convertToExcalidrawElements([ ### Text Containers -In addition to `type`, `x` and `y` properties, [`label`](https://github.com/excalidraw/excalidraw/blob/master/src/data/transform.ts#L124C7-L130C59) property is required for text containers. The `text` property in `label` is required, rest of the attributes are optional. +In addition to `type`, `x` and `y` properties, [`label`](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/data/transform.ts#L124C7-L130C59) property is required for text containers. The `text` property in `label` is required, rest of the attributes are optional. If you don't provide the dimensions of container, we calculate it based of the label dimensions. @@ -326,7 +326,7 @@ convertToExcalidrawElements([ ### Arrow bindings -To bind arrow to a shape you need to specify its [`start`](https://github.com/excalidraw/excalidraw/blob/master/src/data/transform.ts#L86) and [`end`](https://github.com/excalidraw/excalidraw/blob/master/src/data/transform.ts#L54) properties. You need to pass either `type` or `id` property in `start` and `end` properties, rest of the attributes are optional +To bind arrow to a shape you need to specify its [`start`](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/data/transform.ts#L86) and [`end`](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/data/transform.ts#L54) properties. You need to pass either `type` or `id` property in `start` and `end` properties, rest of the attributes are optional ```js convertToExcalidrawElements([ diff --git a/dev-docs/docs/@excalidraw/excalidraw/api/props/excalidraw-api.mdx b/dev-docs/docs/@excalidraw/excalidraw/api/props/excalidraw-api.mdx index 22e9034fd..ffff19fb0 100644 --- a/dev-docs/docs/@excalidraw/excalidraw/api/props/excalidraw-api.mdx +++ b/dev-docs/docs/@excalidraw/excalidraw/api/props/excalidraw-api.mdx @@ -2,7 +2,7 @@
   (api:{" "}
-  
+  
     ExcalidrawAPI
   
   ) => void;
@@ -17,7 +17,7 @@ export default function App() {
 }
 ```
 
-You can use this prop when you want to access some [Excalidraw APIs](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L616). We expose the below APIs :point_down:
+You can use this prop when you want to access some [Excalidraw APIs](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L616). We expose the below APIs :point_down:
 
 | API | Signature | Usage |
 | --- | --- | --- |
@@ -37,7 +37,7 @@ You can use this prop when you want to access some [Excalidraw APIs](https://git
 | [setActiveTool](#setactivetool) | `function` | This API can be used to set the active tool |
 | [setCursor](#setcursor) | `function` | This API can be used to set customise the mouse cursor on the canvas |
 | [resetCursor](#resetcursor) | `function` | This API can be used to reset to default mouse cursor on the canvas |
-| [toggleMenu](#togglemenu) | `function` | Toggles specific menus on/off |
+| [toggleSidebar](#toggleSidebar) | `function` | Toggles specific sidebar on/off |
 | [onChange](#onChange) | `function` | Subscribes to change events |
 | [onPointerDown](#onPointerDown) | `function` | Subscribes to `pointerdown` events |
 | [onPointerUp](#onPointerUp) | `function` | Subscribes to `pointerup` events |
@@ -52,7 +52,7 @@ Additionally `ready` and `readyPromise` from the API have been discontinued. The
 
 
   (scene:{" "}
-  
+  
     sceneData
   
   ) => void
@@ -62,9 +62,9 @@ You can use this function to update the scene with the sceneData. It accepts the
 
 | Name | Type | Description |
 | --- | --- | --- |
-| `elements` | [`ImportedDataState["elements"]`](https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L38) | The `elements` to be updated in the scene |
-| `appState` | [`ImportedDataState["appState"]`](https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L39) | The `appState` to be updated in the scene. |
-| `collaborators` | MapCollaborator> | The list of collaborators to be updated in the scene. |
+| `elements` | [`ImportedDataState["elements"]`](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/data/types.ts#L38) | The `elements` to be updated in the scene |
+| `appState` | [`ImportedDataState["appState"]`](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/data/types.ts#L39) | The `appState` to be updated in the scene. |
+| `collaborators` | MapCollaborator> | The list of collaborators to be updated in the scene. |
 | `commitToHistory` | `boolean` | Implies if the `history (undo/redo)` should be recorded. Defaults to `false`. |
 
 ```jsx live
@@ -125,13 +125,13 @@ function App() {
 
 
   (opts: { 
libraryItems:{" "} - + LibraryItemsSource ;
merge?: boolean;
prompt?: boolean;
openLibraryMenu?: boolean;
defaultStatus?: "unpublished" | "published";
}) => Promise< - + LibraryItems > @@ -141,7 +141,7 @@ You can use this function to update the library. It accepts the below attributes | Name | Type | Default | Description | | --- | --- | --- | --- | -| `libraryItems` | [LibraryItemsSource](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L249) | \_ | The `libraryItems` to be replaced/merged with current library | +| `libraryItems` | [LibraryItemsSource](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L249) | \_ | The `libraryItems` to be replaced/merged with current library | | `merge` | boolean | `false` | Whether to merge with existing library items. | | `prompt` | boolean | `false` | Whether to prompt user for confirmation. | | `openLibraryMenu` | boolean | `false` | Keep the library menu open after library is updated. | @@ -189,7 +189,7 @@ function App() { setExcalidrawAPI(api)} - // initial data retrieved from https://github.com/excalidraw/excalidraw/blob/master/dev-docs/src/initialData.js + // initial data retrieved from https://github.com/excalidraw/excalidraw/blob/master/dev-docs/packages/excalidraw/initialData.js initialData={{ libraryItems: initialData.libraryItems, appState: { openSidebar: "library" }, @@ -204,7 +204,7 @@ function App() {
   (files:{" "}
-  
+  
     BinaryFileData
   
   ) => void
@@ -224,7 +224,7 @@ Resets the scene. If `resetLoadingState` is passed as true then it will also for
 
 
   () =>{" "}
-  
+  
     ExcalidrawElement[]
   
 
@@ -235,7 +235,7 @@ Returns all the elements including the deleted in the scene.
   () => NonDeleted<
-  
+  
     ExcalidrawElement
   
   []>
@@ -247,7 +247,7 @@ Returns all the elements excluding the deleted in the scene
 
 
   () =>{" "}
-  
+  
     AppState
   
 
@@ -288,7 +288,7 @@ Scroll the nearest element out of the elements supplied to the center of the vie | 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 | [ExcalidrawElement](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L115) | [ExcalidrawElement[]](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L115) | All scene elements | The element(s) to scroll to. | | opts.fitToContent | boolean | false | Whether to fit the elements to viewport by automatically changing zoom as needed. Note that the zoom range is between 10%-100%. | | opts.fitToViewport | boolean | false | Similar to fitToContent but the zoom range is not limited. If elements are smaller than the viewport, zoom will go above 100%. | | opts.viewportZoomFactor | number | 0.7 | when fitToViewport=true, how much screen should the content cover, between 0.1 (10%) and 1 (100%) | @@ -336,7 +336,7 @@ The unique id of the excalidraw component. This can be used to identify the exca
   () =>{" "}
-  
+  
     files
   
 
@@ -364,7 +364,7 @@ This API has the below signature. It sets the `tool` passed in param as the acti | Name | Type | Default | Description | | --- | --- | --- | --- | -| `type` | [ToolType](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L91) | `selection` | The tool type which should be set as active tool. When setting `image` as active tool, the insertion onto canvas when using image tool is disabled by default, so you can enable it by setting `insertOnCanvasDirectly` to `true` | +| `type` | [ToolType](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L91) | `selection` | The tool type which should be set as active tool. When setting `image` as active tool, the insertion onto canvas when using image tool is disabled by default, so you can enable it by setting `insertOnCanvasDirectly` to `true` | | `locked` | `boolean` | `false` | Indicates whether the the active tool should be locked. It behaves the same way when using the `lock` tool in the editor interface | ## setCursor diff --git a/dev-docs/docs/@excalidraw/excalidraw/api/props/initialdata.mdx b/dev-docs/docs/@excalidraw/excalidraw/api/props/initialdata.mdx index 7d79128f0..0fec6ea02 100644 --- a/dev-docs/docs/@excalidraw/excalidraw/api/props/initialdata.mdx +++ b/dev-docs/docs/@excalidraw/excalidraw/api/props/initialdata.mdx @@ -1,18 +1,18 @@ # initialData
-{ elements?: ExcalidrawElement[], appState?: AppState }
+{ elements?: ExcalidrawElement[], appState?: AppState }
 
This helps to load Excalidraw with `initialData`. It must be an object or a [promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/Promise) which resolves to an object containing the below optional fields. | Name | Type | Description | | --- | --- | --- | -| `elements` | [ExcalidrawElement[]](https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114) | The `elements` with which `Excalidraw` should be mounted. | -| `appState` | [AppState](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95) | The `AppState` with which `Excalidraw` should be mounted. | +| `elements` | [ExcalidrawElement[]](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L114) | The `elements` with which `Excalidraw` should be mounted. | +| `appState` | [AppState](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L95) | The `AppState` with which `Excalidraw` should be mounted. | | `scrollToContent` | `boolean` | This attribute indicates whether to `scroll` to the nearest element to center once `Excalidraw` is mounted. By default, it will not scroll the nearest element to the center. Make sure you pass `initialData.appState.scrollX` and `initialData.appState.scrollY` when `scrollToContent` is false so that scroll positions are retained | -| `libraryItems` | [LibraryItems](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L247) | Promise<[LibraryItems](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L200)> | This library items with which `Excalidraw` should be mounted. | -| `files` | [BinaryFiles](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L82) | The `files` added to the scene. | +| `libraryItems` | [LibraryItems](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L247) | Promise<[LibraryItems](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L200)> | This library items with which `Excalidraw` should be mounted. | +| `files` | [BinaryFiles](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L82) | The `files` added to the scene. | You might want to use this when you want to load excalidraw with some initial elements and app state. diff --git a/dev-docs/docs/@excalidraw/excalidraw/api/props/props.mdx b/dev-docs/docs/@excalidraw/excalidraw/api/props/props.mdx index 069b15901..766c723e4 100644 --- a/dev-docs/docs/@excalidraw/excalidraw/api/props/props.mdx +++ b/dev-docs/docs/@excalidraw/excalidraw/api/props/props.mdx @@ -23,7 +23,7 @@ All `props` are _optional_. | [`libraryReturnUrl`](#libraryreturnurl) | `string` | _ | What URL should [libraries.excalidraw.com](https://libraries.excalidraw.com) be installed to | | [`theme`](#theme) | `"light"` | `"dark"` | `"light"` | The theme of the Excalidraw component | | [`name`](#name) | `string` | | Name of the drawing | -| [`UIOptions`](/docs/@excalidraw/excalidraw/api/props/ui-options) | `object` | [DEFAULT UI OPTIONS](https://github.com/excalidraw/excalidraw/blob/master/src/constants.ts#L151) | To customise UI options. Currently we support customising [`canvas actions`](#canvasactions) | +| [`UIOptions`](/docs/@excalidraw/excalidraw/api/props/ui-options) | `object` | [DEFAULT UI OPTIONS](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/constants.ts#L151) | To customise UI options. Currently we support customising [`canvas actions`](/docs/@excalidraw/excalidraw/api/props/ui-options#canvasactions) | | [`detectScroll`](#detectscroll) | `boolean` | `true` | Indicates whether to update the offsets when nearest ancestor is scrolled. | | [`handleKeyboardGlobally`](#handlekeyboardglobally) | `boolean` | `false` | Indicates whether to bind the keyboard events to document. | | [`autoFocus`](#autofocus) | `boolean` | `false` | indicates whether to focus the Excalidraw component on page load | @@ -33,7 +33,7 @@ All `props` are _optional_. ### Storing custom data on Excalidraw elements -Beyond attributes that Excalidraw elements already support, you can store `custom` data on each `element` in a `customData` object. The type of the attribute is [`Record`](https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L66) and is optional. +Beyond attributes that Excalidraw elements already support, you can store `custom` data on each `element` in a `customData` object. The type of the attribute is [`Record`](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L66) and is optional. You can use this to add any extra information you need to keep track of. @@ -59,11 +59,11 @@ Every time component updates, this callback if passed will get triggered and has (excalidrawElements, appState, files) => void; ``` -1. `excalidrawElements`: Array of [excalidrawElements](https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114) in the scene. +1. `excalidrawElements`: Array of [excalidrawElements](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L114) in the scene. -2. `appState`: [AppState](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95) of the scene. +2. `appState`: [AppState](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L95) of the scene. -3. `files`: The [BinaryFiles](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L64) which are added to the scene. +3. `files`: The [BinaryFiles](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L64) which are added to the scene. Here you can try saving the data to your backend or local storage for example. @@ -79,14 +79,14 @@ This callback is triggered when mouse pointer is updated. 2.`button`: The position of the button. This will be one of `["down", "up"]` -3.`pointersMap`: [`pointers`](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L131) map of the scene +3.`pointersMap`: [`pointers`](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L131) map of the scene ```js (exportedElements, appState, canvas) => void ``` -1. `exportedElements`: An array of [non deleted elements](https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L87) which needs to be exported. -2. `appState`: [AppState](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95) of the scene. +1. `exportedElements`: An array of [non deleted elements](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L87) which needs to be exported. +2. `appState`: [AppState](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L95) of the scene. 3. `canvas`: The `HTMLCanvasElement` of the scene. ### onPointerDown @@ -96,11 +96,11 @@ This prop if passed will be triggered on pointer down events and has the below s
 (activeTool:{" "}
-  
+  
     {" "}
     AppState["activeTool"]
   
-  , pointerDownState: 
+  , pointerDownState: 
     PointerDownState
   ) => void
 
@@ -119,7 +119,7 @@ This callback is triggered if passed when something is pasted into the scene. Yo
   (data:{" "}
-  
+  
     ClipboardData
   
   , event: ClipboardEvent | null) => boolean
@@ -135,7 +135,7 @@ This callback if supplied will get triggered when the library is updated and has
 
 
   (items:{" "}
-  
+  
     LibraryItems
   
   ) => void | Promise<any>
@@ -149,7 +149,7 @@ This prop if passed will be triggered when clicked on `link`. To handle the redi
 
 
   (element:{" "}
-  
+  
     ExcalidrawElement
   
   , event: CustomEvent<{ nativeEvent: MouseEvent }>) => void
@@ -182,7 +182,7 @@ const onLinkOpen: ExcalidrawProps["onLinkOpen"] = useCallback(
 
 ### langCode
 
-Determines the `language` of the UI. It should be one of the [available language codes](https://github.com/excalidraw/excalidraw/blob/master/src/i18n.ts#L14). Defaults to `en` (English). We also export default language and supported languages which you can import as shown below.
+Determines the `language` of the UI. It should be one of the [available language codes](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/i18n.ts#L14). Defaults to `en` (English). We also export default language and supported languages which you can import as shown below.
 
 ```js
 import { defaultLang, languages } from "@excalidraw/excalidraw";
@@ -191,7 +191,7 @@ import { defaultLang, languages } from "@excalidraw/excalidraw";
 | name | type |
 | --- | --- |
 | `defaultLang` | `string` |
-| `languages` | [`Language[]`](https://github.com/excalidraw/excalidraw/blob/master/src/i18n.ts#L15) |
+| `languages` | [`Language[]`](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/i18n.ts#L15) |
 
 ### viewModeEnabled
 
diff --git a/dev-docs/docs/@excalidraw/excalidraw/api/props/render-props.mdx b/dev-docs/docs/@excalidraw/excalidraw/api/props/render-props.mdx
index f0f4adc44..0df7634cb 100644
--- a/dev-docs/docs/@excalidraw/excalidraw/api/props/render-props.mdx
+++ b/dev-docs/docs/@excalidraw/excalidraw/api/props/render-props.mdx
@@ -4,7 +4,7 @@
 
 
   (isMobile: boolean, appState:
-  
+  
     AppState
   ) => JSX | null
 
@@ -66,7 +66,7 @@ function App() {
   (element: NonDeleted<ExcalidrawEmbeddableElement>, appState:{" "}
-  
+  
     AppState
   
   ) => JSX.Element | null
diff --git a/dev-docs/docs/@excalidraw/excalidraw/api/props/ui-options.mdx b/dev-docs/docs/@excalidraw/excalidraw/api/props/ui-options.mdx
index ac91f9e00..9d77e390a 100644
--- a/dev-docs/docs/@excalidraw/excalidraw/api/props/ui-options.mdx
+++ b/dev-docs/docs/@excalidraw/excalidraw/api/props/ui-options.mdx
@@ -4,7 +4,7 @@ This prop can be used to customise UI of Excalidraw. Currently we support custom
 
 
   {
-  
canvasActions?: +
canvasActions?:
CanvasActions ,
dockedSidebarBreakpoint?: number,
welcomeScreen?: boolean
@@ -55,7 +55,7 @@ If `UIOptions.canvasActions.export` is `false` the export button will not be ren ## dockedSidebarBreakpoint -This prop indicates at what point should we break to a docked, permanent sidebar. If not passed it defaults to [`MQ_RIGHT_SIDEBAR_MAX_WIDTH_PORTRAIT`](https://github.com/excalidraw/excalidraw/blob/master/src/constants.ts#L161). +This prop indicates at what point should we break to a docked, permanent sidebar. If not passed it defaults to [`MQ_RIGHT_SIDEBAR_MAX_WIDTH_PORTRAIT`](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/constants.ts#L161). If the _width_ of the _excalidraw_ container exceeds _dockedSidebarBreakpoint_, the sidebar will be `dockable` and the button to `dock` the sidebar will be shown If user choses to `dock` the sidebar, it will push the right part of the UI towards the left, making space for the sidebar as shown below. @@ -73,9 +73,9 @@ function App() { ## tools -This `prop ` controls the visibility of the tools in the editor. +This `prop` controls the visibility of the tools in the editor. Currently you can control the visibility of `image` tool via this prop. | Prop | Type | Default | Description | | --- | --- | --- | --- | -| image | boolean | true | Decides whether `image` tool should be visible. \ No newline at end of file +| image | boolean | true | Decides whether `image` tool should be visible. diff --git a/dev-docs/docs/@excalidraw/excalidraw/api/utils/export.mdx b/dev-docs/docs/@excalidraw/excalidraw/api/utils/export.mdx index f5469f01d..ef59054c4 100644 --- a/dev-docs/docs/@excalidraw/excalidraw/api/utils/export.mdx +++ b/dev-docs/docs/@excalidraw/excalidraw/api/utils/export.mdx @@ -20,16 +20,16 @@ exportToCanvas({
  getDimensions,
  files,
  exportPadding?: number;
-}: ExportOpts +}: ExportOpts
| Name | Type | Default | Description | | --- | --- | --- | --- | -| `elements` | [Excalidraw Element []](https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114) | | The elements to be exported to canvas. | -| `appState` | [AppState](https://github.com/excalidraw/excalidraw/blob/master/src/packages/utils.ts#L23) | [Default App State](https://github.com/excalidraw/excalidraw/blob/master/src/appState.ts#L17) | The app state of the scene. | +| `elements` | [Excalidraw Element []](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L114) | | The elements to be exported to canvas. | +| `appState` | [AppState](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/packages/utils.ts#L23) | [Default App State](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/appState.ts#L17) | The app state of the scene. | | [`getDimensions`](#getdimensions) | `function` | _ | A function which returns the `width`, `height`, and optionally `scale` (defaults to `1`), with which canvas is to be exported. | | `maxWidthOrHeight` | `number` | _ | The maximum `width` or `height` of the exported image. If provided, `getDimensions` is ignored. | -| `files` | [BinaryFiles](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L59) | _ | The files added to the scene. | +| `files` | [BinaryFiles](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L59) | _ | The files added to the scene. | | `exportPadding` | `number` | `10` | The `padding` to be added on canvas. | @@ -105,7 +105,7 @@ function App() {
 exportToBlob(
  - opts: ExportOpts & {
  + opts: ExportOpts & {
  mimeType?: string,
  quality?: number,
  exportPadding?: number;
@@ -134,16 +134,16 @@ Returns a promise which resolves with a [blob](https://developer.mozilla.org/en-
 exportToSvg({
  elements:  - + ExcalidrawElement[] ,
  appState: - AppState + AppState ,
  exportPadding: number,
  metadata: string,
  files:  - + BinaryFiles ,
}); @@ -151,10 +151,10 @@ exportToSvg({
  | Name | Type | Default | Description | | --- | --- | --- | --- | -| elements | [Excalidraw Element []](https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114) | | The elements to exported as `svg `| -| appState | [AppState](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95) | [defaultAppState](https://github.com/excalidraw/excalidraw/blob/master/src/appState.ts#L11) | The `appState` of the scene | +| elements | [Excalidraw Element []](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L114) | | The elements to exported as `svg `| +| appState | [AppState](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L95) | [defaultAppState](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/appState.ts#L11) | The `appState` of the scene | | exportPadding | number | 10 | The `padding` to be added on canvas | -| files | [BinaryFiles](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L64) | undefined | The `files` added to the scene. | +| files | [BinaryFiles](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L64) | undefined | The `files` added to the scene. | This function returns a promise which resolves to `svg` of the exported drawing. @@ -164,7 +164,7 @@ This function returns a promise which resolves to `svg` of the exported drawing.
 exportToClipboard(
  - opts: ExportOpts & {
  + opts: ExportOpts & {
  mimeType?: string,
  quality?: number;
  type: 'png' | 'svg' |'json'
diff --git a/dev-docs/docs/@excalidraw/excalidraw/api/utils/restore.mdx b/dev-docs/docs/@excalidraw/excalidraw/api/utils/restore.mdx index 665a1ef9f..3f2546483 100644 --- a/dev-docs/docs/@excalidraw/excalidraw/api/utils/restore.mdx +++ b/dev-docs/docs/@excalidraw/excalidraw/api/utils/restore.mdx @@ -8,7 +8,7 @@ id: "restore" **_Signature_**
-restoreAppState(appState: ImportedDataState["appState"],
  localAppState: Partial<AppState> | null): AppState +restoreAppState(appState: ImportedDataState["appState"],
  localAppState: Partial<AppState> | null): AppState
**_How to use_** @@ -17,7 +17,7 @@ restoreAppState(appState: restoreElements( - elements: ImportedDataState["elements"],
  - localElements: ExcalidrawElement[] | null | undefined): ExcalidrawElement[],
  + elements: ImportedDataState["elements"],
  + localElements: ExcalidrawElement[] | null | undefined): ExcalidrawElement[],
  opts: { refreshDimensions?: boolean, repairBindings?: boolean }
)
| Prop | Type | Description | | ---- | ---- | ---- | -| `elements` | ImportedDataState["elements"] | The `elements` to be restored | -| [`localElements`](#localelements) | ExcalidrawElement[] | null | undefined | When `localElements` are supplied, they are used to ensure that existing restored elements reuse `version` (and increment it), and regenerate `versionNonce`. | +| `elements` | ImportedDataState["elements"] | The `elements` to be restored | +| [`localElements`](#localelements) | ExcalidrawElement[] | null | undefined | When `localElements` are supplied, they are used to ensure that existing restored elements reuse `version` (and increment it), and regenerate `versionNonce`. | | [`opts`](#opts) | `Object` | The extra optional parameter to configure restored elements #### localElements @@ -70,15 +70,15 @@ Parameter `refreshDimensions` indicates whether we should also `recalculate` tex
 restore(
-  data: ImportedDataState,
  - localAppState: Partial<AppState> | null | undefined,
  - localElements: ExcalidrawElement[] | null | undefined
): DataState
+ data: ImportedDataState,
  + localAppState: Partial<AppState> | null | undefined,
  + localElements: ExcalidrawElement[] | null | undefined
): DataState
opts: { refreshDimensions?: boolean, repairBindings?: boolean }
)
-See [`restoreAppState()`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#restoreAppState) about `localAppState`, and [`restoreElements()`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#restoreElements) about `localElements`. +See [`restoreAppState()`](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/packages/excalidraw/README.md#restoreAppState) about `localAppState`, and [`restoreElements()`](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/packages/excalidraw/README.md#restoreElements) about `localElements`. **_How to use_** @@ -93,7 +93,7 @@ This function makes sure elements and state is set to appropriate values and set **_Signature_**
-restoreLibraryItems(libraryItems: ImportedDataState["libraryItems"],
  +restoreLibraryItems(libraryItems: ImportedDataState["libraryItems"],
  defaultStatus: "published" | "unpublished")
diff --git a/dev-docs/docs/@excalidraw/excalidraw/api/utils/utils-intro.md b/dev-docs/docs/@excalidraw/excalidraw/api/utils/utils-intro.md index 9f3d3287c..69bd88a83 100644 --- a/dev-docs/docs/@excalidraw/excalidraw/api/utils/utils-intro.md +++ b/dev-docs/docs/@excalidraw/excalidraw/api/utils/utils-intro.md @@ -8,7 +8,7 @@ These are pure Javascript functions exported from the @excalidraw/excalidraw [`@ ### serializeAsJSON -Takes the scene elements and state and returns a JSON string. `Deleted` elements as well as most properties from `AppState` are removed from the resulting JSON. (see [`serializeAsJSON()`](https://github.com/excalidraw/excalidraw/blob/master/src/data/json.ts#L42) source for details). +Takes the scene elements and state and returns a JSON string. `Deleted` elements as well as most properties from `AppState` are removed from the resulting JSON. (see [`serializeAsJSON()`](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/data/json.ts#L42) source for details). If you want to overwrite the `source` field in the `JSON` string, you can set `window.EXCALIDRAW_EXPORT_SOURCE` to the desired value. @@ -16,8 +16,8 @@ If you want to overwrite the `source` field in the `JSON` string, you can set `w
 serializeAsJSON({
  - elements: ExcalidrawElement[],
  - appState: AppState,
+ elements: ExcalidrawElement[],
  + appState: AppState,
}): string
@@ -37,7 +37,7 @@ If you want to overwrite the source field in the JSON string, you can set `windo
 serializeLibraryAsJSON(
-  libraryItems: LibraryItems[])
+  libraryItems: LibraryItems[])
 
**How to use** @@ -53,7 +53,7 @@ Returns `true` if element is invisibly small (e.g. width & height are zero). **_Signature_**
-isInvisiblySmallElement(element:  ExcalidrawElement): boolean
+isInvisiblySmallElement(element:  ExcalidrawElement): boolean
 
**How to use** @@ -80,10 +80,10 @@ excalidrawAPI.updateScene(scene);
 loadFromBlob(
  blob: Blob,
  - localAppState: AppState | null,
  - localElements: ExcalidrawElement[] | null,
  + localAppState: AppState | null,
  + localElements: ExcalidrawElement[] | null,
  fileHandle?: FileSystemHandle | null
-) => Promise<RestoredDataState> +) => Promise<RestoredDataState>
### loadLibraryFromBlob @@ -130,10 +130,10 @@ if (contents.type === MIME_TYPES.excalidraw) {
 loadSceneOrLibraryFromBlob(
  blob: Blob,
  - localAppState: AppState | null,
  - localElements: ExcalidrawElement[] | null,
  + localAppState: AppState | null,
  + localElements: ExcalidrawElement[] | null,
  fileHandle?: FileSystemHandle | null
-) => Promise<{ type: string, data: RestoredDataState | ImportedLibraryState}> +) => Promise<{ type: string, data: RestoredDataState | ImportedLibraryState}>
### getFreeDrawSvgPath @@ -149,7 +149,7 @@ import { getFreeDrawSvgPath } from "@excalidraw/excalidraw"; **Signature**
-getFreeDrawSvgPath(element: ExcalidrawFreeDrawElement)
+getFreeDrawSvgPath(element: ExcalidrawFreeDrawElement)
 
### isLinearElement @@ -165,7 +165,7 @@ import { isLinearElement } from "@excalidraw/excalidraw"; **Signature**
-isLinearElement(elementType?: ExcalidrawElement): boolean
+isLinearElement(elementType?: ExcalidrawElement): boolean
 
### getNonDeletedElements @@ -181,7 +181,7 @@ import { getNonDeletedElements } from "@excalidraw/excalidraw"; **Signature**
-getNonDeletedElements(elements: readonly ExcalidrawElement[]): as readonly NonDeletedExcalidrawElement[]
+getNonDeletedElements(elements: readonly ExcalidrawElement[]): as readonly NonDeletedExcalidrawElement[]
 
### mergeLibraryItems @@ -196,9 +196,9 @@ import { mergeLibraryItems } from "@excalidraw/excalidraw";
 mergeLibraryItems(
  - localItems: LibraryItems,
  - otherItems: LibraryItems
-): LibraryItems + localItems: LibraryItems,
  + otherItems: LibraryItems
+): LibraryItems
### parseLibraryTokensFromUrl @@ -239,8 +239,8 @@ export const App = () => {
 useHandleLibrary(opts: {
  - excalidrawAPI: ExcalidrawAPI,
  - getInitialLibraryItems?: () => LibraryItemsSource
+ excalidrawAPI: ExcalidrawAPI,
  + getInitialLibraryItems?: () => LibraryItemsSource
});
@@ -253,7 +253,7 @@ This function returns the current `scene` version. **_Signature_**
-getSceneVersion(elements:  ExcalidrawElement[])
+getSceneVersion(elements:  ExcalidrawElement[])
 
**How to use** @@ -274,7 +274,7 @@ import { sceneCoordsToViewportCoords } from "@excalidraw/excalidraw";
 sceneCoordsToViewportCoords({ sceneX: number, sceneY: number },
  - appState: AppState
): { x: number, y: number } + appState: AppState
): { x: number, y: number }
### viewportCoordsToSceneCoords @@ -289,7 +289,7 @@ import { viewportCoordsToSceneCoords } from "@excalidraw/excalidraw";
 viewportCoordsToSceneCoords({ clientX: number, clientY: number },
  - appState: AppState
): {x: number, y: number} + appState: AppState
): {x: number, y: number}
### useDevice @@ -350,8 +350,8 @@ To help with localization, we export the following. | name | type | | --- | --- | | `defaultLang` | `string` | -| `languages` | [`Language[]`](https://github.com/excalidraw/excalidraw/blob/master/src/i18n.ts#L15) | -| `useI18n` | [`() => { langCode, t }`](https://github.com/excalidraw/excalidraw/blob/master/src/i18n.ts#L15) | +| `languages` | [`Language[]`](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/i18n.ts#L15) | +| `useI18n` | [`() => { langCode, t }`](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/i18n.ts#L15) | ```js import { defaultLang, languages, useI18n } from "@excalidraw/excalidraw"; diff --git a/dev-docs/docs/@excalidraw/excalidraw/customizing-styles.mdx b/dev-docs/docs/@excalidraw/excalidraw/customizing-styles.mdx index 9e0f9fcdf..ea9073c1c 100644 --- a/dev-docs/docs/@excalidraw/excalidraw/customizing-styles.mdx +++ b/dev-docs/docs/@excalidraw/excalidraw/customizing-styles.mdx @@ -21,7 +21,7 @@ Most notably, you can customize the primary colors, by overriding these variable - `--color-primary-light` - `--color-primary-contrast-offset` — a slightly darker (in light mode), or lighter (in dark mode) `--color-primary` color to fix contrast issues (see [Chubb illusion](https://en.wikipedia.org/wiki/Chubb_illusion)). It will fall back to `--color-primary` if not present. -For a complete list of variables, check [theme.scss](https://github.com/excalidraw/excalidraw/blob/master/src/css/theme.scss), though most of them will not make sense to override. +For a complete list of variables, check [theme.scss](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/css/theme.scss), though most of them will not make sense to override. ```css showLineNumbers .custom-styles .excalidraw { diff --git a/dev-docs/docs/@excalidraw/excalidraw/development.mdx b/dev-docs/docs/@excalidraw/excalidraw/development.mdx index 2e73d5731..3c405a7d9 100644 --- a/dev-docs/docs/@excalidraw/excalidraw/development.mdx +++ b/dev-docs/docs/@excalidraw/excalidraw/development.mdx @@ -13,7 +13,7 @@ To start the example app using the `@excalidraw/excalidraw` package, follow the 1. Install the dependencies ```bash - cd src/packages/excalidraw && yarn + cd packages/excalidraw && yarn ``` 2. Start the example app diff --git a/dev-docs/docs/@excalidraw/excalidraw/faq.mdx b/dev-docs/docs/@excalidraw/excalidraw/faq.mdx index 5c66a603f..e9c6a81d1 100644 --- a/dev-docs/docs/@excalidraw/excalidraw/faq.mdx +++ b/dev-docs/docs/@excalidraw/excalidraw/faq.mdx @@ -39,7 +39,7 @@ Since Vite removes env variables by default, you can update the vite config to e ``` define: { - "process.env.IS_PREACT": process.env.IS_PREACT, + "process.env.IS_PREACT": JSON.stringify("true"), }, ``` diff --git a/dev-docs/docs/@excalidraw/excalidraw/integration.mdx b/dev-docs/docs/@excalidraw/excalidraw/integration.mdx index aa8e002c5..d6bf3fd0d 100644 --- a/dev-docs/docs/@excalidraw/excalidraw/integration.mdx +++ b/dev-docs/docs/@excalidraw/excalidraw/integration.mdx @@ -32,15 +32,9 @@ function App() { ### Next.js -Since _Excalidraw_ doesn't support server side rendering, you should render the component once the host is `mounted`. +Since Excalidraw doesn't support `server side rendering` so it should be rendered only on `client`. The way to achieve this in next.js is using `next.js dynamic import`. -Here are two ways on how you can render **Excalidraw** on **Next.js**. - - - -1. Using **Next.js Dynamic** import [Recommended]. - -Since Excalidraw doesn't support server side rendering so you can also use `dynamic import` to render by setting `ssr` to `false`. +If you want to only import `Excalidraw` component you can do :point_down: ```jsx showLineNumbers import dynamic from "next/dynamic"; @@ -55,25 +49,88 @@ export default function App() { } ``` -Here is a working [demo](https://codesandbox.io/p/sandbox/excalidraw-with-next-dynamic-k8yjq2). +However the above component only works for named component exports. If you want to import some util / constant or something else apart from Excalidraw, then this approach will not work. Instead you can write a wrapper over Excalidraw and import the wrapper dynamically. +If you are using `pages router` then importing the wrapper dynamically would work, where as if you are using `app router` then you will have to also add `useClient` directive on top of the file in addition to dynamically importing the wrapper as shown :point_down: -2. Importing Excalidraw once **client** is rendered. + + -```jsx showLineNumbers -import { useState, useEffect } from "react"; -export default function App() { - const [Excalidraw, setExcalidraw] = useState(null); - useEffect(() => { - import("@excalidraw/excalidraw").then((comp) => - setExcalidraw(comp.Excalidraw), + ```jsx showLineNumbers + "use client"; + import { Excalidraw. convertToExcalidrawElements } from "@excalidraw/excalidraw"; + + import "@excalidraw/excalidraw/index.css"; + + const ExcalidrawWrapper: React.FC = () => { + console.info(convertToExcalidrawElements([{ + type: "rectangle", + id: "rect-1", + width: 186.47265625, + height: 141.9765625, + },])); + return ( +
+
); - }, []); - return <>{Excalidraw && }; -} -``` + }; + export default ExcalidrawWrapper; + ``` + +
+ + + + ```jsx showLineNumbers + import dynamic from "next/dynamic"; + + // Since client components get prerenderd on server as well hence importing + // the excalidraw stuff dynamically with ssr false + + const ExcalidrawWrapper = dynamic( + async () => (await import("../excalidrawWrapper")).default, + { + ssr: false, + }, + ); + + export default function Page() { + return ( + + ); + } + ``` + + + + + ```jsx showLineNumbers + import dynamic from "next/dynamic"; + + // Since client components get prerenderd on server as well hence importing + // the excalidraw stuff dynamically with ssr false + + const ExcalidrawWrapper = dynamic( + async () => (await import("../excalidrawWrapper")).default, + { + ssr: false, + }, + ); + + export default function Page() { + return ( + + ); + } + ``` + + +
+ + +Here is a [source code](https://github.com/excalidraw/excalidraw/tree/master/examples/excalidraw/with-nextjs) for the example with app and pages router. You you can try it out [here](https://excalidraw-package-example-with-nextjs-gh6smrdnq-excalidraw.vercel.app/). -Here is a working [demo](https://codesandbox.io/p/sandbox/excalidraw-with-next-5xb3d) The `types` are available at `@excalidraw/excalidraw/types`, you can view [example for typescript](https://codesandbox.io/s/excalidraw-types-9h2dm) @@ -93,7 +150,7 @@ Since Vite removes env variables by default, you can update the vite config to e ``` define: { - "process.env.IS_PREACT": process.env.IS_PREACT, + "process.env.IS_PREACT": JSON.stringify("true"), }, ``` ::: @@ -148,7 +205,7 @@ import TabItem from "@theme/TabItem";

Excalidraw Embed Example

- + ``` diff --git a/dev-docs/docs/@excalidraw/mermaid-to-excalidraw/codebase/new-diagram-type.mdx b/dev-docs/docs/@excalidraw/mermaid-to-excalidraw/codebase/new-diagram-type.mdx index c59dfaba1..522cf740c 100644 --- a/dev-docs/docs/@excalidraw/mermaid-to-excalidraw/codebase/new-diagram-type.mdx +++ b/dev-docs/docs/@excalidraw/mermaid-to-excalidraw/codebase/new-diagram-type.mdx @@ -38,9 +38,9 @@ Add the diagram type in switch case in [`parseMermaid`](https://github.com/excal ## Writing the Excalidraw Skeleton Convertor -With the completion of previous step, we have all the data, now we need to transform it so to [ExcalidrawElementSkeleton](https://github.com/excalidraw/excalidraw/blob/master/src/data/transform.ts#L133) format. +With the completion of previous step, we have all the data, now we need to transform it so to [ExcalidrawElementSkeleton](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/data/transform.ts#L133) format. -Similar to [`FlowChartToExcalidrawSkeletonConverter`](https://github.com/excalidraw/mermaid-to-excalidraw/blob/master/src/converter/types/flowchart.ts#L24), you have to write the `{{diagramType}}ToExcalidrawSkeletonConverter` which parses the data received in previous step and returns the [ExcalidrawElementSkeleton](https://github.com/excalidraw/excalidraw/blob/master/src/data/transform.ts#L133). +Similar to [`FlowChartToExcalidrawSkeletonConverter`](https://github.com/excalidraw/mermaid-to-excalidraw/blob/master/src/converter/types/flowchart.ts#L24), you have to write the `{{diagramType}}ToExcalidrawSkeletonConverter` which parses the data received in previous step and returns the [ExcalidrawElementSkeleton](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/data/transform.ts#L133). Thats it, you have added the new diagram type 🥳, now lets test it out! diff --git a/dev-docs/docs/@excalidraw/mermaid-to-excalidraw/codebase/parser/flowchart.mdx b/dev-docs/docs/@excalidraw/mermaid-to-excalidraw/codebase/parser/flowchart.mdx index b8d122df3..f85787f4c 100644 --- a/dev-docs/docs/@excalidraw/mermaid-to-excalidraw/codebase/parser/flowchart.mdx +++ b/dev-docs/docs/@excalidraw/mermaid-to-excalidraw/codebase/parser/flowchart.mdx @@ -6,7 +6,7 @@ In this section we will be diving into how the [flowchart parser](https://github ![image](https://github.com/excalidraw/excalidraw/assets/11256141/2a097bbb-64bf-49d6-bf7f-21172bdb538d) -We use `diagram.parser.yy` attribute to parse the data. If you want to know more about how the `diagram.parse.yy` attribute looks like, you can check it [here](https://github.com/mermaid-js/mermaid/blob/00d06c7282a701849793680c1e97da1cfdfcce62/packages/mermaid/src/diagrams/flowchart/flowDb.js#L768), however for scope of flowchart we are using **3** APIs from this parser to compute `vertices`, `edges` and `clusters` as we need these data to transform to [ExcalidrawElementSkeleton](https://github.com/excalidraw/excalidraw/blob/master/src/data/transform.ts#L133C13-L133C38). +We use `diagram.parser.yy` attribute to parse the data. If you want to know more about how the `diagram.parse.yy` attribute looks like, you can check it [here](https://github.com/mermaid-js/mermaid/blob/00d06c7282a701849793680c1e97da1cfdfcce62/packages/mermaid/src/diagrams/flowchart/flowDb.js#L768), however for scope of flowchart we are using **3** APIs from this parser to compute `vertices`, `edges` and `clusters` as we need these data to transform to [ExcalidrawElementSkeleton](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/data/transform.ts#L133C13-L133C38). For computing `vertices` and `edge`s lets consider the below svg generated by mermaid @@ -42,7 +42,7 @@ Considering the same example this is the response from the API } } ``` -The dimensions and position is missing in this response and we need that to transform to [ExcalidrawElementSkeleton](https://github.com/excalidraw/excalidraw/blob/master/src/data/transform.ts#L133C13-L133C38), for this we have our own parser [`parseVertex`](https://github.com/excalidraw/mermaid-to-excalidraw/blob/master/src/parseMermaid.ts#L178) which takes the above response and uses the `svg` together to compute position, dimensions and cleans up the response. +The dimensions and position is missing in this response and we need that to transform to [ExcalidrawElementSkeleton](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/data/transform.ts#L133C13-L133C38), for this we have our own parser [`parseVertex`](https://github.com/excalidraw/mermaid-to-excalidraw/blob/master/src/parseMermaid.ts#L178) which takes the above response and uses the `svg` together to compute position, dimensions and cleans up the response. The final output from `parseVertex` looks like :point_down: diff --git a/dev-docs/docs/@excalidraw/mermaid-to-excalidraw/codebase/parser/parser.mdx b/dev-docs/docs/@excalidraw/mermaid-to-excalidraw/codebase/parser/parser.mdx index 4f4af4dec..7ecc8d05c 100644 --- a/dev-docs/docs/@excalidraw/mermaid-to-excalidraw/codebase/parser/parser.mdx +++ b/dev-docs/docs/@excalidraw/mermaid-to-excalidraw/codebase/parser/parser.mdx @@ -55,11 +55,11 @@ If you want to understand how flowchart parser works, you can navigate to [Flowc ## Converting to ExcalidrawElementSkeleton -Now we have all the data, we just need to transform it to [ExcalidrawElementSkeleton](https://github.com/excalidraw/excalidraw/blob/master/src/data/transform.ts#L133C13-L133C38) API so it can be rendered in Excalidraw. +Now we have all the data, we just need to transform it to [ExcalidrawElementSkeleton](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/data/transform.ts#L133C13-L133C38) API so it can be rendered in Excalidraw. For this we have `converters` which takes the parsed mermaid data and gives back the Excalidraw Skeleton. For Unsupported types, we have already mentioned above that we convert it to `dataURL` and return the ExcalidrawImageSkeleton. -For supported types, currently only flowchart, we have [flowchartConverter](https://github.com/excalidraw/mermaid-to-excalidraw/blob/master/src/converter/types/flowchart.ts#L24) which parses the data and converts to [ExcalidrawElementSkeleton](https://github.com/excalidraw/excalidraw/blob/master/src/data/transform.ts#L133C13-L133C38). +For supported types, currently only flowchart, we have [flowchartConverter](https://github.com/excalidraw/mermaid-to-excalidraw/blob/master/src/converter/types/flowchart.ts#L24) which parses the data and converts to [ExcalidrawElementSkeleton](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/data/transform.ts#L133C13-L133C38). ![image](https://github.com/excalidraw/excalidraw/assets/11256141/00226e9d-043d-4a08-989a-3ad9d2a574f1) \ No newline at end of file diff --git a/dev-docs/docs/introduction/contributing.mdx b/dev-docs/docs/introduction/contributing.mdx index a33cb7a03..1602fd76d 100644 --- a/dev-docs/docs/introduction/contributing.mdx +++ b/dev-docs/docs/introduction/contributing.mdx @@ -52,15 +52,6 @@ Make sure the title starts with a semantic prefix: - **chore**: Other changes that don't modify src or test files - **revert**: Reverts a previous commit -### Changelog - -Add a brief description of your pull request to the changelog located here: [changelog](https://github.com/excalidraw/excalidraw/blob/master/CHANGELOG.md) - -Notes: - -- Make sure to prepend to the section corresponding with the semantic prefix you selected in the title -- Link to your pull request - this will require updating the CHANGELOG _after_ creating the pull request - ### Testing Once you submit your pull request it will automatically be tested. Be sure to check the results of the test and fix any issues that arise. diff --git a/dev-docs/docusaurus.config.js b/dev-docs/docusaurus.config.js index 6b1c0d469..a246522c1 100644 --- a/dev-docs/docusaurus.config.js +++ b/dev-docs/docusaurus.config.js @@ -41,10 +41,7 @@ const config = { showLastUpdateTime: true, }, theme: { - customCss: [ - require.resolve("./src/css/custom.scss"), - require.resolve("../src/packages/excalidraw/example/App.scss"), - ], + customCss: [require.resolve("./src/css/custom.scss")], }, }), ], diff --git a/dev-docs/vercel.json b/dev-docs/vercel.json new file mode 100644 index 000000000..c997f0d02 --- /dev/null +++ b/dev-docs/vercel.json @@ -0,0 +1,4 @@ +{ + "outputDirectory": "build", + "installCommand": "yarn install" +} diff --git a/src/packages/excalidraw/example/App.scss b/examples/excalidraw/components/App.scss similarity index 83% rename from src/packages/excalidraw/example/App.scss rename to examples/excalidraw/components/App.scss index 7f37540d8..e41a77ccc 100644 --- a/src/packages/excalidraw/example/App.scss +++ b/examples/excalidraw/components/App.scss @@ -15,14 +15,23 @@ border-radius: 50%; } } + .app-title { + margin-block-start: 0.83em; + margin-block-end: 0.83em; + } } -.button-wrapper button { - z-index: 1; - height: 40px; - max-width: 200px; - margin: 10px; - padding: 5px; +.button-wrapper { + input[type="checkbox"] { + margin: 5px; + } + button { + z-index: 1; + height: 40px; + max-width: 200px; + margin: 10px; + padding: 5px; + } } .excalidraw .App-menu_top .buttonList { diff --git a/src/packages/excalidraw/example/App.tsx b/examples/excalidraw/components/App.tsx similarity index 83% rename from src/packages/excalidraw/example/App.tsx rename to examples/excalidraw/components/App.tsx index e404f36ea..eea0da6ca 100644 --- a/src/packages/excalidraw/example/App.tsx +++ b/examples/excalidraw/components/App.tsx @@ -1,23 +1,31 @@ -import { useEffect, useState, useRef, useCallback } from "react"; - +import React, { + useEffect, + useState, + useRef, + useCallback, + Children, + cloneElement, +} from "react"; import ExampleSidebar from "./sidebar/ExampleSidebar"; -import type * as TExcalidraw from "../index"; +import type * as TExcalidraw from "@excalidraw/excalidraw"; -import "./App.scss"; -import initialData from "./initialData"; import { nanoid } from "nanoid"; + import { resolvablePromise, ResolvablePromise, + distance2d, + fileOpen, withBatchedUpdates, withBatchedUpdatesThrottled, -} from "../../../utils"; -import { EVENT, ROUNDNESS } from "../../../constants"; -import { distance2d } from "../../../math"; -import { fileOpen } from "../../../data/filesystem"; -import { loadSceneOrLibraryFromBlob } from "../../utils"; -import { +} from "../utils"; + +import CustomFooter from "./CustomFooter"; +import MobileFooter from "./MobileFooter"; +import initialData from "../initialData"; + +import type { AppState, BinaryFileData, ExcalidrawImperativeAPI, @@ -25,18 +33,14 @@ import { Gesture, LibraryItems, PointerDownState as ExcalidrawPointerDownState, -} from "../../../types"; -import { NonDeletedExcalidrawElement, Theme } from "../../../element/types"; -import { ImportedLibraryData } from "../../../data/types"; -import CustomFooter from "./CustomFooter"; -import MobileFooter from "./MobileFooter"; -import { KEYS } from "../../../keys"; +} from "@excalidraw/excalidraw/dist/excalidraw/types"; +import type { + NonDeletedExcalidrawElement, + Theme, +} from "@excalidraw/excalidraw/dist/excalidraw/element/types"; +import type { ImportedLibraryData } from "@excalidraw/excalidraw/dist/excalidraw/data/types"; -declare global { - interface Window { - ExcalidrawLib: typeof TExcalidraw; - } -} +import "./App.scss"; type Comment = { x: number; @@ -57,29 +61,6 @@ type PointerDownState = { }; }; -// This is so that we use the bundled excalidraw.development.js file instead -// of the actual source code -const { - exportToCanvas, - exportToSvg, - exportToBlob, - exportToClipboard, - Excalidraw, - useHandleLibrary, - MIME_TYPES, - sceneCoordsToViewportCoords, - viewportCoordsToSceneCoords, - restoreElements, - Sidebar, - Footer, - WelcomeScreen, - MainMenu, - LiveCollaborationTrigger, - convertToExcalidrawElements, - TTDDialog, - TTDDialogTrigger, -} = window.ExcalidrawLib; - const COMMENT_ICON_DIMENSION = 32; const COMMENT_INPUT_HEIGHT = 50; const COMMENT_INPUT_WIDTH = 150; @@ -88,9 +69,38 @@ export interface AppProps { appTitle: string; useCustom: (api: ExcalidrawImperativeAPI | null, customArgs?: any[]) => void; customArgs?: any[]; + children: React.ReactNode; + excalidrawLib: typeof TExcalidraw; } -export default function App({ appTitle, useCustom, customArgs }: AppProps) { +export default function App({ + appTitle, + useCustom, + customArgs, + children, + excalidrawLib, +}: AppProps) { + const { + exportToCanvas, + exportToSvg, + exportToBlob, + exportToClipboard, + useHandleLibrary, + MIME_TYPES, + sceneCoordsToViewportCoords, + viewportCoordsToSceneCoords, + restoreElements, + Sidebar, + Footer, + WelcomeScreen, + MainMenu, + LiveCollaborationTrigger, + convertToExcalidrawElements, + TTDDialog, + TTDDialogTrigger, + ROUNDNESS, + loadSceneOrLibraryFromBlob, + } = excalidrawLib; const appRef = useRef(null); const [viewModeEnabled, setViewModeEnabled] = useState(false); const [zenModeEnabled, setZenModeEnabled] = useState(false); @@ -152,8 +162,105 @@ export default function App({ appTitle, useCustom, customArgs }: AppProps) { }; }; fetchData(); - }, [excalidrawAPI]); + }, [excalidrawAPI, convertToExcalidrawElements, MIME_TYPES]); + const renderExcalidraw = (children: React.ReactNode) => { + const Excalidraw: any = Children.toArray(children).find( + (child) => + React.isValidElement(child) && + typeof child.type !== "string" && + //@ts-ignore + child.type.displayName === "Excalidraw", + ); + if (!Excalidraw) { + return; + } + const newElement = cloneElement( + Excalidraw, + { + excalidrawAPI: (api: ExcalidrawImperativeAPI) => setExcalidrawAPI(api), + initialData: initialStatePromiseRef.current.promise, + onChange: ( + elements: NonDeletedExcalidrawElement[], + state: AppState, + ) => { + console.info("Elements :", elements, "State : ", state); + }, + onPointerUpdate: (payload: { + pointer: { x: number; y: number }; + button: "down" | "up"; + pointersMap: Gesture["pointers"]; + }) => setPointerData(payload), + viewModeEnabled, + zenModeEnabled, + gridModeEnabled, + theme, + name: "Custom name of drawing", + UIOptions: { + canvasActions: { + loadScene: false, + }, + tools: { image: !disableImageTool }, + }, + renderTopRightUI, + onLinkOpen, + onPointerDown, + onScrollChange: rerenderCommentIcons, + validateEmbeddable: true, + }, + <> + {excalidrawAPI && ( +
+ +
+ )} + + + + + Tab one! + Tab two! + + One + Two + + + + + Toggle Custom Sidebar + + {renderMenu()} + {excalidrawAPI && ( + 😀}> + Text to diagram + + )} + { + console.info("submit"); + // sleep for 2s + await new Promise((resolve) => setTimeout(resolve, 2000)); + throw new Error("error, go away now"); + // return "dummy"; + }} + /> + , + ); + return newElement; + }; const renderTopRightUI = (isMobile: boolean) => { return ( <> @@ -337,8 +444,8 @@ export default function App({ appTitle, useCustom, customArgs }: AppProps) { pointerDownState: PointerDownState, ) => { return withBatchedUpdates((event) => { - window.removeEventListener(EVENT.POINTER_MOVE, pointerDownState.onMove); - window.removeEventListener(EVENT.POINTER_UP, pointerDownState.onUp); + window.removeEventListener("pointermove", pointerDownState.onMove); + window.removeEventListener("pointerup", pointerDownState.onUp); excalidrawAPI?.setActiveTool({ type: "selection" }); const distance = distance2d( pointerDownState.x, @@ -402,8 +509,8 @@ export default function App({ appTitle, useCustom, customArgs }: AppProps) { onPointerMoveFromPointerDownHandler(pointerDownState); const onPointerUp = onPointerUpFromPointerDownHandler(pointerDownState); - window.addEventListener(EVENT.POINTER_MOVE, onPointerMove); - window.addEventListener(EVENT.POINTER_UP, onPointerUp); + window.addEventListener("pointermove", onPointerMove); + window.addEventListener("pointerup", onPointerUp); pointerDownState.onMove = onPointerMove; pointerDownState.onUp = onPointerUp; @@ -495,7 +602,7 @@ export default function App({ appTitle, useCustom, customArgs }: AppProps) { }} onBlur={saveComment} onKeyDown={(event) => { - if (!event.shiftKey && event.key === KEYS.ENTER) { + if (!event.shiftKey && event.key === "Enter") { event.preventDefault(); saveComment(); } @@ -528,7 +635,12 @@ export default function App({ appTitle, useCustom, customArgs }: AppProps) { - {excalidrawAPI && } + {excalidrawAPI && ( + + )} ); }; @@ -677,83 +789,7 @@ export default function App({ appTitle, useCustom, customArgs }: AppProps) {
- - setExcalidrawAPI(api) - } - initialData={initialStatePromiseRef.current.promise} - onChange={(elements, state) => { - // console.info("Elements :", elements, "State : ", state); - }} - onPointerUpdate={(payload: { - pointer: { x: number; y: number }; - button: "down" | "up"; - pointersMap: Gesture["pointers"]; - }) => setPointerData(payload)} - viewModeEnabled={viewModeEnabled} - zenModeEnabled={zenModeEnabled} - gridModeEnabled={gridModeEnabled} - theme={theme} - name="Custom name of drawing" - UIOptions={{ - canvasActions: { - loadScene: false, - }, - tools: { image: !disableImageTool }, - }} - renderTopRightUI={renderTopRightUI} - onLinkOpen={onLinkOpen} - onPointerDown={onPointerDown} - onScrollChange={rerenderCommentIcons} - // allow all urls - validateEmbeddable={true} - > - {excalidrawAPI && ( -
- -
- )} - - - - - Tab one! - Tab two! - - One - Two - - - - - Toggle Custom Sidebar - - {renderMenu()} - {excalidrawAPI && ( - 😀}> - Text to diagram - - )} - { - console.info("submit"); - // sleep for 2s - await new Promise((resolve) => setTimeout(resolve, 2000)); - throw new Error("error, go away now"); - // return "dummy"; - }} - /> -
+ {renderExcalidraw(children)} {Object.keys(commentIcons || []).length > 0 && renderCommentIcons()} {comment && renderComment()}
diff --git a/src/packages/excalidraw/example/CustomFooter.tsx b/examples/excalidraw/components/CustomFooter.tsx similarity index 78% rename from src/packages/excalidraw/example/CustomFooter.tsx rename to examples/excalidraw/components/CustomFooter.tsx index 5c47de6c6..30d51ecf0 100644 --- a/src/packages/excalidraw/example/CustomFooter.tsx +++ b/examples/excalidraw/components/CustomFooter.tsx @@ -1,6 +1,5 @@ -import { ExcalidrawImperativeAPI } from "../../../types"; -import { MIME_TYPES } from "../entry"; -import { Button } from "../../../components/Button"; +import type * as TExcalidraw from "@excalidraw/excalidraw"; +import type { ExcalidrawImperativeAPI } from "@excalidraw/excalidraw/dist/excalidraw/types"; const COMMENT_SVG = ( ); + const CustomFooter = ({ excalidrawAPI, + excalidrawLib, }: { excalidrawAPI: ExcalidrawImperativeAPI; + excalidrawLib: typeof TExcalidraw; }) => { + const { Button, MIME_TYPES } = excalidrawLib; + return ( <> - - + ); }; diff --git a/examples/excalidraw/components/MobileFooter.tsx b/examples/excalidraw/components/MobileFooter.tsx new file mode 100644 index 000000000..7ab62b918 --- /dev/null +++ b/examples/excalidraw/components/MobileFooter.tsx @@ -0,0 +1,27 @@ +import { ExcalidrawImperativeAPI } from "@excalidraw/excalidraw/dist/excalidraw/types"; +import CustomFooter from "./CustomFooter"; +import type * as TExcalidraw from "@excalidraw/excalidraw"; + +const MobileFooter = ({ + excalidrawAPI, + excalidrawLib, +}: { + excalidrawAPI: ExcalidrawImperativeAPI; + excalidrawLib: typeof TExcalidraw; +}) => { + const { useDevice, Footer } = excalidrawLib; + + const device = useDevice(); + if (device.editor.isMobile) { + return ( +
+ +
+ ); + } + return null; +}; +export default MobileFooter; diff --git a/src/packages/excalidraw/example/sidebar/ExampleSidebar.scss b/examples/excalidraw/components/sidebar/ExampleSidebar.scss similarity index 100% rename from src/packages/excalidraw/example/sidebar/ExampleSidebar.scss rename to examples/excalidraw/components/sidebar/ExampleSidebar.scss diff --git a/src/packages/excalidraw/example/sidebar/ExampleSidebar.tsx b/examples/excalidraw/components/sidebar/ExampleSidebar.tsx similarity index 94% rename from src/packages/excalidraw/example/sidebar/ExampleSidebar.tsx rename to examples/excalidraw/components/sidebar/ExampleSidebar.tsx index 4c51ecdc2..8b475f16f 100644 --- a/src/packages/excalidraw/example/sidebar/ExampleSidebar.tsx +++ b/examples/excalidraw/components/sidebar/ExampleSidebar.tsx @@ -1,5 +1,6 @@ -import React, { useState } from "react"; +import { useState } from "react"; import "./ExampleSidebar.scss"; + export default function Sidebar({ children }: { children: React.ReactNode }) { const [open, setOpen] = useState(false); diff --git a/src/packages/excalidraw/example/initialData.tsx b/examples/excalidraw/initialData.tsx similarity index 99% rename from src/packages/excalidraw/example/initialData.tsx rename to examples/excalidraw/initialData.tsx index cca343ae5..3cb5e7af4 100644 --- a/src/packages/excalidraw/example/initialData.tsx +++ b/examples/excalidraw/initialData.tsx @@ -1,5 +1,5 @@ -import { ExcalidrawElementSkeleton } from "../../../data/transform"; -import { FileId } from "../../../element/types"; +import type { ExcalidrawElementSkeleton } from "@excalidraw/excalidraw/data/transform"; +import type { FileId } from "@excalidraw/excalidraw/element/types"; const elements: ExcalidrawElementSkeleton[] = [ { diff --git a/examples/excalidraw/package.json b/examples/excalidraw/package.json new file mode 100644 index 000000000..fe48d5532 --- /dev/null +++ b/examples/excalidraw/package.json @@ -0,0 +1,13 @@ +{ + "name": "examples", + "version": "1.0.0", + "private": true, + "dependencies": { + "react": "18.2.0", + "react-dom": "18.2.0", + "@excalidraw/excalidraw": "*" + }, + "devDependencies": { + "typescript": "^5" + } +} diff --git a/examples/excalidraw/tsconfig.json b/examples/excalidraw/tsconfig.json new file mode 100644 index 000000000..41716a7dd --- /dev/null +++ b/examples/excalidraw/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "../../tsconfig" +} diff --git a/examples/excalidraw/utils.ts b/examples/excalidraw/utils.ts new file mode 100644 index 000000000..822be29b7 --- /dev/null +++ b/examples/excalidraw/utils.ts @@ -0,0 +1,146 @@ +import { unstable_batchedUpdates } from "react-dom"; +import { fileOpen as _fileOpen } from "browser-fs-access"; +import type { MIME_TYPES } from "@excalidraw/excalidraw"; +import { AbortError } from "../../packages/excalidraw/errors"; + +type FILE_EXTENSION = Exclude; + +const INPUT_CHANGE_INTERVAL_MS = 500; + +export type ResolvablePromise = Promise & { + resolve: [T] extends [undefined] ? (value?: T) => void : (value: T) => void; + reject: (error: Error) => void; +}; +export const resolvablePromise = () => { + let resolve!: any; + let reject!: any; + const promise = new Promise((_resolve, _reject) => { + resolve = _resolve; + reject = _reject; + }); + (promise as any).resolve = resolve; + (promise as any).reject = reject; + return promise as ResolvablePromise; +}; + +export const distance2d = (x1: number, y1: number, x2: number, y2: number) => { + const xd = x2 - x1; + const yd = y2 - y1; + return Math.hypot(xd, yd); +}; + +export const fileOpen = (opts: { + extensions?: FILE_EXTENSION[]; + description: string; + multiple?: M; +}): Promise => { + // an unsafe TS hack, alas not much we can do AFAIK + type RetType = M extends false | undefined ? File : File[]; + + const mimeTypes = opts.extensions?.reduce((mimeTypes, type) => { + mimeTypes.push(MIME_TYPES[type]); + + return mimeTypes; + }, [] as string[]); + + const extensions = opts.extensions?.reduce((acc, ext) => { + if (ext === "jpg") { + return acc.concat(".jpg", ".jpeg"); + } + return acc.concat(`.${ext}`); + }, [] as string[]); + + return _fileOpen({ + description: opts.description, + extensions, + mimeTypes, + multiple: opts.multiple ?? false, + legacySetup: (resolve, reject, input) => { + const scheduleRejection = debounce(reject, INPUT_CHANGE_INTERVAL_MS); + const focusHandler = () => { + checkForFile(); + document.addEventListener("keyup", scheduleRejection); + document.addEventListener("pointerup", scheduleRejection); + scheduleRejection(); + }; + const checkForFile = () => { + // this hack might not work when expecting multiple files + if (input.files?.length) { + const ret = opts.multiple ? [...input.files] : input.files[0]; + resolve(ret as RetType); + } + }; + requestAnimationFrame(() => { + window.addEventListener("focus", focusHandler); + }); + const interval = window.setInterval(() => { + checkForFile(); + }, INPUT_CHANGE_INTERVAL_MS); + return (rejectPromise) => { + clearInterval(interval); + scheduleRejection.cancel(); + window.removeEventListener("focus", focusHandler); + document.removeEventListener("keyup", scheduleRejection); + document.removeEventListener("pointerup", scheduleRejection); + if (rejectPromise) { + // so that something is shown in console if we need to debug this + console.warn("Opening the file was canceled (legacy-fs)."); + rejectPromise(new AbortError()); + } + }; + }, + }) as Promise; +}; + +export const debounce = ( + fn: (...args: T) => void, + timeout: number, +) => { + let handle = 0; + let lastArgs: T | null = null; + const ret = (...args: T) => { + lastArgs = args; + clearTimeout(handle); + handle = window.setTimeout(() => { + lastArgs = null; + fn(...args); + }, timeout); + }; + ret.flush = () => { + clearTimeout(handle); + if (lastArgs) { + const _lastArgs = lastArgs; + lastArgs = null; + fn(..._lastArgs); + } + }; + ret.cancel = () => { + lastArgs = null; + clearTimeout(handle); + }; + return ret; +}; + +export const withBatchedUpdates = < + TFunction extends ((event: any) => void) | (() => void), +>( + func: Parameters["length"] extends 0 | 1 ? TFunction : never, +) => + ((event) => { + unstable_batchedUpdates(func as TFunction, event); + }) as TFunction; + +/** + * barches React state updates and throttles the calls to a single call per + * animation frame + */ +export const withBatchedUpdatesThrottled = < + TFunction extends ((event: any) => void) | (() => void), +>( + func: Parameters["length"] extends 0 | 1 ? TFunction : never, +) => { + // @ts-ignore + return throttleRAF>(((event) => { + unstable_batchedUpdates(func, event); + }) as TFunction); +}; diff --git a/examples/excalidraw/with-nextjs/.gitignore b/examples/excalidraw/with-nextjs/.gitignore new file mode 100644 index 000000000..fd3dbb571 --- /dev/null +++ b/examples/excalidraw/with-nextjs/.gitignore @@ -0,0 +1,36 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js +.yarn/install-state.gz + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/examples/excalidraw/with-nextjs/README.md b/examples/excalidraw/with-nextjs/README.md new file mode 100644 index 000000000..9e8d9b96d --- /dev/null +++ b/examples/excalidraw/with-nextjs/README.md @@ -0,0 +1,36 @@ +This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). + +## Getting Started + +First, run the development server: + +```bash +npm run dev +# or +yarn dev +# or +pnpm dev +# or +bun dev +``` + +Open [http://localhost:3000](http://localhost:3005) with your browser to see the result. + +You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. + +This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. + +## Learn More + +To learn more about Next.js, take a look at the following resources: + +- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. +- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. + +You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! + +## Deploy on Vercel + +The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. + +Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. diff --git a/examples/excalidraw/with-nextjs/next.config.js b/examples/excalidraw/with-nextjs/next.config.js new file mode 100644 index 000000000..701438ebf --- /dev/null +++ b/examples/excalidraw/with-nextjs/next.config.js @@ -0,0 +1,12 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + distDir: "build", + typescript: { + // The ts config doesn't work with `jsx: preserve" and if updated to `react-jsx` it gets ovewritten by next js throwing ts errors hence I am ignoring build errors until this is fixed. + ignoreBuildErrors: true, + }, + // This is needed as in pages router the code for importing types throws error as its outside next js app + transpilePackages: ["../"], +}; + +module.exports = nextConfig; diff --git a/examples/excalidraw/with-nextjs/package.json b/examples/excalidraw/with-nextjs/package.json new file mode 100644 index 000000000..177952407 --- /dev/null +++ b/examples/excalidraw/with-nextjs/package.json @@ -0,0 +1,25 @@ +{ + "name": "with-nextjs", + "version": "0.1.0", + "private": true, + "scripts": { + "build:workspace": "yarn workspace @excalidraw/excalidraw run build:esm", + "dev": "yarn build:workspace && next dev -p 3005", + "build": "yarn build:workspace && next build", + "start": "next start -p 3006", + "lint": "next lint" + }, + "dependencies": { + "@excalidraw/excalidraw": "*", + "next": "14.1", + "react": "^18", + "react-dom": "^18" + }, + "devDependencies": { + "@types/node": "^20", + "@types/react": "^18", + "@types/react-dom": "^18", + "path2d-polyfill": "2.0.1", + "typescript": "^5" + } +} diff --git a/src/packages/excalidraw/example/public/images/doremon.png b/examples/excalidraw/with-nextjs/public/images/doremon.png similarity index 100% rename from src/packages/excalidraw/example/public/images/doremon.png rename to examples/excalidraw/with-nextjs/public/images/doremon.png diff --git a/src/packages/excalidraw/example/public/images/excalibot.png b/examples/excalidraw/with-nextjs/public/images/excalibot.png similarity index 100% rename from src/packages/excalidraw/example/public/images/excalibot.png rename to examples/excalidraw/with-nextjs/public/images/excalibot.png diff --git a/src/packages/excalidraw/example/public/images/pika.jpeg b/examples/excalidraw/with-nextjs/public/images/pika.jpeg similarity index 100% rename from src/packages/excalidraw/example/public/images/pika.jpeg rename to examples/excalidraw/with-nextjs/public/images/pika.jpeg diff --git a/src/packages/excalidraw/example/public/images/rocket.jpeg b/examples/excalidraw/with-nextjs/public/images/rocket.jpeg similarity index 100% rename from src/packages/excalidraw/example/public/images/rocket.jpeg rename to examples/excalidraw/with-nextjs/public/images/rocket.jpeg diff --git a/examples/excalidraw/with-nextjs/src/app/favicon.ico b/examples/excalidraw/with-nextjs/src/app/favicon.ico new file mode 100644 index 000000000..718d6fea4 Binary files /dev/null and b/examples/excalidraw/with-nextjs/src/app/favicon.ico differ diff --git a/examples/excalidraw/with-nextjs/src/app/layout.tsx b/examples/excalidraw/with-nextjs/src/app/layout.tsx new file mode 100644 index 000000000..225b6038d --- /dev/null +++ b/examples/excalidraw/with-nextjs/src/app/layout.tsx @@ -0,0 +1,11 @@ +export default function RootLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + + {children} + + ); +} diff --git a/examples/excalidraw/with-nextjs/src/app/page.tsx b/examples/excalidraw/with-nextjs/src/app/page.tsx new file mode 100644 index 000000000..bc8c98fcf --- /dev/null +++ b/examples/excalidraw/with-nextjs/src/app/page.tsx @@ -0,0 +1,23 @@ +import dynamic from "next/dynamic"; +import "../common.scss"; + +// Since client components get prerenderd on server as well hence importing the excalidraw stuff dynamically +// with ssr false +const ExcalidrawWithClientOnly = dynamic( + async () => (await import("../excalidrawWrapper")).default, + { + ssr: false, + }, +); + +export default function Page() { + return ( + <> + Switch to Pages router +

App Router

+ + {/* @ts-expect-error - https://github.com/vercel/next.js/issues/42292 */} + + + ); +} diff --git a/examples/excalidraw/with-nextjs/src/common.scss b/examples/excalidraw/with-nextjs/src/common.scss new file mode 100644 index 000000000..1a77600a9 --- /dev/null +++ b/examples/excalidraw/with-nextjs/src/common.scss @@ -0,0 +1,15 @@ +* { + box-sizing: border-box; + font-family: sans-serif; +} + +a { + color: #1c7ed6; + font-size: 20px; + text-decoration: none; + font-weight: 550; +} + +.page-title { + text-align: center; +} diff --git a/examples/excalidraw/with-nextjs/src/excalidrawWrapper.tsx b/examples/excalidraw/with-nextjs/src/excalidrawWrapper.tsx new file mode 100644 index 000000000..40af9f0cc --- /dev/null +++ b/examples/excalidraw/with-nextjs/src/excalidrawWrapper.tsx @@ -0,0 +1,22 @@ +"use client"; +import * as excalidrawLib from "@excalidraw/excalidraw"; +import { Excalidraw } from "@excalidraw/excalidraw"; +import App from "../../components/App"; + +import "@excalidraw/excalidraw/index.css"; + +const ExcalidrawWrapper: React.FC = () => { + return ( + <> + {}} + excalidrawLib={excalidrawLib} + > + + + + ); +}; + +export default ExcalidrawWrapper; diff --git a/examples/excalidraw/with-nextjs/src/pages/excalidraw-in-pages.tsx b/examples/excalidraw/with-nextjs/src/pages/excalidraw-in-pages.tsx new file mode 100644 index 000000000..527a346b9 --- /dev/null +++ b/examples/excalidraw/with-nextjs/src/pages/excalidraw-in-pages.tsx @@ -0,0 +1,22 @@ +import dynamic from "next/dynamic"; +import "../common.scss"; + +// Since client components get prerenderd on server as well hence importing the excalidraw stuff dynamically +// with ssr false +const Excalidraw = dynamic( + async () => (await import("../excalidrawWrapper")).default, + { + ssr: false, + }, +); + +export default function Page() { + return ( + <> + Switch to App router +

Pages Router

+ {/* @ts-expect-error - https://github.com/vercel/next.js/issues/42292 */} + + + ); +} diff --git a/examples/excalidraw/with-nextjs/tsconfig.json b/examples/excalidraw/with-nextjs/tsconfig.json new file mode 100644 index 000000000..09ae73d2e --- /dev/null +++ b/examples/excalidraw/with-nextjs/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./src/*"] + }, + "forceConsistentCasingInFileNames": true + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "build/types/**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/examples/excalidraw/with-nextjs/vercel.json b/examples/excalidraw/with-nextjs/vercel.json new file mode 100644 index 000000000..bd885f4a5 --- /dev/null +++ b/examples/excalidraw/with-nextjs/vercel.json @@ -0,0 +1,3 @@ +{ + "outputDirectory": "build" +} diff --git a/examples/excalidraw/with-nextjs/yarn.lock b/examples/excalidraw/with-nextjs/yarn.lock new file mode 100644 index 000000000..0072235c0 --- /dev/null +++ b/examples/excalidraw/with-nextjs/yarn.lock @@ -0,0 +1,252 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@excalidraw/excalidraw@workspace:^": + version "0.17.2" + resolved "https://registry.yarnpkg.com/@excalidraw/excalidraw/-/excalidraw-0.17.2.tgz#9a636a1e6bb3c88c5883347d3a7e75e9cce8ab96" + integrity sha512-7pqUWD8+mPjDhF4XxG3gw4rvE2JGaLW3Vss5UZfTbITPxAtFaGEc1K081bncitnaYhUwN9ENJE0i87QB3poDwQ== + +"@next/env@14.0.4": + version "14.0.4" + resolved "https://registry.yarnpkg.com/@next/env/-/env-14.0.4.tgz#d5cda0c4a862d70ae760e58c0cd96a8899a2e49a" + integrity sha512-irQnbMLbUNQpP1wcE5NstJtbuA/69kRfzBrpAD7Gsn8zm/CY6YQYc3HQBz8QPxwISG26tIm5afvvVbu508oBeQ== + +"@next/swc-darwin-arm64@14.0.4": + version "14.0.4" + resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.0.4.tgz#27b1854c2cd04eb1d5e75081a1a792ad91526618" + integrity sha512-mF05E/5uPthWzyYDyptcwHptucf/jj09i2SXBPwNzbgBNc+XnwzrL0U6BmPjQeOL+FiB+iG1gwBeq7mlDjSRPg== + +"@next/swc-darwin-x64@14.0.4": + version "14.0.4" + resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-14.0.4.tgz#9940c449e757d0ee50bb9e792d2600cc08a3eb3b" + integrity sha512-IZQ3C7Bx0k2rYtrZZxKKiusMTM9WWcK5ajyhOZkYYTCc8xytmwSzR1skU7qLgVT/EY9xtXDG0WhY6fyujnI3rw== + +"@next/swc-linux-arm64-gnu@14.0.4": + version "14.0.4" + resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.0.4.tgz#0eafd27c8587f68ace7b4fa80695711a8434de21" + integrity sha512-VwwZKrBQo/MGb1VOrxJ6LrKvbpo7UbROuyMRvQKTFKhNaXjUmKTu7wxVkIuCARAfiI8JpaWAnKR+D6tzpCcM4w== + +"@next/swc-linux-arm64-musl@14.0.4": + version "14.0.4" + resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.0.4.tgz#2b0072adb213f36dada5394ea67d6e82069ae7dd" + integrity sha512-8QftwPEW37XxXoAwsn+nXlodKWHfpMaSvt81W43Wh8dv0gkheD+30ezWMcFGHLI71KiWmHK5PSQbTQGUiidvLQ== + +"@next/swc-linux-x64-gnu@14.0.4": + version "14.0.4" + resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.0.4.tgz#68c67d20ebc8e3f6ced6ff23a4ba2a679dbcec32" + integrity sha512-/s/Pme3VKfZAfISlYVq2hzFS8AcAIOTnoKupc/j4WlvF6GQ0VouS2Q2KEgPuO1eMBwakWPB1aYFIA4VNVh667A== + +"@next/swc-linux-x64-musl@14.0.4": + version "14.0.4" + resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.0.4.tgz#67cd81b42fb2caf313f7992fcf6d978af55a1247" + integrity sha512-m8z/6Fyal4L9Bnlxde5g2Mfa1Z7dasMQyhEhskDATpqr+Y0mjOBZcXQ7G5U+vgL22cI4T7MfvgtrM2jdopqWaw== + +"@next/swc-win32-arm64-msvc@14.0.4": + version "14.0.4" + resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.0.4.tgz#be06585906b195d755ceda28f33c633e1443f1a3" + integrity sha512-7Wv4PRiWIAWbm5XrGz3D8HUkCVDMMz9igffZG4NB1p4u1KoItwx9qjATHz88kwCEal/HXmbShucaslXCQXUM5w== + +"@next/swc-win32-ia32-msvc@14.0.4": + version "14.0.4" + resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.0.4.tgz#e76cabefa9f2d891599c3d85928475bd8d3f6600" + integrity sha512-zLeNEAPULsl0phfGb4kdzF/cAVIfaC7hY+kt0/d+y9mzcZHsMS3hAS829WbJ31DkSlVKQeHEjZHIdhN+Pg7Gyg== + +"@next/swc-win32-x64-msvc@14.0.4": + version "14.0.4" + resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.0.4.tgz#e74892f1a9ccf41d3bf5979ad6d3d77c07b9cba1" + integrity sha512-yEh2+R8qDlDCjxVpzOTEpBLQTEFAcP2A8fUFLaWNap9GitYKkKv1//y2S6XY6zsR4rCOPRpU7plYDR+az2n30A== + +"@swc/helpers@0.5.2": + version "0.5.2" + resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.5.2.tgz#85ea0c76450b61ad7d10a37050289eded783c27d" + integrity sha512-E4KcWTpoLHqwPHLxidpOqQbcrZVgi0rsmmZXUle1jXmJfuIf/UWpczUJ7MZZ5tlxytgJXyp0w4PGkkeLiuIdZw== + dependencies: + tslib "^2.4.0" + +"@types/node@^20": + version "20.11.0" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.0.tgz#8e0b99e70c0c1ade1a86c4a282f7b7ef87c9552f" + integrity sha512-o9bjXmDNcF7GbM4CNQpmi+TutCgap/K3w1JyKgxAjqx41zp9qlIAVFi0IhCNsJcXolEqLWhbFbEeL0PvYm4pcQ== + dependencies: + undici-types "~5.26.4" + +"@types/prop-types@*": + version "15.7.11" + resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.11.tgz#2596fb352ee96a1379c657734d4b913a613ad563" + integrity sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng== + +"@types/react-dom@^18": + version "18.2.18" + resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.2.18.tgz#16946e6cd43971256d874bc3d0a72074bb8571dd" + integrity sha512-TJxDm6OfAX2KJWJdMEVTwWke5Sc/E/RlnPGvGfS0W7+6ocy2xhDVQVh/KvC2Uf7kACs+gDytdusDSdWfWkaNzw== + dependencies: + "@types/react" "*" + +"@types/react@*", "@types/react@^18": + version "18.2.47" + resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.47.tgz#85074b27ab563df01fbc3f68dc64bf7050b0af40" + integrity sha512-xquNkkOirwyCgoClNk85BjP+aqnIS+ckAJ8i37gAbDs14jfW/J23f2GItAf33oiUPQnqNMALiFeoM9Y5mbjpVQ== + dependencies: + "@types/prop-types" "*" + "@types/scheduler" "*" + csstype "^3.0.2" + +"@types/scheduler@*": + version "0.16.8" + resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.8.tgz#ce5ace04cfeabe7ef87c0091e50752e36707deff" + integrity sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A== + +busboy@1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/busboy/-/busboy-1.6.0.tgz#966ea36a9502e43cdb9146962523b92f531f6893" + integrity sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA== + dependencies: + streamsearch "^1.1.0" + +caniuse-lite@^1.0.30001406: + version "1.0.30001576" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001576.tgz#893be772cf8ee6056d6c1e2d07df365b9ec0a5c4" + integrity sha512-ff5BdakGe2P3SQsMsiqmt1Lc8221NR1VzHj5jXN5vBny9A6fpze94HiVV/n7XRosOlsShJcvMv5mdnpjOGCEgg== + +client-only@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/client-only/-/client-only-0.0.1.tgz#38bba5d403c41ab150bff64a95c85013cf73bca1" + integrity sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA== + +csstype@^3.0.2: + version "3.1.3" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81" + integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw== + +glob-to-regexp@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" + integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== + +graceful-fs@^4.1.2, graceful-fs@^4.2.11: + version "4.2.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" + integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== + +"js-tokens@^3.0.0 || ^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + +loose-envify@^1.1.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" + integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== + dependencies: + js-tokens "^3.0.0 || ^4.0.0" + +nanoid@^3.3.6: + version "3.3.7" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8" + integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g== + +next@14.0.4: + version "14.0.4" + resolved "https://registry.yarnpkg.com/next/-/next-14.0.4.tgz#bf00b6f835b20d10a5057838fa2dfced1d0d84dc" + integrity sha512-qbwypnM7327SadwFtxXnQdGiKpkuhaRLE2uq62/nRul9cj9KhQ5LhHmlziTNqUidZotw/Q1I9OjirBROdUJNgA== + dependencies: + "@next/env" "14.0.4" + "@swc/helpers" "0.5.2" + busboy "1.6.0" + caniuse-lite "^1.0.30001406" + graceful-fs "^4.2.11" + postcss "8.4.31" + styled-jsx "5.1.1" + watchpack "2.4.0" + optionalDependencies: + "@next/swc-darwin-arm64" "14.0.4" + "@next/swc-darwin-x64" "14.0.4" + "@next/swc-linux-arm64-gnu" "14.0.4" + "@next/swc-linux-arm64-musl" "14.0.4" + "@next/swc-linux-x64-gnu" "14.0.4" + "@next/swc-linux-x64-musl" "14.0.4" + "@next/swc-win32-arm64-msvc" "14.0.4" + "@next/swc-win32-ia32-msvc" "14.0.4" + "@next/swc-win32-x64-msvc" "14.0.4" + +path2d-polyfill@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/path2d-polyfill/-/path2d-polyfill-2.0.1.tgz#24c554a738f42700d6961992bf5f1049672f2391" + integrity sha512-ad/3bsalbbWhmBo0D6FZ4RNMwsLsPpL6gnvhuSaU5Vm7b06Kr5ubSltQQ0T7YKsiJQO+g22zJ4dJKNTXIyOXtA== + +picocolors@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" + integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== + +postcss@8.4.31: + version "8.4.31" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.31.tgz#92b451050a9f914da6755af352bdc0192508656d" + integrity sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ== + dependencies: + nanoid "^3.3.6" + picocolors "^1.0.0" + source-map-js "^1.0.2" + +react-dom@^18: + version "18.2.0" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d" + integrity sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g== + dependencies: + loose-envify "^1.1.0" + scheduler "^0.23.0" + +react@^18: + version "18.2.0" + resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5" + integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ== + dependencies: + loose-envify "^1.1.0" + +scheduler@^0.23.0: + version "0.23.0" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.0.tgz#ba8041afc3d30eb206a487b6b384002e4e61fdfe" + integrity sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw== + dependencies: + loose-envify "^1.1.0" + +source-map-js@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" + integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== + +streamsearch@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-1.1.0.tgz#404dd1e2247ca94af554e841a8ef0eaa238da764" + integrity sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg== + +styled-jsx@5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/styled-jsx/-/styled-jsx-5.1.1.tgz#839a1c3aaacc4e735fed0781b8619ea5d0009d1f" + integrity sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw== + dependencies: + client-only "0.0.1" + +tslib@^2.4.0: + version "2.6.2" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" + integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== + +typescript@^5: + version "5.3.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.3.3.tgz#b3ce6ba258e72e6305ba66f5c9b452aaee3ffe37" + integrity sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw== + +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== + +watchpack@2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.0.tgz#fa33032374962c78113f93c7f2fb4c54c9862a5d" + integrity sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg== + dependencies: + glob-to-regexp "^0.4.1" + graceful-fs "^4.1.2" diff --git a/src/packages/excalidraw/example/public/index.html b/examples/excalidraw/with-script-in-browser/index.html similarity index 65% rename from src/packages/excalidraw/example/public/index.html rename to examples/excalidraw/with-script-in-browser/index.html index 7f4ed2494..a56d7f421 100644 --- a/src/packages/excalidraw/example/public/index.html +++ b/examples/excalidraw/with-script-in-browser/index.html @@ -12,18 +12,21 @@ +
- - - + + console.log(ExcalidrawLib); + window.ExcalidrawLib = ExcalidrawLib; + + diff --git a/examples/excalidraw/with-script-in-browser/index.tsx b/examples/excalidraw/with-script-in-browser/index.tsx new file mode 100644 index 000000000..e8584d7ca --- /dev/null +++ b/examples/excalidraw/with-script-in-browser/index.tsx @@ -0,0 +1,28 @@ +import App from "../components/App"; +import React, { StrictMode } from "react"; +import { createRoot } from "react-dom/client"; + +import type * as TExcalidraw from "@excalidraw/excalidraw"; + +import "@excalidraw/excalidraw/index.css"; + +declare global { + interface Window { + ExcalidrawLib: typeof TExcalidraw; + } +} + +const rootElement = document.getElementById("root")!; +const root = createRoot(rootElement); +const { Excalidraw } = window.ExcalidrawLib; +root.render( + + {}} + excalidrawLib={window.ExcalidrawLib} + > + + + , +); diff --git a/examples/excalidraw/with-script-in-browser/package.json b/examples/excalidraw/with-script-in-browser/package.json new file mode 100644 index 000000000..d721ac162 --- /dev/null +++ b/examples/excalidraw/with-script-in-browser/package.json @@ -0,0 +1,19 @@ +{ + "name": "with-script-in-browser", + "version": "1.0.0", + "private": true, + "dependencies": { + "react": "18.2.0", + "react-dom": "18.2.0", + "@excalidraw/excalidraw": "*" + }, + "devDependencies": { + "vite": "5.0.12", + "typescript": "^5" + }, + "scripts": { + "start": "yarn workspace @excalidraw/excalidraw run build:esm && vite", + "build": "yarn workspace @excalidraw/excalidraw run build:esm && vite build", + "build:preview": "yarn build && vite preview --port 5002" + } +} diff --git a/examples/excalidraw/with-script-in-browser/public/images/doremon.png b/examples/excalidraw/with-script-in-browser/public/images/doremon.png new file mode 100644 index 000000000..36208a466 Binary files /dev/null and b/examples/excalidraw/with-script-in-browser/public/images/doremon.png differ diff --git a/examples/excalidraw/with-script-in-browser/public/images/excalibot.png b/examples/excalidraw/with-script-in-browser/public/images/excalibot.png new file mode 100644 index 000000000..7928ec325 Binary files /dev/null and b/examples/excalidraw/with-script-in-browser/public/images/excalibot.png differ diff --git a/examples/excalidraw/with-script-in-browser/public/images/pika.jpeg b/examples/excalidraw/with-script-in-browser/public/images/pika.jpeg new file mode 100644 index 000000000..455ed52a6 Binary files /dev/null and b/examples/excalidraw/with-script-in-browser/public/images/pika.jpeg differ diff --git a/examples/excalidraw/with-script-in-browser/public/images/rocket.jpeg b/examples/excalidraw/with-script-in-browser/public/images/rocket.jpeg new file mode 100644 index 000000000..f17a74bd6 Binary files /dev/null and b/examples/excalidraw/with-script-in-browser/public/images/rocket.jpeg differ diff --git a/examples/excalidraw/with-script-in-browser/vercel.json b/examples/excalidraw/with-script-in-browser/vercel.json new file mode 100644 index 000000000..139f31ef0 --- /dev/null +++ b/examples/excalidraw/with-script-in-browser/vercel.json @@ -0,0 +1,4 @@ +{ + "outputDirectory": "dist", + "installCommand": "yarn install" +} diff --git a/examples/excalidraw/with-script-in-browser/vite.config.mts b/examples/excalidraw/with-script-in-browser/vite.config.mts new file mode 100644 index 000000000..e2e5e19ac --- /dev/null +++ b/examples/excalidraw/with-script-in-browser/vite.config.mts @@ -0,0 +1,11 @@ +import { defineConfig } from "vite"; + +// https://vitejs.dev/config/ +export default defineConfig({ + server: { + port: 3001, + // open the browser + open: true, + }, + publicDir: "public", +}); diff --git a/examples/excalidraw/yarn.lock b/examples/excalidraw/yarn.lock new file mode 100644 index 000000000..1eb584205 --- /dev/null +++ b/examples/excalidraw/yarn.lock @@ -0,0 +1,313 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@esbuild/aix-ppc64@0.19.11": + version "0.19.11" + resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.19.11.tgz#2acd20be6d4f0458bc8c784103495ff24f13b1d3" + integrity sha512-FnzU0LyE3ySQk7UntJO4+qIiQgI7KoODnZg5xzXIrFJlKd2P2gwHsHY4927xj9y5PJmJSzULiUCWmv7iWnNa7g== + +"@esbuild/android-arm64@0.19.11": + version "0.19.11" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.19.11.tgz#b45d000017385c9051a4f03e17078abb935be220" + integrity sha512-aiu7K/5JnLj//KOnOfEZ0D90obUkRzDMyqd/wNAUQ34m4YUPVhRZpnqKV9uqDGxT7cToSDnIHsGooyIczu9T+Q== + +"@esbuild/android-arm@0.19.11": + version "0.19.11" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.19.11.tgz#f46f55414e1c3614ac682b29977792131238164c" + integrity sha512-5OVapq0ClabvKvQ58Bws8+wkLCV+Rxg7tUVbo9xu034Nm536QTII4YzhaFriQ7rMrorfnFKUsArD2lqKbFY4vw== + +"@esbuild/android-x64@0.19.11": + version "0.19.11" + resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.19.11.tgz#bfc01e91740b82011ef503c48f548950824922b2" + integrity sha512-eccxjlfGw43WYoY9QgB82SgGgDbibcqyDTlk3l3C0jOVHKxrjdc9CTwDUQd0vkvYg5um0OH+GpxYvp39r+IPOg== + +"@esbuild/darwin-arm64@0.19.11": + version "0.19.11" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.19.11.tgz#533fb7f5a08c37121d82c66198263dcc1bed29bf" + integrity sha512-ETp87DRWuSt9KdDVkqSoKoLFHYTrkyz2+65fj9nfXsaV3bMhTCjtQfw3y+um88vGRKRiF7erPrh/ZuIdLUIVxQ== + +"@esbuild/darwin-x64@0.19.11": + version "0.19.11" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.19.11.tgz#62f3819eff7e4ddc656b7c6815a31cf9a1e7d98e" + integrity sha512-fkFUiS6IUK9WYUO/+22omwetaSNl5/A8giXvQlcinLIjVkxwTLSktbF5f/kJMftM2MJp9+fXqZ5ezS7+SALp4g== + +"@esbuild/freebsd-arm64@0.19.11": + version "0.19.11" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.11.tgz#d478b4195aa3ca44160272dab85ef8baf4175b4a" + integrity sha512-lhoSp5K6bxKRNdXUtHoNc5HhbXVCS8V0iZmDvyWvYq9S5WSfTIHU2UGjcGt7UeS6iEYp9eeymIl5mJBn0yiuxA== + +"@esbuild/freebsd-x64@0.19.11": + version "0.19.11" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.19.11.tgz#7bdcc1917409178257ca6a1a27fe06e797ec18a2" + integrity sha512-JkUqn44AffGXitVI6/AbQdoYAq0TEullFdqcMY/PCUZ36xJ9ZJRtQabzMA+Vi7r78+25ZIBosLTOKnUXBSi1Kw== + +"@esbuild/linux-arm64@0.19.11": + version "0.19.11" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.19.11.tgz#58ad4ff11685fcc735d7ff4ca759ab18fcfe4545" + integrity sha512-LneLg3ypEeveBSMuoa0kwMpCGmpu8XQUh+mL8XXwoYZ6Be2qBnVtcDI5azSvh7vioMDhoJFZzp9GWp9IWpYoUg== + +"@esbuild/linux-arm@0.19.11": + version "0.19.11" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.19.11.tgz#ce82246d873b5534d34de1e5c1b33026f35e60e3" + integrity sha512-3CRkr9+vCV2XJbjwgzjPtO8T0SZUmRZla+UL1jw+XqHZPkPgZiyWvbDvl9rqAN8Zl7qJF0O/9ycMtjU67HN9/Q== + +"@esbuild/linux-ia32@0.19.11": + version "0.19.11" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.19.11.tgz#cbae1f313209affc74b80f4390c4c35c6ab83fa4" + integrity sha512-caHy++CsD8Bgq2V5CodbJjFPEiDPq8JJmBdeyZ8GWVQMjRD0sU548nNdwPNvKjVpamYYVL40AORekgfIubwHoA== + +"@esbuild/linux-loong64@0.19.11": + version "0.19.11" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.19.11.tgz#5f32aead1c3ec8f4cccdb7ed08b166224d4e9121" + integrity sha512-ppZSSLVpPrwHccvC6nQVZaSHlFsvCQyjnvirnVjbKSHuE5N24Yl8F3UwYUUR1UEPaFObGD2tSvVKbvR+uT1Nrg== + +"@esbuild/linux-mips64el@0.19.11": + version "0.19.11" + resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.19.11.tgz#38eecf1cbb8c36a616261de858b3c10d03419af9" + integrity sha512-B5x9j0OgjG+v1dF2DkH34lr+7Gmv0kzX6/V0afF41FkPMMqaQ77pH7CrhWeR22aEeHKaeZVtZ6yFwlxOKPVFyg== + +"@esbuild/linux-ppc64@0.19.11": + version "0.19.11" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.19.11.tgz#9c5725a94e6ec15b93195e5a6afb821628afd912" + integrity sha512-MHrZYLeCG8vXblMetWyttkdVRjQlQUb/oMgBNurVEnhj4YWOr4G5lmBfZjHYQHHN0g6yDmCAQRR8MUHldvvRDA== + +"@esbuild/linux-riscv64@0.19.11": + version "0.19.11" + resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.19.11.tgz#2dc4486d474a2a62bbe5870522a9a600e2acb916" + integrity sha512-f3DY++t94uVg141dozDu4CCUkYW+09rWtaWfnb3bqe4w5NqmZd6nPVBm+qbz7WaHZCoqXqHz5p6CM6qv3qnSSQ== + +"@esbuild/linux-s390x@0.19.11": + version "0.19.11" + resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.19.11.tgz#4ad8567df48f7dd4c71ec5b1753b6f37561a65a8" + integrity sha512-A5xdUoyWJHMMlcSMcPGVLzYzpcY8QP1RtYzX5/bS4dvjBGVxdhuiYyFwp7z74ocV7WDc0n1harxmpq2ePOjI0Q== + +"@esbuild/linux-x64@0.19.11": + version "0.19.11" + resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.19.11.tgz#b7390c4d5184f203ebe7ddaedf073df82a658766" + integrity sha512-grbyMlVCvJSfxFQUndw5mCtWs5LO1gUlwP4CDi4iJBbVpZcqLVT29FxgGuBJGSzyOxotFG4LoO5X+M1350zmPA== + +"@esbuild/netbsd-x64@0.19.11": + version "0.19.11" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.19.11.tgz#d633c09492a1721377f3bccedb2d821b911e813d" + integrity sha512-13jvrQZJc3P230OhU8xgwUnDeuC/9egsjTkXN49b3GcS5BKvJqZn86aGM8W9pd14Kd+u7HuFBMVtrNGhh6fHEQ== + +"@esbuild/openbsd-x64@0.19.11": + version "0.19.11" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.19.11.tgz#17388c76e2f01125bf831a68c03a7ffccb65d1a2" + integrity sha512-ysyOGZuTp6SNKPE11INDUeFVVQFrhcNDVUgSQVDzqsqX38DjhPEPATpid04LCoUr2WXhQTEZ8ct/EgJCUDpyNw== + +"@esbuild/sunos-x64@0.19.11": + version "0.19.11" + resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.19.11.tgz#e320636f00bb9f4fdf3a80e548cb743370d41767" + integrity sha512-Hf+Sad9nVwvtxy4DXCZQqLpgmRTQqyFyhT3bZ4F2XlJCjxGmRFF0Shwn9rzhOYRB61w9VMXUkxlBy56dk9JJiQ== + +"@esbuild/win32-arm64@0.19.11": + version "0.19.11" + resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.19.11.tgz#c778b45a496e90b6fc373e2a2bb072f1441fe0ee" + integrity sha512-0P58Sbi0LctOMOQbpEOvOL44Ne0sqbS0XWHMvvrg6NE5jQ1xguCSSw9jQeUk2lfrXYsKDdOe6K+oZiwKPilYPQ== + +"@esbuild/win32-ia32@0.19.11": + version "0.19.11" + resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.19.11.tgz#481a65fee2e5cce74ec44823e6b09ecedcc5194c" + integrity sha512-6YOrWS+sDJDmshdBIQU+Uoyh7pQKrdykdefC1avn76ss5c+RN6gut3LZA4E2cH5xUEp5/cA0+YxRaVtRAb0xBg== + +"@esbuild/win32-x64@0.19.11": + version "0.19.11" + resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.19.11.tgz#a5d300008960bb39677c46bf16f53ec70d8dee04" + integrity sha512-vfkhltrjCAb603XaFhqhAF4LGDi2M4OrCRrFusyQ+iTLQ/o60QQXxc9cZC/FFpihBI9N1Grn6SMKVJ4KP7Fuiw== + +"@rollup/rollup-android-arm-eabi@4.9.5": + version "4.9.5" + resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.9.5.tgz#b752b6c88a14ccfcbdf3f48c577ccc3a7f0e66b9" + integrity sha512-idWaG8xeSRCfRq9KpRysDHJ/rEHBEXcHuJ82XY0yYFIWnLMjZv9vF/7DOq8djQ2n3Lk6+3qfSH8AqlmHlmi1MA== + +"@rollup/rollup-android-arm64@4.9.5": + version "4.9.5" + resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.9.5.tgz#33757c3a448b9ef77b6f6292d8b0ec45c87e9c1a" + integrity sha512-f14d7uhAMtsCGjAYwZGv6TwuS3IFaM4ZnGMUn3aCBgkcHAYErhV1Ad97WzBvS2o0aaDv4mVz+syiN0ElMyfBPg== + +"@rollup/rollup-darwin-arm64@4.9.5": + version "4.9.5" + resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.9.5.tgz#5234ba62665a3f443143bc8bcea9df2cc58f55fb" + integrity sha512-ndoXeLx455FffL68OIUrVr89Xu1WLzAG4n65R8roDlCoYiQcGGg6MALvs2Ap9zs7AHg8mpHtMpwC8jBBjZrT/w== + +"@rollup/rollup-darwin-x64@4.9.5": + version "4.9.5" + resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.9.5.tgz#981256c054d3247b83313724938d606798a919d1" + integrity sha512-UmElV1OY2m/1KEEqTlIjieKfVwRg0Zwg4PLgNf0s3glAHXBN99KLpw5A5lrSYCa1Kp63czTpVll2MAqbZYIHoA== + +"@rollup/rollup-linux-arm-gnueabihf@4.9.5": + version "4.9.5" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.9.5.tgz#120678a5a2b3a283a548dbb4d337f9187a793560" + integrity sha512-Q0LcU61v92tQB6ae+udZvOyZ0wfpGojtAKrrpAaIqmJ7+psq4cMIhT/9lfV6UQIpeItnq/2QDROhNLo00lOD1g== + +"@rollup/rollup-linux-arm64-gnu@4.9.5": + version "4.9.5" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.9.5.tgz#c99d857e2372ece544b6f60b85058ad259f64114" + integrity sha512-dkRscpM+RrR2Ee3eOQmRWFjmV/payHEOrjyq1VZegRUa5OrZJ2MAxBNs05bZuY0YCtpqETDy1Ix4i/hRqX98cA== + +"@rollup/rollup-linux-arm64-musl@4.9.5": + version "4.9.5" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.9.5.tgz#3064060f568a5718c2a06858cd6e6d24f2ff8632" + integrity sha512-QaKFVOzzST2xzY4MAmiDmURagWLFh+zZtttuEnuNn19AiZ0T3fhPyjPPGwLNdiDT82ZE91hnfJsUiDwF9DClIQ== + +"@rollup/rollup-linux-riscv64-gnu@4.9.5": + version "4.9.5" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.9.5.tgz#987d30b5d2b992fff07d055015991a57ff55fbad" + integrity sha512-HeGqmRJuyVg6/X6MpE2ur7GbymBPS8Np0S/vQFHDmocfORT+Zt76qu+69NUoxXzGqVP1pzaY6QIi0FJWLC3OPA== + +"@rollup/rollup-linux-x64-gnu@4.9.5": + version "4.9.5" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.9.5.tgz#85946ee4d068bd12197aeeec2c6f679c94978a49" + integrity sha512-Dq1bqBdLaZ1Gb/l2e5/+o3B18+8TI9ANlA1SkejZqDgdU/jK/ThYaMPMJpVMMXy2uRHvGKbkz9vheVGdq3cJfA== + +"@rollup/rollup-linux-x64-musl@4.9.5": + version "4.9.5" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.9.5.tgz#fe0b20f9749a60eb1df43d20effa96c756ddcbd4" + integrity sha512-ezyFUOwldYpj7AbkwyW9AJ203peub81CaAIVvckdkyH8EvhEIoKzaMFJj0G4qYJ5sw3BpqhFrsCc30t54HV8vg== + +"@rollup/rollup-win32-arm64-msvc@4.9.5": + version "4.9.5" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.9.5.tgz#422661ef0e16699a234465d15b2c1089ef963b2a" + integrity sha512-aHSsMnUw+0UETB0Hlv7B/ZHOGY5bQdwMKJSzGfDfvyhnpmVxLMGnQPGNE9wgqkLUs3+gbG1Qx02S2LLfJ5GaRQ== + +"@rollup/rollup-win32-ia32-msvc@4.9.5": + version "4.9.5" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.9.5.tgz#7b73a145891c202fbcc08759248983667a035d85" + integrity sha512-AiqiLkb9KSf7Lj/o1U3SEP9Zn+5NuVKgFdRIZkvd4N0+bYrTOovVd0+LmYCPQGbocT4kvFyK+LXCDiXPBF3fyA== + +"@rollup/rollup-win32-x64-msvc@4.9.5": + version "4.9.5" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.9.5.tgz#10491ccf4f63c814d4149e0316541476ea603602" + integrity sha512-1q+mykKE3Vot1kaFJIDoUFv5TuW+QQVaf2FmTT9krg86pQrGStOSJJ0Zil7CFagyxDuouTepzt5Y5TVzyajOdQ== + +"@types/estree@1.0.5": + version "1.0.5" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4" + integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw== + +esbuild@^0.19.3: + version "0.19.11" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.19.11.tgz#4a02dca031e768b5556606e1b468fe72e3325d96" + integrity sha512-HJ96Hev2hX/6i5cDVwcqiJBBtuo9+FeIJOtZ9W1kA5M6AMJRHUZlpYZ1/SbEwtO0ioNAW8rUooVpC/WehY2SfA== + optionalDependencies: + "@esbuild/aix-ppc64" "0.19.11" + "@esbuild/android-arm" "0.19.11" + "@esbuild/android-arm64" "0.19.11" + "@esbuild/android-x64" "0.19.11" + "@esbuild/darwin-arm64" "0.19.11" + "@esbuild/darwin-x64" "0.19.11" + "@esbuild/freebsd-arm64" "0.19.11" + "@esbuild/freebsd-x64" "0.19.11" + "@esbuild/linux-arm" "0.19.11" + "@esbuild/linux-arm64" "0.19.11" + "@esbuild/linux-ia32" "0.19.11" + "@esbuild/linux-loong64" "0.19.11" + "@esbuild/linux-mips64el" "0.19.11" + "@esbuild/linux-ppc64" "0.19.11" + "@esbuild/linux-riscv64" "0.19.11" + "@esbuild/linux-s390x" "0.19.11" + "@esbuild/linux-x64" "0.19.11" + "@esbuild/netbsd-x64" "0.19.11" + "@esbuild/openbsd-x64" "0.19.11" + "@esbuild/sunos-x64" "0.19.11" + "@esbuild/win32-arm64" "0.19.11" + "@esbuild/win32-ia32" "0.19.11" + "@esbuild/win32-x64" "0.19.11" + +fsevents@~2.3.2, fsevents@~2.3.3: + version "2.3.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" + integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== + +"js-tokens@^3.0.0 || ^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + +loose-envify@^1.1.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" + integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== + dependencies: + js-tokens "^3.0.0 || ^4.0.0" + +nanoid@^3.3.7: + version "3.3.7" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8" + integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g== + +picocolors@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" + integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== + +postcss@^8.4.32: + version "8.4.33" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.33.tgz#1378e859c9f69bf6f638b990a0212f43e2aaa742" + integrity sha512-Kkpbhhdjw2qQs2O2DGX+8m5OVqEcbB9HRBvuYM9pgrjEFUg30A9LmXNlTAUj4S9kgtGyrMbTzVjH7E+s5Re2yg== + dependencies: + nanoid "^3.3.7" + picocolors "^1.0.0" + source-map-js "^1.0.2" + +react-dom@18.2.0: + version "18.2.0" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d" + integrity sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g== + dependencies: + loose-envify "^1.1.0" + scheduler "^0.23.0" + +react@18.2.0: + version "18.2.0" + resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5" + integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ== + dependencies: + loose-envify "^1.1.0" + +rollup@^4.2.0: + version "4.9.5" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.9.5.tgz#62999462c90f4c8b5d7c38fc7161e63b29101b05" + integrity sha512-E4vQW0H/mbNMw2yLSqJyjtkHY9dslf/p0zuT1xehNRqUTBOFMqEjguDvqhXr7N7r/4ttb2jr4T41d3dncmIgbQ== + dependencies: + "@types/estree" "1.0.5" + optionalDependencies: + "@rollup/rollup-android-arm-eabi" "4.9.5" + "@rollup/rollup-android-arm64" "4.9.5" + "@rollup/rollup-darwin-arm64" "4.9.5" + "@rollup/rollup-darwin-x64" "4.9.5" + "@rollup/rollup-linux-arm-gnueabihf" "4.9.5" + "@rollup/rollup-linux-arm64-gnu" "4.9.5" + "@rollup/rollup-linux-arm64-musl" "4.9.5" + "@rollup/rollup-linux-riscv64-gnu" "4.9.5" + "@rollup/rollup-linux-x64-gnu" "4.9.5" + "@rollup/rollup-linux-x64-musl" "4.9.5" + "@rollup/rollup-win32-arm64-msvc" "4.9.5" + "@rollup/rollup-win32-ia32-msvc" "4.9.5" + "@rollup/rollup-win32-x64-msvc" "4.9.5" + fsevents "~2.3.2" + +scheduler@^0.23.0: + version "0.23.0" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.0.tgz#ba8041afc3d30eb206a487b6b384002e4e61fdfe" + integrity sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw== + dependencies: + loose-envify "^1.1.0" + +source-map-js@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" + integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== + +vite@5.0.6: + version "5.0.6" + resolved "https://registry.yarnpkg.com/vite/-/vite-5.0.6.tgz#f9e13503a4c5ccd67312c67803dec921f3bdea7c" + integrity sha512-MD3joyAEBtV7QZPl2JVVUai6zHms3YOmLR+BpMzLlX2Yzjfcc4gTgNi09d/Rua3F4EtC8zdwPU8eQYyib4vVMQ== + dependencies: + esbuild "^0.19.3" + postcss "^8.4.32" + rollup "^4.2.0" + optionalDependencies: + fsevents "~2.3.3" diff --git a/excalidraw-app/App.tsx b/excalidraw-app/App.tsx new file mode 100644 index 000000000..e7083ca9e --- /dev/null +++ b/excalidraw-app/App.tsx @@ -0,0 +1,900 @@ +import polyfill from "../packages/excalidraw/polyfill"; +import LanguageDetector from "i18next-browser-languagedetector"; +import { useCallback, useEffect, useRef, useState } from "react"; +import { trackEvent } from "../packages/excalidraw/analytics"; +import { getDefaultAppState } from "../packages/excalidraw/appState"; +import { ErrorDialog } from "../packages/excalidraw/components/ErrorDialog"; +import { TopErrorBoundary } from "./components/TopErrorBoundary"; +import { useMathSubtype } from "../packages/excalidraw/element/subtypes/mathjax"; +import { + APP_NAME, + EVENT, + THEME, + TITLE_TIMEOUT, + VERSION_TIMEOUT, +} from "../packages/excalidraw/constants"; +import { loadFromBlob } from "../packages/excalidraw/data/blob"; +import { + ExcalidrawElement, + FileId, + NonDeletedExcalidrawElement, + Theme, +} from "../packages/excalidraw/element/types"; +import { useCallbackRefState } from "../packages/excalidraw/hooks/useCallbackRefState"; +import { t } from "../packages/excalidraw/i18n"; +import { + Excalidraw, + defaultLang, + LiveCollaborationTrigger, + TTDDialog, + TTDDialogTrigger, +} from "../packages/excalidraw/index"; +import { + AppState, + LibraryItems, + ExcalidrawImperativeAPI, + BinaryFiles, + ExcalidrawInitialDataState, + UIAppState, +} from "../packages/excalidraw/types"; +import { + debounce, + getVersion, + getFrame, + isTestEnv, + preventUnload, + ResolvablePromise, + resolvablePromise, + isRunningInIframe, +} from "../packages/excalidraw/utils"; +import { + FIREBASE_STORAGE_PREFIXES, + STORAGE_KEYS, + SYNC_BROWSER_TABS_TIMEOUT, +} from "./app_constants"; +import Collab, { + CollabAPI, + collabAPIAtom, + isCollaboratingAtom, + isOfflineAtom, +} from "./collab/Collab"; +import { + exportToBackend, + getCollaborationLinkData, + isCollaborationLink, + loadScene, +} from "./data"; +import { + getLibraryItemsFromStorage, + importFromLocalStorage, + importUsernameFromLocalStorage, +} from "./data/localStorage"; +import CustomStats from "./CustomStats"; +import { + restore, + restoreAppState, + RestoredDataState, +} from "../packages/excalidraw/data/restore"; +import { + ExportToExcalidrawPlus, + exportToExcalidrawPlus, +} from "./components/ExportToExcalidrawPlus"; +import { updateStaleImageStatuses } from "./data/FileManager"; +import { newElementWith } from "../packages/excalidraw/element/mutateElement"; +import { isInitializedImageElement } from "../packages/excalidraw/element/typeChecks"; +import { loadFilesFromFirebase } from "./data/firebase"; +import { LocalData } from "./data/LocalData"; +import { isBrowserStorageStateNewer } from "./data/tabSync"; +import clsx from "clsx"; +import { reconcileElements } from "./collab/reconciliation"; +import { + parseLibraryTokensFromUrl, + useHandleLibrary, +} from "../packages/excalidraw/data/library"; +import { AppMainMenu } from "./components/AppMainMenu"; +import { AppWelcomeScreen } from "./components/AppWelcomeScreen"; +import { AppFooter } from "./components/AppFooter"; +import { atom, Provider, useAtom, useAtomValue } from "jotai"; +import { useAtomWithInitialValue } from "../packages/excalidraw/jotai"; +import { appJotaiStore } from "./app-jotai"; + +import "./index.scss"; +import { ResolutionType } from "../packages/excalidraw/utility-types"; +import { ShareableLinkDialog } from "../packages/excalidraw/components/ShareableLinkDialog"; +import { openConfirmModal } from "../packages/excalidraw/components/OverwriteConfirm/OverwriteConfirmState"; +import { OverwriteConfirmDialog } from "../packages/excalidraw/components/OverwriteConfirm/OverwriteConfirm"; +import Trans from "../packages/excalidraw/components/Trans"; +import { ShareDialog, shareDialogStateAtom } from "./share/ShareDialog"; + +polyfill(); + +window.EXCALIDRAW_THROTTLE_RENDER = true; + +let isSelfEmbedding = false; + +if (window.self !== window.top) { + try { + const parentUrl = new URL(document.referrer); + const currentUrl = new URL(window.location.href); + if (parentUrl.origin === currentUrl.origin) { + isSelfEmbedding = true; + } + } catch (error) { + // ignore + } +} + +const languageDetector = new LanguageDetector(); +languageDetector.init({ + languageUtils: {}, +}); + +const shareableLinkConfirmDialog = { + title: t("overwriteConfirm.modal.shareableLink.title"), + description: ( + {text}} + br={() =>
} + /> + ), + actionLabel: t("overwriteConfirm.modal.shareableLink.button"), + color: "danger", +} as const; + +const initializeScene = async (opts: { + collabAPI: CollabAPI | null; + excalidrawAPI: ExcalidrawImperativeAPI; +}): Promise< + { scene: ExcalidrawInitialDataState | null } & ( + | { isExternalScene: true; id: string; key: string } + | { isExternalScene: false; id?: null; key?: null } + ) +> => { + const searchParams = new URLSearchParams(window.location.search); + const id = searchParams.get("id"); + const jsonBackendMatch = window.location.hash.match( + /^#json=([a-zA-Z0-9_-]+),([a-zA-Z0-9_-]+)$/, + ); + const externalUrlMatch = window.location.hash.match(/^#url=(.*)$/); + + const localDataState = importFromLocalStorage(); + + let scene: RestoredDataState & { + scrollToContent?: boolean; + } = await loadScene(null, null, localDataState); + + let roomLinkData = getCollaborationLinkData(window.location.href); + const isExternalScene = !!(id || jsonBackendMatch || roomLinkData); + if (isExternalScene) { + if ( + // don't prompt if scene is empty + !scene.elements.length || + // don't prompt for collab scenes because we don't override local storage + roomLinkData || + // otherwise, prompt whether user wants to override current scene + (await openConfirmModal(shareableLinkConfirmDialog)) + ) { + if (jsonBackendMatch) { + scene = await loadScene( + jsonBackendMatch[1], + jsonBackendMatch[2], + localDataState, + ); + } + scene.scrollToContent = true; + if (!roomLinkData) { + window.history.replaceState({}, APP_NAME, window.location.origin); + } + } else { + // https://github.com/excalidraw/excalidraw/issues/1919 + if (document.hidden) { + return new Promise((resolve, reject) => { + window.addEventListener( + "focus", + () => initializeScene(opts).then(resolve).catch(reject), + { + once: true, + }, + ); + }); + } + + roomLinkData = null; + window.history.replaceState({}, APP_NAME, window.location.origin); + } + } else if (externalUrlMatch) { + window.history.replaceState({}, APP_NAME, window.location.origin); + + const url = externalUrlMatch[1]; + try { + const request = await fetch(window.decodeURIComponent(url)); + const data = await loadFromBlob(await request.blob(), null, null); + if ( + !scene.elements.length || + (await openConfirmModal(shareableLinkConfirmDialog)) + ) { + return { scene: data, isExternalScene }; + } + } catch (error: any) { + return { + scene: { + appState: { + errorMessage: t("alerts.invalidSceneUrl"), + }, + }, + isExternalScene, + }; + } + } + + if (roomLinkData && opts.collabAPI) { + const { excalidrawAPI } = opts; + + const scene = await opts.collabAPI.startCollaboration(roomLinkData); + + return { + // when collaborating, the state may have already been updated at this + // point (we may have received updates from other clients), so reconcile + // elements and appState with existing state + scene: { + ...scene, + appState: { + ...restoreAppState( + { + ...scene?.appState, + theme: localDataState?.appState?.theme || scene?.appState?.theme, + }, + excalidrawAPI.getAppState(), + ), + // necessary if we're invoking from a hashchange handler which doesn't + // go through App.initializeScene() that resets this flag + isLoading: false, + }, + elements: reconcileElements( + scene?.elements || [], + excalidrawAPI.getSceneElementsIncludingDeleted(), + excalidrawAPI.getAppState(), + ), + }, + isExternalScene: true, + id: roomLinkData.roomId, + key: roomLinkData.roomKey, + }; + } else if (scene) { + return isExternalScene && jsonBackendMatch + ? { + scene, + isExternalScene, + id: jsonBackendMatch[1], + key: jsonBackendMatch[2], + } + : { scene, isExternalScene: false }; + } + return { scene: null, isExternalScene: false }; +}; + +const detectedLangCode = languageDetector.detect() || defaultLang.code; +export const appLangCodeAtom = atom( + Array.isArray(detectedLangCode) ? detectedLangCode[0] : detectedLangCode, +); + +const ExcalidrawWrapper = () => { + const [errorMessage, setErrorMessage] = useState(""); + const [langCode, setLangCode] = useAtom(appLangCodeAtom); + const isCollabDisabled = isRunningInIframe(); + + // initial state + // --------------------------------------------------------------------------- + + const initialStatePromiseRef = useRef<{ + promise: ResolvablePromise; + }>({ promise: null! }); + if (!initialStatePromiseRef.current.promise) { + initialStatePromiseRef.current.promise = + resolvablePromise(); + } + + useEffect(() => { + trackEvent("load", "frame", getFrame()); + // Delayed so that the app has a time to load the latest SW + setTimeout(() => { + trackEvent("load", "version", getVersion()); + }, VERSION_TIMEOUT); + }, []); + + const [excalidrawAPI, excalidrawRefCallback] = + useCallbackRefState(); + + useMathSubtype(excalidrawAPI); + + const [, setShareDialogState] = useAtom(shareDialogStateAtom); + const [collabAPI] = useAtom(collabAPIAtom); + const [isCollaborating] = useAtomWithInitialValue(isCollaboratingAtom, () => { + return isCollaborationLink(window.location.href); + }); + + useHandleLibrary({ + excalidrawAPI, + getInitialLibraryItems: getLibraryItemsFromStorage, + }); + + useEffect(() => { + if (!excalidrawAPI || (!isCollabDisabled && !collabAPI)) { + return; + } + + const loadImages = ( + data: ResolutionType, + isInitialLoad = false, + ) => { + if (!data.scene) { + return; + } + if (collabAPI?.isCollaborating()) { + if (data.scene.elements) { + collabAPI + .fetchImageFilesFromFirebase({ + elements: data.scene.elements, + forceFetchFiles: true, + }) + .then(({ loadedFiles, erroredFiles }) => { + excalidrawAPI.addFiles(loadedFiles); + updateStaleImageStatuses({ + excalidrawAPI, + erroredFiles, + elements: excalidrawAPI.getSceneElementsIncludingDeleted(), + }); + }); + } + } else { + const fileIds = + data.scene.elements?.reduce((acc, element) => { + if (isInitializedImageElement(element)) { + return acc.concat(element.fileId); + } + return acc; + }, [] as FileId[]) || []; + + if (data.isExternalScene) { + loadFilesFromFirebase( + `${FIREBASE_STORAGE_PREFIXES.shareLinkFiles}/${data.id}`, + data.key, + fileIds, + ).then(({ loadedFiles, erroredFiles }) => { + excalidrawAPI.addFiles(loadedFiles); + updateStaleImageStatuses({ + excalidrawAPI, + erroredFiles, + elements: excalidrawAPI.getSceneElementsIncludingDeleted(), + }); + }); + } else if (isInitialLoad) { + if (fileIds.length) { + LocalData.fileStorage + .getFiles(fileIds) + .then(({ loadedFiles, erroredFiles }) => { + if (loadedFiles.length) { + excalidrawAPI.addFiles(loadedFiles); + } + updateStaleImageStatuses({ + excalidrawAPI, + erroredFiles, + elements: excalidrawAPI.getSceneElementsIncludingDeleted(), + }); + }); + } + // on fresh load, clear unused files from IDB (from previous + // session) + LocalData.fileStorage.clearObsoleteFiles({ currentFileIds: fileIds }); + } + } + }; + + initializeScene({ collabAPI, excalidrawAPI }).then(async (data) => { + loadImages(data, /* isInitialLoad */ true); + initialStatePromiseRef.current.promise.resolve(data.scene); + }); + + const onHashChange = async (event: HashChangeEvent) => { + event.preventDefault(); + const libraryUrlTokens = parseLibraryTokensFromUrl(); + if (!libraryUrlTokens) { + if ( + collabAPI?.isCollaborating() && + !isCollaborationLink(window.location.href) + ) { + collabAPI.stopCollaboration(false); + } + excalidrawAPI.updateScene({ appState: { isLoading: true } }); + + initializeScene({ collabAPI, excalidrawAPI }).then((data) => { + loadImages(data); + if (data.scene) { + excalidrawAPI.updateScene({ + ...data.scene, + ...restore(data.scene, null, null, { repairBindings: true }), + commitToHistory: true, + }); + } + }); + } + }; + + const titleTimeout = setTimeout( + () => (document.title = APP_NAME), + TITLE_TIMEOUT, + ); + + const syncData = debounce(() => { + if (isTestEnv()) { + return; + } + if ( + !document.hidden && + ((collabAPI && !collabAPI.isCollaborating()) || isCollabDisabled) + ) { + // don't sync if local state is newer or identical to browser state + if (isBrowserStorageStateNewer(STORAGE_KEYS.VERSION_DATA_STATE)) { + const localDataState = importFromLocalStorage(); + const username = importUsernameFromLocalStorage(); + let langCode = languageDetector.detect() || defaultLang.code; + if (Array.isArray(langCode)) { + langCode = langCode[0]; + } + setLangCode(langCode); + excalidrawAPI.updateScene({ + ...localDataState, + }); + excalidrawAPI.updateLibrary({ + libraryItems: getLibraryItemsFromStorage(), + }); + collabAPI?.setUsername(username || ""); + } + + if (isBrowserStorageStateNewer(STORAGE_KEYS.VERSION_FILES)) { + const elements = excalidrawAPI.getSceneElementsIncludingDeleted(); + const currFiles = excalidrawAPI.getFiles(); + const fileIds = + elements?.reduce((acc, element) => { + if ( + isInitializedImageElement(element) && + // only load and update images that aren't already loaded + !currFiles[element.fileId] + ) { + return acc.concat(element.fileId); + } + return acc; + }, [] as FileId[]) || []; + if (fileIds.length) { + LocalData.fileStorage + .getFiles(fileIds) + .then(({ loadedFiles, erroredFiles }) => { + if (loadedFiles.length) { + excalidrawAPI.addFiles(loadedFiles); + } + updateStaleImageStatuses({ + excalidrawAPI, + erroredFiles, + elements: excalidrawAPI.getSceneElementsIncludingDeleted(), + }); + }); + } + } + } + }, SYNC_BROWSER_TABS_TIMEOUT); + + const onUnload = () => { + LocalData.flushSave(); + }; + + const visibilityChange = (event: FocusEvent | Event) => { + if (event.type === EVENT.BLUR || document.hidden) { + LocalData.flushSave(); + } + if ( + event.type === EVENT.VISIBILITY_CHANGE || + event.type === EVENT.FOCUS + ) { + syncData(); + } + }; + + window.addEventListener(EVENT.HASHCHANGE, onHashChange, false); + window.addEventListener(EVENT.UNLOAD, onUnload, false); + window.addEventListener(EVENT.BLUR, visibilityChange, false); + document.addEventListener(EVENT.VISIBILITY_CHANGE, visibilityChange, false); + window.addEventListener(EVENT.FOCUS, visibilityChange, false); + return () => { + window.removeEventListener(EVENT.HASHCHANGE, onHashChange, false); + window.removeEventListener(EVENT.UNLOAD, onUnload, false); + window.removeEventListener(EVENT.BLUR, visibilityChange, false); + window.removeEventListener(EVENT.FOCUS, visibilityChange, false); + document.removeEventListener( + EVENT.VISIBILITY_CHANGE, + visibilityChange, + false, + ); + clearTimeout(titleTimeout); + }; + }, [isCollabDisabled, collabAPI, excalidrawAPI, setLangCode]); + + useEffect(() => { + const unloadHandler = (event: BeforeUnloadEvent) => { + LocalData.flushSave(); + + if ( + excalidrawAPI && + LocalData.fileStorage.shouldPreventUnload( + excalidrawAPI.getSceneElements(), + ) + ) { + preventUnload(event); + } + }; + window.addEventListener(EVENT.BEFORE_UNLOAD, unloadHandler); + return () => { + window.removeEventListener(EVENT.BEFORE_UNLOAD, unloadHandler); + }; + }, [excalidrawAPI]); + + useEffect(() => { + languageDetector.cacheUserLanguage(langCode); + }, [langCode]); + + const [theme, setTheme] = useState( + () => + (localStorage.getItem( + STORAGE_KEYS.LOCAL_STORAGE_THEME, + ) as Theme | null) || + // FIXME migration from old LS scheme. Can be removed later. #5660 + importFromLocalStorage().appState?.theme || + THEME.LIGHT, + ); + + useEffect(() => { + localStorage.setItem(STORAGE_KEYS.LOCAL_STORAGE_THEME, theme); + // currently only used for body styling during init (see public/index.html), + // but may change in the future + document.documentElement.classList.toggle("dark", theme === THEME.DARK); + }, [theme]); + + const onChange = ( + elements: readonly ExcalidrawElement[], + appState: AppState, + files: BinaryFiles, + ) => { + if (collabAPI?.isCollaborating()) { + collabAPI.syncElements(elements); + } + + setTheme(appState.theme); + + // this check is redundant, but since this is a hot path, it's best + // not to evaludate the nested expression every time + if (!LocalData.isSavePaused()) { + LocalData.save(elements, appState, files, () => { + if (excalidrawAPI) { + let didChange = false; + + const elements = excalidrawAPI + .getSceneElementsIncludingDeleted() + .map((element) => { + if ( + LocalData.fileStorage.shouldUpdateImageElementStatus(element) + ) { + const newElement = newElementWith(element, { status: "saved" }); + if (newElement !== element) { + didChange = true; + } + return newElement; + } + return element; + }); + + if (didChange) { + excalidrawAPI.updateScene({ + elements, + }); + } + } + }); + } + }; + + const [latestShareableLink, setLatestShareableLink] = useState( + null, + ); + + const onExportToBackend = async ( + exportedElements: readonly NonDeletedExcalidrawElement[], + appState: Partial, + files: BinaryFiles, + ) => { + if (exportedElements.length === 0) { + throw new Error(t("alerts.cannotExportEmptyCanvas")); + } + try { + const { url, errorMessage } = await exportToBackend( + exportedElements, + { + ...appState, + viewBackgroundColor: appState.exportBackground + ? appState.viewBackgroundColor + : getDefaultAppState().viewBackgroundColor, + }, + files, + ); + + if (errorMessage) { + throw new Error(errorMessage); + } + + if (url) { + setLatestShareableLink(url); + } + } catch (error: any) { + if (error.name !== "AbortError") { + const { width, height } = appState; + console.error(error, { + width, + height, + devicePixelRatio: window.devicePixelRatio, + }); + throw new Error(error.message); + } + } + }; + + const renderCustomStats = ( + elements: readonly NonDeletedExcalidrawElement[], + appState: UIAppState, + ) => { + return ( + excalidrawAPI!.setToast({ message })} + appState={appState} + elements={elements} + /> + ); + }; + + const onLibraryChange = async (items: LibraryItems) => { + if (!items.length) { + localStorage.removeItem(STORAGE_KEYS.LOCAL_STORAGE_LIBRARY); + return; + } + const serializedItems = JSON.stringify(items); + localStorage.setItem(STORAGE_KEYS.LOCAL_STORAGE_LIBRARY, serializedItems); + }; + + const isOffline = useAtomValue(isOfflineAtom); + + const onCollabDialogOpen = useCallback( + () => setShareDialogState({ isOpen: true, type: "collaborationOnly" }), + [setShareDialogState], + ); + + // browsers generally prevent infinite self-embedding, there are + // cases where it still happens, and while we disallow self-embedding + // by not whitelisting our own origin, this serves as an additional guard + if (isSelfEmbedding) { + return ( +
+

I'm not a pretzel!

+
+ ); + } + + return ( +
+ { + return ( + { + excalidrawAPI?.updateScene({ + appState: { + errorMessage: error.message, + }, + }); + }} + onSuccess={() => { + excalidrawAPI?.updateScene({ + appState: { openDialog: null }, + }); + }} + /> + ); + }, + }, + }, + }} + langCode={langCode} + renderCustomStats={renderCustomStats} + detectScroll={false} + handleKeyboardGlobally={true} + onLibraryChange={onLibraryChange} + autoFocus={true} + theme={theme} + renderTopRightUI={(isMobile) => { + if (isMobile || !collabAPI || isCollabDisabled) { + return null; + } + return ( + + setShareDialogState({ isOpen: true, type: "share" }) + } + /> + ); + }} + > + + + + + + {excalidrawAPI && ( + { + exportToExcalidrawPlus( + excalidrawAPI.getSceneElements(), + excalidrawAPI.getAppState(), + excalidrawAPI.getFiles(), + ); + }} + > + {t("overwriteConfirm.action.excalidrawPlus.description")} + + )} + + + { + try { + const response = await fetch( + `${ + import.meta.env.VITE_APP_AI_BACKEND + }/v1/ai/text-to-diagram/generate`, + { + method: "POST", + headers: { + Accept: "application/json", + "Content-Type": "application/json", + }, + body: JSON.stringify({ prompt: input }), + }, + ); + + const rateLimit = response.headers.has("X-Ratelimit-Limit") + ? parseInt(response.headers.get("X-Ratelimit-Limit") || "0", 10) + : undefined; + + const rateLimitRemaining = response.headers.has( + "X-Ratelimit-Remaining", + ) + ? parseInt( + response.headers.get("X-Ratelimit-Remaining") || "0", + 10, + ) + : undefined; + + const json = await response.json(); + + if (!response.ok) { + if (response.status === 429) { + return { + rateLimit, + rateLimitRemaining, + error: new Error( + "Too many requests today, please try again tomorrow!", + ), + }; + } + + throw new Error(json.message || "Generation failed..."); + } + + const generatedResponse = json.generatedResponse; + if (!generatedResponse) { + throw new Error("Generation failed..."); + } + + return { generatedResponse, rateLimit, rateLimitRemaining }; + } catch (err: any) { + throw new Error("Request failed"); + } + }} + /> + + {isCollaborating && isOffline && ( +
+ {t("alerts.collabOfflineWarning")} +
+ )} + {latestShareableLink && ( + setLatestShareableLink(null)} + setErrorMessage={setErrorMessage} + /> + )} + {excalidrawAPI && !isCollabDisabled && ( + + )} + + { + if (excalidrawAPI) { + try { + await onExportToBackend( + excalidrawAPI.getSceneElements(), + excalidrawAPI.getAppState(), + excalidrawAPI.getFiles(), + ); + } catch (error: any) { + setErrorMessage(error.message); + } + } + }} + /> + + {errorMessage && ( + setErrorMessage("")}> + {errorMessage} + + )} +
+
+ ); +}; + +const ExcalidrawApp = () => { + return ( + + appJotaiStore}> + + + + ); +}; + +export default ExcalidrawApp; diff --git a/excalidraw-app/CustomStats.tsx b/excalidraw-app/CustomStats.tsx index b34c1f7d7..f2ce80f21 100644 --- a/excalidraw-app/CustomStats.tsx +++ b/excalidraw-app/CustomStats.tsx @@ -1,14 +1,14 @@ import { useEffect, useState } from "react"; -import { debounce, getVersion, nFormatter } from "../src/utils"; +import { debounce, getVersion, nFormatter } from "../packages/excalidraw/utils"; import { getElementsStorageSize, getTotalStorageSize, } from "./data/localStorage"; -import { DEFAULT_VERSION } from "../src/constants"; -import { t } from "../src/i18n"; -import { copyTextToSystemClipboard } from "../src/clipboard"; -import { NonDeletedExcalidrawElement } from "../src/element/types"; -import { UIAppState } from "../src/types"; +import { DEFAULT_VERSION } from "../packages/excalidraw/constants"; +import { t } from "../packages/excalidraw/i18n"; +import { copyTextToSystemClipboard } from "../packages/excalidraw/clipboard"; +import { NonDeletedExcalidrawElement } from "../packages/excalidraw/element/types"; +import { UIAppState } from "../packages/excalidraw/types"; type StorageSizes = { scene: number; total: number }; diff --git a/excalidraw-app/app_constants.ts b/excalidraw-app/app_constants.ts index 179fe52e7..3402bf106 100644 --- a/excalidraw-app/app_constants.ts +++ b/excalidraw-app/app_constants.ts @@ -15,11 +15,17 @@ export const FILE_CACHE_MAX_AGE_SEC = 31536000; export const WS_EVENTS = { SERVER_VOLATILE: "server-volatile-broadcast", SERVER: "server-broadcast", -}; + USER_FOLLOW_CHANGE: "user-follow", + USER_FOLLOW_ROOM_CHANGE: "user-follow-room-change", +} as const; -export enum WS_SCENE_EVENT_TYPES { +export enum WS_SUBTYPES { + INVALID_RESPONSE = "INVALID_RESPONSE", INIT = "SCENE_INIT", UPDATE = "SCENE_UPDATE", + MOUSE_LOCATION = "MOUSE_LOCATION", + IDLE_STATUS = "IDLE_STATUS", + USER_VISIBLE_SCENE_BOUNDS = "USER_VISIBLE_SCENE_BOUNDS", } export const FIREBASE_STORAGE_PREFIXES = { diff --git a/src/bug-issue-template.js b/excalidraw-app/bug-issue-template.js similarity index 100% rename from src/bug-issue-template.js rename to excalidraw-app/bug-issue-template.js diff --git a/excalidraw-app/collab/Collab.tsx b/excalidraw-app/collab/Collab.tsx index 0d57a8906..14538b674 100644 --- a/excalidraw-app/collab/Collab.tsx +++ b/excalidraw-app/collab/Collab.tsx @@ -1,36 +1,41 @@ import throttle from "lodash.throttle"; import { PureComponent } from "react"; -import { ExcalidrawImperativeAPI } from "../../src/types"; -import { ErrorDialog } from "../../src/components/ErrorDialog"; -import { APP_NAME, ENV, EVENT } from "../../src/constants"; -import { ImportedDataState } from "../../src/data/types"; +import { + ExcalidrawImperativeAPI, + SocketId, +} from "../../packages/excalidraw/types"; +import { ErrorDialog } from "../../packages/excalidraw/components/ErrorDialog"; +import { APP_NAME, ENV, EVENT } from "../../packages/excalidraw/constants"; +import { ImportedDataState } from "../../packages/excalidraw/data/types"; import { ExcalidrawElement, InitializedExcalidrawImageElement, -} from "../../src/element/types"; +} from "../../packages/excalidraw/element/types"; import { getSceneVersion, restoreElements, -} from "../../src/packages/excalidraw/index"; -import { Collaborator, Gesture } from "../../src/types"; + zoomToFitBounds, +} from "../../packages/excalidraw/index"; +import { Collaborator, Gesture } from "../../packages/excalidraw/types"; import { + assertNever, preventUnload, resolvablePromise, - withBatchedUpdates, -} from "../../src/utils"; + throttleRAF, +} from "../../packages/excalidraw/utils"; import { CURSOR_SYNC_TIMEOUT, FILE_UPLOAD_MAX_BYTES, FIREBASE_STORAGE_PREFIXES, INITIAL_SCENE_UPDATE_TIMEOUT, LOAD_IMAGES_TIMEOUT, - WS_SCENE_EVENT_TYPES, + WS_SUBTYPES, SYNC_FULL_SCENE_INTERVAL_MS, + WS_EVENTS, } from "../app_constants"; import { generateCollaborationLinkData, getCollaborationLink, - getCollabServer, getSyncableElements, SocketUpdateDataSource, SyncableExcalidrawElement, @@ -47,42 +52,48 @@ import { saveUsernameToLocalStorage, } from "../data/localStorage"; import Portal from "./Portal"; -import RoomDialog from "./RoomDialog"; -import { t } from "../../src/i18n"; -import { UserIdleState } from "../../src/types"; -import { IDLE_THRESHOLD, ACTIVE_THRESHOLD } from "../../src/constants"; +import { t } from "../../packages/excalidraw/i18n"; +import { UserIdleState } from "../../packages/excalidraw/types"; +import { + IDLE_THRESHOLD, + ACTIVE_THRESHOLD, +} from "../../packages/excalidraw/constants"; import { encodeFilesForUpload, FileManager, updateStaleImageStatuses, } from "../data/FileManager"; -import { AbortError } from "../../src/errors"; +import { AbortError } from "../../packages/excalidraw/errors"; import { isImageElement, isInitializedImageElement, -} from "../../src/element/typeChecks"; -import { newElementWith } from "../../src/element/mutateElement"; +} from "../../packages/excalidraw/element/typeChecks"; +import { newElementWith } from "../../packages/excalidraw/element/mutateElement"; import { ReconciledElements, reconcileElements as _reconcileElements, } from "./reconciliation"; -import { decryptData } from "../../src/data/encryption"; +import { decryptData } from "../../packages/excalidraw/data/encryption"; import { resetBrowserStateVersions } from "../data/tabSync"; import { LocalData } from "../data/LocalData"; -import { atom, useAtom } from "jotai"; +import { atom } from "jotai"; import { appJotaiStore } from "../app-jotai"; +import { Mutable, ValueOf } from "../../packages/excalidraw/utility-types"; +import { getVisibleSceneBounds } from "../../packages/excalidraw/element/bounds"; +import { withBatchedUpdates } from "../../packages/excalidraw/reactUtils"; export const collabAPIAtom = atom(null); -export const collabDialogShownAtom = atom(false); export const isCollaboratingAtom = atom(false); export const isOfflineAtom = atom(false); interface CollabState { - errorMessage: string; + errorMessage: string | null; username: string; - activeRoomLink: string; + activeRoomLink: string | null; } +export const activeRoomLinkAtom = atom(null); + type CollabInstance = InstanceType; export interface CollabAPI { @@ -93,32 +104,33 @@ export interface CollabAPI { stopCollaboration: CollabInstance["stopCollaboration"]; syncElements: CollabInstance["syncElements"]; fetchImageFilesFromFirebase: CollabInstance["fetchImageFilesFromFirebase"]; - setUsername: (username: string) => void; + setUsername: CollabInstance["setUsername"]; + getUsername: CollabInstance["getUsername"]; + getActiveRoomLink: CollabInstance["getActiveRoomLink"]; + setErrorMessage: CollabInstance["setErrorMessage"]; } -interface PublicProps { +interface CollabProps { excalidrawAPI: ExcalidrawImperativeAPI; } -type Props = PublicProps & { modalIsShown: boolean }; - -class Collab extends PureComponent { +class Collab extends PureComponent { portal: Portal; fileManager: FileManager; - excalidrawAPI: Props["excalidrawAPI"]; + excalidrawAPI: CollabProps["excalidrawAPI"]; activeIntervalId: number | null; idleTimeoutId: number | null; private socketInitializationTimer?: number; private lastBroadcastedOrReceivedSceneVersion: number = -1; - private collaborators = new Map(); + private collaborators = new Map(); - constructor(props: Props) { + constructor(props: CollabProps) { super(props); this.state = { - errorMessage: "", + errorMessage: null, username: importUsernameFromLocalStorage() || "", - activeRoomLink: "", + activeRoomLink: null, }; this.portal = new Portal(this); this.fileManager = new FileManager({ @@ -151,12 +163,28 @@ class Collab extends PureComponent { this.idleTimeoutId = null; } + private onUmmount: (() => void) | null = null; + componentDidMount() { window.addEventListener(EVENT.BEFORE_UNLOAD, this.beforeUnload); window.addEventListener("online", this.onOfflineStatusToggle); window.addEventListener("offline", this.onOfflineStatusToggle); window.addEventListener(EVENT.UNLOAD, this.onUnload); + const unsubOnUserFollow = this.excalidrawAPI.onUserFollow((payload) => { + this.portal.socket && this.portal.broadcastUserFollowed(payload); + }); + const throttledRelayUserViewportBounds = throttleRAF( + this.relayVisibleSceneBounds, + ); + const unsubOnScrollChange = this.excalidrawAPI.onScrollChange(() => + throttledRelayUserViewportBounds(), + ); + this.onUmmount = () => { + unsubOnUserFollow(); + unsubOnScrollChange(); + }; + this.onOfflineStatusToggle(); const collabAPI: CollabAPI = { @@ -167,6 +195,9 @@ class Collab extends PureComponent { fetchImageFilesFromFirebase: this.fetchImageFilesFromFirebase, stopCollaboration: this.stopCollaboration, setUsername: this.setUsername, + getUsername: this.getUsername, + getActiveRoomLink: this.getActiveRoomLink, + setErrorMessage: this.setErrorMessage, }; appJotaiStore.set(collabAPIAtom, collabAPI); @@ -204,6 +235,7 @@ class Collab extends PureComponent { window.clearTimeout(this.idleTimeoutId); this.idleTimeoutId = null; } + this.onUmmount?.(); } isCollaborating = () => appJotaiStore.get(isCollaboratingAtom)!; @@ -313,9 +345,7 @@ class Collab extends PureComponent { this.fileManager.reset(); if (!opts?.isUnload) { this.setIsCollaborating(false); - this.setState({ - activeRoomLink: "", - }); + this.setActiveRoomLink(null); this.collaborators = new Map(); this.excalidrawAPI.updateScene({ collaborators: this.collaborators, @@ -356,7 +386,7 @@ class Collab extends PureComponent { iv: Uint8Array, encryptedData: ArrayBuffer, decryptionKey: string, - ) => { + ): Promise> => { try { const decrypted = await decryptData(iv, encryptedData, decryptionKey); @@ -368,7 +398,7 @@ class Collab extends PureComponent { window.alert(t("alerts.decryptFailed")); console.error(error); return { - type: "INVALID_RESPONSE", + type: WS_SUBTYPES.INVALID_RESPONSE, }; } }; @@ -381,7 +411,7 @@ class Collab extends PureComponent { if (!this.state.username) { import("@excalidraw/random-username").then(({ getRandomUsername }) => { const username = getRandomUsername(); - this.onUsernameChange(username); + this.setUsername(username); }); } @@ -423,13 +453,9 @@ class Collab extends PureComponent { this.fallbackInitializationHandler = fallbackInitializationHandler; try { - const socketServerData = await getCollabServer(); - this.portal.socket = this.portal.open( - socketIOClient(socketServerData.url, { - transports: socketServerData.polling - ? ["websocket", "polling"] - : ["websocket"], + socketIOClient(import.meta.env.VITE_APP_WS_SERVER_URL, { + transports: ["websocket", "polling"], }), roomId, roomKey, @@ -484,9 +510,9 @@ class Collab extends PureComponent { ); switch (decryptedData.type) { - case "INVALID_RESPONSE": + case WS_SUBTYPES.INVALID_RESPONSE: return; - case WS_SCENE_EVENT_TYPES.INIT: { + case WS_SUBTYPES.INIT: { if (!this.portal.socketInitialized) { this.initializeRoom({ fetchScene: false }); const remoteElements = decryptedData.payload.elements; @@ -502,41 +528,75 @@ class Collab extends PureComponent { } break; } - case WS_SCENE_EVENT_TYPES.UPDATE: + case WS_SUBTYPES.UPDATE: this.handleRemoteSceneUpdate( this.reconcileElements(decryptedData.payload.elements), ); break; - case "MOUSE_LOCATION": { + case WS_SUBTYPES.MOUSE_LOCATION: { const { pointer, button, username, selectedElementIds } = decryptedData.payload; + const socketId: SocketUpdateDataSource["MOUSE_LOCATION"]["payload"]["socketId"] = decryptedData.payload.socketId || // @ts-ignore legacy, see #2094 (#2097) decryptedData.payload.socketID; - const collaborators = new Map(this.collaborators); - const user = collaborators.get(socketId) || {}!; - user.pointer = pointer; - user.button = button; - user.selectedElementIds = selectedElementIds; - user.username = username; - collaborators.set(socketId, user); + this.updateCollaborator(socketId, { + pointer, + button, + selectedElementIds, + username, + }); + + break; + } + + case WS_SUBTYPES.USER_VISIBLE_SCENE_BOUNDS: { + const { sceneBounds, socketId } = decryptedData.payload; + + const appState = this.excalidrawAPI.getAppState(); + + // we're not following the user + // (shouldn't happen, but could be late message or bug upstream) + if (appState.userToFollow?.socketId !== socketId) { + console.warn( + `receiving remote client's (from ${socketId}) viewport bounds even though we're not subscribed to it!`, + ); + return; + } + + // cross-follow case, ignore updates in this case + if ( + appState.userToFollow && + appState.followedBy.has(appState.userToFollow.socketId) + ) { + return; + } + this.excalidrawAPI.updateScene({ - collaborators, + appState: zoomToFitBounds({ + appState, + bounds: sceneBounds, + fitToViewport: true, + viewportZoomFactor: 1, + }).appState, + }); + + break; + } + + case WS_SUBTYPES.IDLE_STATUS: { + const { userState, socketId, username } = decryptedData.payload; + this.updateCollaborator(socketId, { + userState, + username, }); break; } - case "IDLE_STATUS": { - const { userState, socketId, username } = decryptedData.payload; - const collaborators = new Map(this.collaborators); - const user = collaborators.get(socketId) || {}!; - user.userState = userState; - user.username = username; - this.excalidrawAPI.updateScene({ - collaborators, - }); - break; + + default: { + assertNever(decryptedData, null); } } }, @@ -553,11 +613,20 @@ class Collab extends PureComponent { scenePromise.resolve(sceneData); }); + this.portal.socket.on( + WS_EVENTS.USER_FOLLOW_ROOM_CHANGE, + (followedBy: SocketId[]) => { + this.excalidrawAPI.updateScene({ + appState: { followedBy: new Set(followedBy) }, + }); + + this.relayVisibleSceneBounds({ force: true }); + }, + ); + this.initializeIdleDetector(); - this.setState({ - activeRoomLink: window.location.href, - }); + this.setActiveRoomLink(window.location.href); return scenePromise; }; @@ -721,20 +790,39 @@ class Collab extends PureComponent { document.addEventListener(EVENT.VISIBILITY_CHANGE, this.onVisibilityChange); }; - setCollaborators(sockets: string[]) { + setCollaborators(sockets: SocketId[]) { const collaborators: InstanceType["collaborators"] = new Map(); for (const socketId of sockets) { - if (this.collaborators.has(socketId)) { - collaborators.set(socketId, this.collaborators.get(socketId)!); - } else { - collaborators.set(socketId, {}); - } + collaborators.set( + socketId, + Object.assign({}, this.collaborators.get(socketId), { + isCurrentUser: socketId === this.portal.socket?.id, + }), + ); } this.collaborators = collaborators; this.excalidrawAPI.updateScene({ collaborators }); } + updateCollaborator = (socketId: SocketId, updates: Partial) => { + const collaborators = new Map(this.collaborators); + const user: Mutable = Object.assign( + {}, + collaborators.get(socketId), + updates, + { + isCurrentUser: socketId === this.portal.socket?.id, + }, + ); + collaborators.set(socketId, user); + this.collaborators = collaborators; + + this.excalidrawAPI.updateScene({ + collaborators, + }); + }; + public setLastBroadcastedOrReceivedSceneVersion = (version: number) => { this.lastBroadcastedOrReceivedSceneVersion = version; }; @@ -760,6 +848,19 @@ class Collab extends PureComponent { CURSOR_SYNC_TIMEOUT, ); + relayVisibleSceneBounds = (props?: { force: boolean }) => { + const appState = this.excalidrawAPI.getAppState(); + + if (this.portal.socket && (appState.followedBy.size > 0 || props?.force)) { + this.portal.broadcastVisibleSceneBounds( + { + sceneBounds: getVisibleSceneBounds(appState), + }, + `follow@${this.portal.socket.id}`, + ); + } + }; + onIdleStateChange = (userState: UserIdleState) => { this.portal.broadcastIdleChange(userState); }; @@ -769,7 +870,7 @@ class Collab extends PureComponent { getSceneVersion(elements) > this.getLastBroadcastedOrReceivedSceneVersion() ) { - this.portal.broadcastScene(WS_SCENE_EVENT_TYPES.UPDATE, elements, false); + this.portal.broadcastScene(WS_SUBTYPES.UPDATE, elements, false); this.lastBroadcastedOrReceivedSceneVersion = getSceneVersion(elements); this.queueBroadcastAllElements(); } @@ -782,7 +883,7 @@ class Collab extends PureComponent { queueBroadcastAllElements = throttle(() => { this.portal.broadcastScene( - WS_SCENE_EVENT_TYPES.UPDATE, + WS_SUBTYPES.UPDATE, this.excalidrawAPI.getSceneElementsIncludingDeleted(), true, ); @@ -808,41 +909,31 @@ class Collab extends PureComponent { { leading: false }, ); - handleClose = () => { - appJotaiStore.set(collabDialogShownAtom, false); - }; - setUsername = (username: string) => { this.setState({ username }); - }; - - onUsernameChange = (username: string) => { - this.setUsername(username); saveUsernameToLocalStorage(username); }; - render() { - const { username, errorMessage, activeRoomLink } = this.state; + getUsername = () => this.state.username; - const { modalIsShown } = this.props; + setActiveRoomLink = (activeRoomLink: string | null) => { + this.setState({ activeRoomLink }); + appJotaiStore.set(activeRoomLinkAtom, activeRoomLink); + }; + + getActiveRoomLink = () => this.state.activeRoomLink; + + setErrorMessage = (errorMessage: string | null) => { + this.setState({ errorMessage }); + }; + + render() { + const { errorMessage } = this.state; return ( <> - {modalIsShown && ( - this.startCollaboration(null)} - onRoomDestroy={this.stopCollaboration} - setErrorMessage={(errorMessage) => { - this.setState({ errorMessage }); - }} - /> - )} - {errorMessage && ( - this.setState({ errorMessage: "" })}> + {errorMessage != null && ( + this.setState({ errorMessage: null })}> {errorMessage} )} @@ -861,11 +952,6 @@ if (import.meta.env.MODE === ENV.TEST || import.meta.env.DEV) { window.collab = window.collab || ({} as Window["collab"]); } -const _Collab: React.FC = (props) => { - const [collabDialogShown] = useAtom(collabDialogShownAtom); - return ; -}; - -export default _Collab; +export default Collab; export type TCollabClass = Collab; diff --git a/excalidraw-app/collab/Portal.tsx b/excalidraw-app/collab/Portal.tsx index 401b83ec5..bf8ffa5de 100644 --- a/excalidraw-app/collab/Portal.tsx +++ b/excalidraw-app/collab/Portal.tsx @@ -6,23 +6,24 @@ import { import { TCollabClass } from "./Collab"; -import { ExcalidrawElement } from "../../src/element/types"; +import { ExcalidrawElement } from "../../packages/excalidraw/element/types"; +import { WS_EVENTS, FILE_UPLOAD_TIMEOUT, WS_SUBTYPES } from "../app_constants"; import { - WS_EVENTS, - FILE_UPLOAD_TIMEOUT, - WS_SCENE_EVENT_TYPES, -} from "../app_constants"; -import { UserIdleState } from "../../src/types"; -import { trackEvent } from "../../src/analytics"; + OnUserFollowedPayload, + SocketId, + UserIdleState, +} from "../../packages/excalidraw/types"; +import { trackEvent } from "../../packages/excalidraw/analytics"; import throttle from "lodash.throttle"; -import { newElementWith } from "../../src/element/mutateElement"; +import { newElementWith } from "../../packages/excalidraw/element/mutateElement"; import { BroadcastedExcalidrawElement } from "./reconciliation"; -import { encryptData } from "../../src/data/encryption"; -import { PRECEDING_ELEMENT_KEY } from "../../src/constants"; +import { encryptData } from "../../packages/excalidraw/data/encryption"; +import { PRECEDING_ELEMENT_KEY } from "../../packages/excalidraw/constants"; +import type { Socket } from "socket.io-client"; class Portal { collab: TCollabClass; - socket: SocketIOClient.Socket | null = null; + socket: Socket | null = null; socketInitialized: boolean = false; // we don't want the socket to emit any updates until it is fully initialized roomId: string | null = null; roomKey: string | null = null; @@ -32,7 +33,7 @@ class Portal { this.collab = collab; } - open(socket: SocketIOClient.Socket, id: string, key: string) { + open(socket: Socket, id: string, key: string) { this.socket = socket; this.roomId = id; this.roomKey = key; @@ -46,12 +47,12 @@ class Portal { }); this.socket.on("new-user", async (_socketId: string) => { this.broadcastScene( - WS_SCENE_EVENT_TYPES.INIT, + WS_SUBTYPES.INIT, this.collab.getSceneElementsIncludingDeleted(), /* syncAll */ true, ); }); - this.socket.on("room-user-change", (clients: string[]) => { + this.socket.on("room-user-change", (clients: SocketId[]) => { this.collab.setCollaborators(clients); }); @@ -83,6 +84,7 @@ class Portal { async _broadcastSocketData( data: SocketUpdateData, volatile: boolean = false, + roomId?: string, ) { if (this.isOpen()) { const json = JSON.stringify(data); @@ -91,7 +93,7 @@ class Portal { this.socket?.emit( volatile ? WS_EVENTS.SERVER_VOLATILE : WS_EVENTS.SERVER, - this.roomId, + roomId ?? this.roomId, encryptedBuffer, iv, ); @@ -130,11 +132,11 @@ class Portal { }, FILE_UPLOAD_TIMEOUT); broadcastScene = async ( - updateType: WS_SCENE_EVENT_TYPES.INIT | WS_SCENE_EVENT_TYPES.UPDATE, + updateType: WS_SUBTYPES.INIT | WS_SUBTYPES.UPDATE, allElements: readonly ExcalidrawElement[], syncAll: boolean, ) => { - if (updateType === WS_SCENE_EVENT_TYPES.INIT && !syncAll) { + if (updateType === WS_SUBTYPES.INIT && !syncAll) { throw new Error("syncAll must be true when sending SCENE.INIT"); } @@ -183,9 +185,9 @@ class Portal { broadcastIdleChange = (userState: UserIdleState) => { if (this.socket?.id) { const data: SocketUpdateDataSource["IDLE_STATUS"] = { - type: "IDLE_STATUS", + type: WS_SUBTYPES.IDLE_STATUS, payload: { - socketId: this.socket.id, + socketId: this.socket.id as SocketId, userState, username: this.collab.state.username, }, @@ -203,9 +205,9 @@ class Portal { }) => { if (this.socket?.id) { const data: SocketUpdateDataSource["MOUSE_LOCATION"] = { - type: "MOUSE_LOCATION", + type: WS_SUBTYPES.MOUSE_LOCATION, payload: { - socketId: this.socket.id, + socketId: this.socket.id as SocketId, pointer: payload.pointer, button: payload.button || "up", selectedElementIds: @@ -213,12 +215,43 @@ class Portal { username: this.collab.state.username, }, }; + return this._broadcastSocketData( data as SocketUpdateData, true, // volatile ); } }; + + broadcastVisibleSceneBounds = ( + payload: { + sceneBounds: SocketUpdateDataSource["USER_VISIBLE_SCENE_BOUNDS"]["payload"]["sceneBounds"]; + }, + roomId: string, + ) => { + if (this.socket?.id) { + const data: SocketUpdateDataSource["USER_VISIBLE_SCENE_BOUNDS"] = { + type: WS_SUBTYPES.USER_VISIBLE_SCENE_BOUNDS, + payload: { + socketId: this.socket.id as SocketId, + username: this.collab.state.username, + sceneBounds: payload.sceneBounds, + }, + }; + + return this._broadcastSocketData( + data as SocketUpdateData, + true, // volatile + roomId, + ); + } + }; + + broadcastUserFollowed = (payload: OnUserFollowedPayload) => { + if (this.socket?.id) { + this.socket.emit(WS_EVENTS.USER_FOLLOW_CHANGE, payload); + } + }; } export default Portal; diff --git a/excalidraw-app/collab/RoomDialog.tsx b/excalidraw-app/collab/RoomDialog.tsx index 3f2a9e7bc..f2614674d 100644 --- a/excalidraw-app/collab/RoomDialog.tsx +++ b/excalidraw-app/collab/RoomDialog.tsx @@ -1,13 +1,13 @@ import { useRef, useState } from "react"; import * as Popover from "@radix-ui/react-popover"; -import { copyTextToSystemClipboard } from "../../src/clipboard"; -import { trackEvent } from "../../src/analytics"; -import { getFrame } from "../../src/utils"; -import { useI18n } from "../../src/i18n"; -import { KEYS } from "../../src/keys"; +import { copyTextToSystemClipboard } from "../../packages/excalidraw/clipboard"; +import { trackEvent } from "../../packages/excalidraw/analytics"; +import { getFrame } from "../../packages/excalidraw/utils"; +import { useI18n } from "../../packages/excalidraw/i18n"; +import { KEYS } from "../../packages/excalidraw/keys"; -import { Dialog } from "../../src/components/Dialog"; +import { Dialog } from "../../packages/excalidraw/components/Dialog"; import { copyIcon, playerPlayIcon, @@ -16,11 +16,11 @@ import { shareIOS, shareWindows, tablerCheckIcon, -} from "../../src/components/icons"; -import { TextField } from "../../src/components/TextField"; -import { FilledButton } from "../../src/components/FilledButton"; +} from "../../packages/excalidraw/components/icons"; +import { TextField } from "../../packages/excalidraw/components/TextField"; +import { FilledButton } from "../../packages/excalidraw/components/FilledButton"; -import { ReactComponent as CollabImage } from "../../src/assets/lock.svg"; +import { ReactComponent as CollabImage } from "../../packages/excalidraw/assets/lock.svg"; import "./RoomDialog.scss"; const getShareIcon = () => { @@ -120,7 +120,7 @@ export const RoomModal = ({ size="large" variant="icon" label="Share" - startIcon={getShareIcon()} + icon={getShareIcon()} className="RoomDialog__active__share" onClick={shareRoomLink} /> @@ -130,7 +130,7 @@ export const RoomModal = ({ @@ -166,7 +166,7 @@ export const RoomModal = ({ variant="outlined" color="danger" label={t("roomDialog.button_stopSession")} - startIcon={playerStopFilledIcon} + icon={playerStopFilledIcon} onClick={() => { trackEvent("share", "room closed"); onRoomDestroy(); @@ -195,7 +195,7 @@ export const RoomModal = ({ { trackEvent("share", "room creation", `ui (${getFrame()})`); onRoomCreate(); diff --git a/excalidraw-app/collab/reconciliation.ts b/excalidraw-app/collab/reconciliation.ts index 1efc5db46..15e17ed42 100644 --- a/excalidraw-app/collab/reconciliation.ts +++ b/excalidraw-app/collab/reconciliation.ts @@ -1,7 +1,7 @@ -import { PRECEDING_ELEMENT_KEY } from "../../src/constants"; -import { ExcalidrawElement } from "../../src/element/types"; -import { AppState } from "../../src/types"; -import { arrayToMapWithIndex } from "../../src/utils"; +import { PRECEDING_ELEMENT_KEY } from "../../packages/excalidraw/constants"; +import { ExcalidrawElement } from "../../packages/excalidraw/element/types"; +import { AppState } from "../../packages/excalidraw/types"; +import { arrayToMapWithIndex } from "../../packages/excalidraw/utils"; export type ReconciledElements = readonly ExcalidrawElement[] & { _brand: "reconciledElements"; diff --git a/excalidraw-app/components/AppFooter.tsx b/excalidraw-app/components/AppFooter.tsx index 81846f0d3..624873218 100644 --- a/excalidraw-app/components/AppFooter.tsx +++ b/excalidraw-app/components/AppFooter.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { Footer } from "../../src/packages/excalidraw/index"; +import { Footer } from "../../packages/excalidraw/index"; import { EncryptedIcon } from "./EncryptedIcon"; import { ExcalidrawPlusAppLink } from "./ExcalidrawPlusAppLink"; import { isExcalidrawPlusSignedUser } from "../app_constants"; diff --git a/excalidraw-app/components/AppMainMenu.tsx b/excalidraw-app/components/AppMainMenu.tsx index 7a984a8f5..6806c969c 100644 --- a/excalidraw-app/components/AppMainMenu.tsx +++ b/excalidraw-app/components/AppMainMenu.tsx @@ -1,10 +1,10 @@ import React from "react"; -import { PlusPromoIcon } from "../../src/components/icons"; -import { MainMenu } from "../../src/packages/excalidraw/index"; +import { PlusPromoIcon } from "../../packages/excalidraw/components/icons"; +import { MainMenu } from "../../packages/excalidraw/index"; import { LanguageList } from "./LanguageList"; export const AppMainMenu: React.FC<{ - setCollabDialogShown: (toggle: boolean) => any; + onCollabDialogOpen: () => any; isCollaborating: boolean; isCollabEnabled: boolean; }> = React.memo((props) => { @@ -17,7 +17,7 @@ export const AppMainMenu: React.FC<{ {props.isCollabEnabled && ( props.setCollabDialogShown(true)} + onSelect={() => props.onCollabDialogOpen()} /> )} diff --git a/excalidraw-app/components/AppWelcomeScreen.tsx b/excalidraw-app/components/AppWelcomeScreen.tsx index cdefd1fe4..f74bc14e2 100644 --- a/excalidraw-app/components/AppWelcomeScreen.tsx +++ b/excalidraw-app/components/AppWelcomeScreen.tsx @@ -1,12 +1,12 @@ import React from "react"; -import { PlusPromoIcon } from "../../src/components/icons"; -import { useI18n } from "../../src/i18n"; -import { WelcomeScreen } from "../../src/packages/excalidraw/index"; +import { PlusPromoIcon } from "../../packages/excalidraw/components/icons"; +import { useI18n } from "../../packages/excalidraw/i18n"; +import { WelcomeScreen } from "../../packages/excalidraw/index"; import { isExcalidrawPlusSignedUser } from "../app_constants"; -import { POINTER_EVENTS } from "../../src/constants"; +import { POINTER_EVENTS } from "../../packages/excalidraw/constants"; export const AppWelcomeScreen: React.FC<{ - setCollabDialogShown: (toggle: boolean) => any; + onCollabDialogOpen: () => any; isCollabEnabled: boolean; }> = React.memo((props) => { const { t } = useI18n(); @@ -52,7 +52,7 @@ export const AppWelcomeScreen: React.FC<{ {props.isCollabEnabled && ( props.setCollabDialogShown(true)} + onSelect={() => props.onCollabDialogOpen()} /> )} {!isExcalidrawPlusSignedUser && ( diff --git a/excalidraw-app/components/EncryptedIcon.tsx b/excalidraw-app/components/EncryptedIcon.tsx index 84b2f1a0c..3b8655eff 100644 --- a/excalidraw-app/components/EncryptedIcon.tsx +++ b/excalidraw-app/components/EncryptedIcon.tsx @@ -1,6 +1,6 @@ -import { shield } from "../../src/components/icons"; -import { Tooltip } from "../../src/components/Tooltip"; -import { useI18n } from "../../src/i18n"; +import { shield } from "../../packages/excalidraw/components/icons"; +import { Tooltip } from "../../packages/excalidraw/components/Tooltip"; +import { useI18n } from "../../packages/excalidraw/i18n"; export const EncryptedIcon = () => { const { t } = useI18n(); diff --git a/excalidraw-app/components/ExportToExcalidrawPlus.tsx b/excalidraw-app/components/ExportToExcalidrawPlus.tsx index c0818c966..4c566950b 100644 --- a/excalidraw-app/components/ExportToExcalidrawPlus.tsx +++ b/excalidraw-app/components/ExportToExcalidrawPlus.tsx @@ -1,20 +1,30 @@ import React from "react"; -import { Card } from "../../src/components/Card"; -import { ToolButton } from "../../src/components/ToolButton"; -import { serializeAsJSON } from "../../src/data/json"; +import { Card } from "../../packages/excalidraw/components/Card"; +import { ToolButton } from "../../packages/excalidraw/components/ToolButton"; +import { serializeAsJSON } from "../../packages/excalidraw/data/json"; import { loadFirebaseStorage, saveFilesToFirebase } from "../data/firebase"; -import { FileId, NonDeletedExcalidrawElement } from "../../src/element/types"; -import { AppState, BinaryFileData, BinaryFiles } from "../../src/types"; +import { + FileId, + NonDeletedExcalidrawElement, +} from "../../packages/excalidraw/element/types"; +import { + AppState, + BinaryFileData, + BinaryFiles, +} from "../../packages/excalidraw/types"; import { nanoid } from "nanoid"; -import { useI18n } from "../../src/i18n"; -import { encryptData, generateEncryptionKey } from "../../src/data/encryption"; -import { isInitializedImageElement } from "../../src/element/typeChecks"; +import { useI18n } from "../../packages/excalidraw/i18n"; +import { + encryptData, + generateEncryptionKey, +} from "../../packages/excalidraw/data/encryption"; +import { isInitializedImageElement } from "../../packages/excalidraw/element/typeChecks"; import { FILE_UPLOAD_MAX_BYTES } from "../app_constants"; import { encodeFilesForUpload } from "../data/FileManager"; -import { MIME_TYPES } from "../../src/constants"; -import { trackEvent } from "../../src/analytics"; -import { getFrame } from "../../src/utils"; -import { ExcalidrawLogo } from "../../src/components/ExcalidrawLogo"; +import { MIME_TYPES } from "../../packages/excalidraw/constants"; +import { trackEvent } from "../../packages/excalidraw/analytics"; +import { getFrame } from "../../packages/excalidraw/utils"; +import { ExcalidrawLogo } from "../../packages/excalidraw/components/ExcalidrawLogo"; export const exportToExcalidrawPlus = async ( elements: readonly NonDeletedExcalidrawElement[], diff --git a/excalidraw-app/components/GitHubCorner.tsx b/excalidraw-app/components/GitHubCorner.tsx index e0575433d..ad343a899 100644 --- a/excalidraw-app/components/GitHubCorner.tsx +++ b/excalidraw-app/components/GitHubCorner.tsx @@ -1,7 +1,7 @@ import oc from "open-color"; import React from "react"; -import { THEME } from "../../src/constants"; -import { Theme } from "../../src/element/types"; +import { THEME } from "../../packages/excalidraw/constants"; +import { Theme } from "../../packages/excalidraw/element/types"; // https://github.com/tholman/github-corners export const GitHubCorner = React.memo( diff --git a/excalidraw-app/components/LanguageList.tsx b/excalidraw-app/components/LanguageList.tsx index 11d4b6d00..8370d2f3e 100644 --- a/excalidraw-app/components/LanguageList.tsx +++ b/excalidraw-app/components/LanguageList.tsx @@ -1,8 +1,8 @@ import { useSetAtom } from "jotai"; import React from "react"; -import { appLangCodeAtom } from ".."; -import { useI18n } from "../../src/i18n"; -import { languages } from "../../src/i18n"; +import { appLangCodeAtom } from "../App"; +import { useI18n } from "../../packages/excalidraw/i18n"; +import { languages } from "../../packages/excalidraw/i18n"; export const LanguageList = ({ style }: { style?: React.CSSProperties }) => { const { t, langCode } = useI18n(); diff --git a/src/components/TopErrorBoundary.tsx b/excalidraw-app/components/TopErrorBoundary.tsx similarity index 97% rename from src/components/TopErrorBoundary.tsx rename to excalidraw-app/components/TopErrorBoundary.tsx index d465514f1..f796906d6 100644 --- a/src/components/TopErrorBoundary.tsx +++ b/excalidraw-app/components/TopErrorBoundary.tsx @@ -1,7 +1,7 @@ import React from "react"; import * as Sentry from "@sentry/browser"; -import { t } from "../i18n"; -import Trans from "./Trans"; +import { t } from "../../packages/excalidraw/i18n"; +import Trans from "../../packages/excalidraw/components/Trans"; interface TopErrorBoundaryState { hasError: boolean; diff --git a/excalidraw-app/data/FileManager.ts b/excalidraw-app/data/FileManager.ts index 426afc4d3..5a47a4a48 100644 --- a/excalidraw-app/data/FileManager.ts +++ b/excalidraw-app/data/FileManager.ts @@ -1,19 +1,19 @@ -import { compressData } from "../../src/data/encode"; -import { newElementWith } from "../../src/element/mutateElement"; -import { isInitializedImageElement } from "../../src/element/typeChecks"; +import { compressData } from "../../packages/excalidraw/data/encode"; +import { newElementWith } from "../../packages/excalidraw/element/mutateElement"; +import { isInitializedImageElement } from "../../packages/excalidraw/element/typeChecks"; import { ExcalidrawElement, ExcalidrawImageElement, FileId, InitializedExcalidrawImageElement, -} from "../../src/element/types"; -import { t } from "../../src/i18n"; +} from "../../packages/excalidraw/element/types"; +import { t } from "../../packages/excalidraw/i18n"; import { BinaryFileData, BinaryFileMetadata, ExcalidrawImperativeAPI, BinaryFiles, -} from "../../src/types"; +} from "../../packages/excalidraw/types"; export class FileManager { /** files being fetched */ diff --git a/excalidraw-app/data/LocalData.ts b/excalidraw-app/data/LocalData.ts index 25f068aaa..a8a6c41b2 100644 --- a/excalidraw-app/data/LocalData.ts +++ b/excalidraw-app/data/LocalData.ts @@ -11,11 +11,18 @@ */ import { createStore, entries, del, getMany, set, setMany } from "idb-keyval"; -import { clearAppStateForLocalStorage } from "../../src/appState"; -import { clearElementsForLocalStorage } from "../../src/element"; -import { ExcalidrawElement, FileId } from "../../src/element/types"; -import { AppState, BinaryFileData, BinaryFiles } from "../../src/types"; -import { debounce } from "../../src/utils"; +import { clearAppStateForLocalStorage } from "../../packages/excalidraw/appState"; +import { clearElementsForLocalStorage } from "../../packages/excalidraw/element"; +import { + ExcalidrawElement, + FileId, +} from "../../packages/excalidraw/element/types"; +import { + AppState, + BinaryFileData, + BinaryFiles, +} from "../../packages/excalidraw/types"; +import { debounce } from "../../packages/excalidraw/utils"; import { SAVE_TO_LOCAL_STORAGE_TIMEOUT, STORAGE_KEYS } from "../app_constants"; import { FileManager } from "./FileManager"; import { Locker } from "./Locker"; diff --git a/excalidraw-app/data/firebase.ts b/excalidraw-app/data/firebase.ts index 831213d60..f37fbbd81 100644 --- a/excalidraw-app/data/firebase.ts +++ b/excalidraw-app/data/firebase.ts @@ -1,20 +1,27 @@ -import { ExcalidrawElement, FileId } from "../../src/element/types"; -import { getSceneVersion } from "../../src/element"; +import { + ExcalidrawElement, + FileId, +} from "../../packages/excalidraw/element/types"; +import { getSceneVersion } from "../../packages/excalidraw/element"; import Portal from "../collab/Portal"; -import { restoreElements } from "../../src/data/restore"; +import { restoreElements } from "../../packages/excalidraw/data/restore"; import { AppState, BinaryFileData, BinaryFileMetadata, DataURL, -} from "../../src/types"; +} from "../../packages/excalidraw/types"; import { FILE_CACHE_MAX_AGE_SEC } from "../app_constants"; -import { decompressData } from "../../src/data/encode"; -import { encryptData, decryptData } from "../../src/data/encryption"; -import { MIME_TYPES } from "../../src/constants"; +import { decompressData } from "../../packages/excalidraw/data/encode"; +import { + encryptData, + decryptData, +} from "../../packages/excalidraw/data/encryption"; +import { MIME_TYPES } from "../../packages/excalidraw/constants"; import { reconcileElements } from "../collab/reconciliation"; import { getSyncableElements, SyncableExcalidrawElement } from "."; -import { ResolutionType } from "../../src/utility-types"; +import { ResolutionType } from "../../packages/excalidraw/utility-types"; +import type { Socket } from "socket.io-client"; // private // ----------------------------------------------------------------------------- @@ -132,12 +139,12 @@ const decryptElements = async ( }; class FirebaseSceneVersionCache { - private static cache = new WeakMap(); - static get = (socket: SocketIOClient.Socket) => { + private static cache = new WeakMap(); + static get = (socket: Socket) => { return FirebaseSceneVersionCache.cache.get(socket); }; static set = ( - socket: SocketIOClient.Socket, + socket: Socket, elements: readonly SyncableExcalidrawElement[], ) => { FirebaseSceneVersionCache.cache.set(socket, getSceneVersion(elements)); @@ -279,7 +286,7 @@ export const saveToFirebase = async ( export const loadFromFirebase = async ( roomId: string, roomKey: string, - socket: SocketIOClient.Socket | null, + socket: Socket | null, ): Promise => { const firebase = await loadFirestore(); const db = firebase.firestore(); diff --git a/excalidraw-app/data/index.ts b/excalidraw-app/data/index.ts index 4dfb78017..5699568b4 100644 --- a/excalidraw-app/data/index.ts +++ b/excalidraw-app/data/index.ts @@ -1,27 +1,36 @@ -import { compressData, decompressData } from "../../src/data/encode"; +import { + compressData, + decompressData, +} from "../../packages/excalidraw/data/encode"; import { decryptData, generateEncryptionKey, IV_LENGTH_BYTES, -} from "../../src/data/encryption"; -import { serializeAsJSON } from "../../src/data/json"; -import { restore } from "../../src/data/restore"; -import { ImportedDataState } from "../../src/data/types"; -import { isInvisiblySmallElement } from "../../src/element/sizeHelpers"; -import { isInitializedImageElement } from "../../src/element/typeChecks"; -import { ExcalidrawElement, FileId } from "../../src/element/types"; -import { t } from "../../src/i18n"; +} from "../../packages/excalidraw/data/encryption"; +import { serializeAsJSON } from "../../packages/excalidraw/data/json"; +import { restore } from "../../packages/excalidraw/data/restore"; +import { ImportedDataState } from "../../packages/excalidraw/data/types"; +import { SceneBounds } from "../../packages/excalidraw/element/bounds"; +import { isInvisiblySmallElement } from "../../packages/excalidraw/element/sizeHelpers"; +import { isInitializedImageElement } from "../../packages/excalidraw/element/typeChecks"; +import { + ExcalidrawElement, + FileId, +} from "../../packages/excalidraw/element/types"; +import { t } from "../../packages/excalidraw/i18n"; import { AppState, BinaryFileData, BinaryFiles, + SocketId, UserIdleState, -} from "../../src/types"; -import { bytesToHexString } from "../../src/utils"; +} from "../../packages/excalidraw/types"; +import { bytesToHexString } from "../../packages/excalidraw/utils"; import { DELETED_ELEMENT_TIMEOUT, FILE_UPLOAD_MAX_BYTES, ROOM_ID_BYTES, + WS_SUBTYPES, } from "../app_constants"; import { encodeFilesForUpload } from "./FileManager"; import { saveFilesToFirebase } from "./firebase"; @@ -56,67 +65,49 @@ const generateRoomId = async () => { return bytesToHexString(buffer); }; -/** - * Right now the reason why we resolve connection params (url, polling...) - * from upstream is to allow changing the params immediately when needed without - * having to wait for clients to update the SW. - * - * If REACT_APP_WS_SERVER_URL env is set, we use that instead (useful for forks) - */ -export const getCollabServer = async (): Promise<{ - url: string; - polling: boolean; -}> => { - if (import.meta.env.VITE_APP_WS_SERVER_URL) { - return { - url: import.meta.env.VITE_APP_WS_SERVER_URL, - polling: true, - }; - } - - try { - const resp = await fetch( - `${import.meta.env.VITE_APP_PORTAL_URL}/collab-server`, - ); - return await resp.json(); - } catch (error) { - console.error(error); - throw new Error(t("errors.cannotResolveCollabServer")); - } -}; - export type EncryptedData = { data: ArrayBuffer; iv: Uint8Array; }; export type SocketUpdateDataSource = { + INVALID_RESPONSE: { + type: WS_SUBTYPES.INVALID_RESPONSE; + }; SCENE_INIT: { - type: "SCENE_INIT"; + type: WS_SUBTYPES.INIT; payload: { elements: readonly ExcalidrawElement[]; }; }; SCENE_UPDATE: { - type: "SCENE_UPDATE"; + type: WS_SUBTYPES.UPDATE; payload: { elements: readonly ExcalidrawElement[]; }; }; MOUSE_LOCATION: { - type: "MOUSE_LOCATION"; + type: WS_SUBTYPES.MOUSE_LOCATION; payload: { - socketId: string; + socketId: SocketId; pointer: { x: number; y: number; tool: "pointer" | "laser" }; button: "down" | "up"; selectedElementIds: AppState["selectedElementIds"]; username: string; }; }; - IDLE_STATUS: { - type: "IDLE_STATUS"; + USER_VISIBLE_SCENE_BOUNDS: { + type: WS_SUBTYPES.USER_VISIBLE_SCENE_BOUNDS; payload: { - socketId: string; + socketId: SocketId; + username: string; + sceneBounds: SceneBounds; + }; + }; + IDLE_STATUS: { + type: WS_SUBTYPES.IDLE_STATUS; + payload: { + socketId: SocketId; userState: UserIdleState; username: string; }; @@ -124,10 +115,7 @@ export type SocketUpdateDataSource = { }; export type SocketUpdateDataIncoming = - | SocketUpdateDataSource[keyof SocketUpdateDataSource] - | { - type: "INVALID_RESPONSE"; - }; + SocketUpdateDataSource[keyof SocketUpdateDataSource]; export type SocketUpdateData = SocketUpdateDataSource[keyof SocketUpdateDataSource] & { diff --git a/excalidraw-app/data/localStorage.ts b/excalidraw-app/data/localStorage.ts index 2a1b93ebf..ce4258f4e 100644 --- a/excalidraw-app/data/localStorage.ts +++ b/excalidraw-app/data/localStorage.ts @@ -1,12 +1,12 @@ -import { ExcalidrawElement } from "../../src/element/types"; -import { AppState } from "../../src/types"; +import { ExcalidrawElement } from "../../packages/excalidraw/element/types"; +import { AppState } from "../../packages/excalidraw/types"; import { clearAppStateForLocalStorage, getDefaultAppState, -} from "../../src/appState"; -import { clearElementsForLocalStorage } from "../../src/element"; +} from "../../packages/excalidraw/appState"; +import { clearElementsForLocalStorage } from "../../packages/excalidraw/element"; import { STORAGE_KEYS } from "../app_constants"; -import { ImportedDataState } from "../../src/data/types"; +import { ImportedDataState } from "../../packages/excalidraw/data/types"; export const saveUsernameToLocalStorage = (username: string) => { try { diff --git a/excalidraw-app/global.d.ts b/excalidraw-app/global.d.ts new file mode 100644 index 000000000..1ce684585 --- /dev/null +++ b/excalidraw-app/global.d.ts @@ -0,0 +1,3 @@ +interface Window { + __EXCALIDRAW_SHA__: string | undefined; +} diff --git a/index.html b/excalidraw-app/index.html similarity index 98% rename from index.html rename to excalidraw-app/index.html index b1e0f2abc..66f3afdab 100644 --- a/index.html +++ b/excalidraw-app/index.html @@ -121,7 +121,7 @@ crossorigin="anonymous" /> - + <% if ("%VITE_APP_DEV_DISABLE_LIVE_RELOAD%"==="true" ) { %> + <% if ("%VITE_APP_DEV_DISABLE_LIVE_RELOAD%" !== 'true') { %> + ``` + - `appState.openDialog` type was changed from `null | string` to `null | { name: string }`. [#7336](https://github.com/excalidraw/excalidraw/pull/7336) +## 0.17.3 (2024-02-09) + +### Fixes + +- Keep customData when converting to ExcalidrawElement. [#7656](https://github.com/excalidraw/excalidraw/pull/7656) + +- Umd build for browser since it was breaking in v0.17.0 [#7349](https://github.com/excalidraw/excalidraw/pull/7349). Also make sure that when using `Vite`, the `process.env.IS_PREACT` is set as `"true"` (string) and not a boolean. + +``` +define: { + "process.env.IS_PREACT": JSON.stringify("true"), +} +``` + +- Disable caching bounds for arrow labels [#7343](https://github.com/excalidraw/excalidraw/pull/7343) + +- Bounds cached prematurely resulting in incorrectly rendered labels [#7339](https://github.com/excalidraw/excalidraw/pull/7339) + +## Excalidraw Library + +### Fixes + +- Disable caching bounds for arrow labels [#7343](https://github.com/excalidraw/excalidraw/pull/7343) + +--- + ## 0.17.0 (2023-11-14) ### Features @@ -212,7 +282,7 @@ Please add the latest change on the top under the correct section. - Add a `subtype` attribute to `ExcalidrawElement` to allow self-contained extensions of any `ExcalidrawElement` type. Implement MathJax support on stem.excalidraw.com as a `math` subtype of `ExcalidrawTextElement`. Both standard Latex input and simplified AsciiMath input are supported. [#6037](https://github.com/excalidraw/excalidraw/pull/6037). - Support creating containers, linear elements, text containers, labelled arrows and arrow bindings programatically [#6546](https://github.com/excalidraw/excalidraw/pull/6546) - Introducing Web-Embeds (alias iframe element)[#6691](https://github.com/excalidraw/excalidraw/pull/6691) -- Added [`props.validateEmbeddable`](https://docs.excalidraw.com/docs/@excalidraw/excalidraw/api/props#validateEmbeddable) to customize embeddable src url validation. [#6691](https://github.com/excalidraw/excalidraw/pull/6691) +- Added [`props.validateEmbeddable`](https://docs.excalidraw.com/docs/@excalidraw/excalidraw/api/props#validateembeddable) to customize embeddable src url validation. [#6691](https://github.com/excalidraw/excalidraw/pull/6691) - Add support for `opts.fitToViewport` and `opts.viewportZoomFactor` in the [`ExcalidrawAPI.scrollToContent`](https://docs.excalidraw.com/docs/@excalidraw/excalidraw/api/props/excalidraw-api#scrolltocontent) API. [#6581](https://github.com/excalidraw/excalidraw/pull/6581). - Properly sanitize element `link` urls. [#6728](https://github.com/excalidraw/excalidraw/pull/6728). - Sidebar component now supports tabs — for more detailed description of new behavior and breaking changes, see the linked PR. [#6213](https://github.com/excalidraw/excalidraw/pull/6213) diff --git a/src/packages/excalidraw/README.md b/packages/excalidraw/README.md similarity index 100% rename from src/packages/excalidraw/README.md rename to packages/excalidraw/README.md diff --git a/src/actions/actionAddToLibrary.ts b/packages/excalidraw/actions/actionAddToLibrary.ts similarity index 100% rename from src/actions/actionAddToLibrary.ts rename to packages/excalidraw/actions/actionAddToLibrary.ts diff --git a/src/actions/actionAlign.tsx b/packages/excalidraw/actions/actionAlign.tsx similarity index 98% rename from src/actions/actionAlign.tsx rename to packages/excalidraw/actions/actionAlign.tsx index 137f68ae9..8d7d36217 100644 --- a/src/actions/actionAlign.tsx +++ b/packages/excalidraw/actions/actionAlign.tsx @@ -40,8 +40,13 @@ const alignSelectedElements = ( alignment: Alignment, ) => { const selectedElements = app.scene.getSelectedElements(appState); + const elementsMap = arrayToMap(elements); - const updatedElements = alignElements(selectedElements, alignment); + const updatedElements = alignElements( + selectedElements, + elementsMap, + alignment, + ); const updatedElementsMap = arrayToMap(updatedElements); diff --git a/src/actions/actionBoundText.tsx b/packages/excalidraw/actions/actionBoundText.tsx similarity index 96% rename from src/actions/actionBoundText.tsx rename to packages/excalidraw/actions/actionBoundText.tsx index 3d8b37dbb..5084bd911 100644 --- a/src/actions/actionBoundText.tsx +++ b/packages/excalidraw/actions/actionBoundText.tsx @@ -17,7 +17,7 @@ import { getOriginalContainerHeightFromCache, resetOriginalContainerCache, updateOriginalContainerCache, -} from "../element/textWysiwyg"; +} from "../element/containerCache"; import { hasBoundTextElement, isTextBindableContainer, @@ -44,8 +44,9 @@ export const actionUnbindText = register({ }, perform: (elements, appState, _, app) => { const selectedElements = app.scene.getSelectedElements(appState); + const elementsMap = app.scene.getNonDeletedElementsMap(); selectedElements.forEach((element) => { - const boundTextElement = getBoundTextElement(element); + const boundTextElement = getBoundTextElement(element, elementsMap); if (boundTextElement) { const { width, height, baseline } = measureTextElement( boundTextElement, @@ -106,7 +107,10 @@ export const actionBindText = register({ if ( textElement && bindingContainer && - getBoundTextElement(bindingContainer) === null + getBoundTextElement( + bindingContainer, + app.scene.getNonDeletedElementsMap(), + ) === null ) { return true; } diff --git a/src/actions/actionCanvas.tsx b/packages/excalidraw/actions/actionCanvas.tsx similarity index 92% rename from src/actions/actionCanvas.tsx rename to packages/excalidraw/actions/actionCanvas.tsx index 2194f63b1..73a0dd3bc 100644 --- a/src/actions/actionCanvas.tsx +++ b/packages/excalidraw/actions/actionCanvas.tsx @@ -20,7 +20,7 @@ import { isHandToolActive, } from "../appState"; import { DEFAULT_CANVAS_BACKGROUND_PICKS } from "../colors"; -import { Bounds } from "../element/bounds"; +import { SceneBounds } from "../element/bounds"; import { setCursor } from "../cursor"; export const actionChangeViewBackgroundColor = register({ @@ -109,6 +109,7 @@ export const actionZoomIn = register({ }, appState, ), + userToFollow: null, }, commitToHistory: false, }; @@ -146,6 +147,7 @@ export const actionZoomOut = register({ }, appState, ), + userToFollow: null, }, commitToHistory: false, }; @@ -183,6 +185,7 @@ export const actionResetZoom = register({ }, appState, ), + userToFollow: null, }, commitToHistory: false, }; @@ -208,7 +211,7 @@ export const actionResetZoom = register({ }); const zoomValueToFitBoundsOnViewport = ( - bounds: Bounds, + bounds: SceneBounds, viewportDimensions: { width: number; height: number }, ) => { const [x1, y1, x2, y2] = bounds; @@ -226,22 +229,20 @@ const zoomValueToFitBoundsOnViewport = ( return clampedZoomValueToFitElements as NormalizedZoomValue; }; -export const zoomToFit = ({ - targetElements, +export const zoomToFitBounds = ({ + bounds, appState, fitToViewport = false, viewportZoomFactor = 0.7, }: { - targetElements: readonly ExcalidrawElement[]; + bounds: SceneBounds; appState: Readonly; /** whether to fit content to viewport (beyond >100%) */ fitToViewport: boolean; /** zoom content to cover X of the viewport, when fitToViewport=true */ viewportZoomFactor?: number; }) => { - const commonBounds = getCommonBounds(getNonDeletedElements(targetElements)); - - const [x1, y1, x2, y2] = commonBounds; + const [x1, y1, x2, y2] = bounds; const centerX = (x1 + x2) / 2; const centerY = (y1 + y2) / 2; @@ -268,7 +269,7 @@ export const zoomToFit = ({ scrollX = (appState.width / 2) * (1 / newZoomValue) - centerX; scrollY = (appState.height / 2) * (1 / newZoomValue) - centerY; } else { - newZoomValue = zoomValueToFitBoundsOnViewport(commonBounds, { + newZoomValue = zoomValueToFitBoundsOnViewport(bounds, { width: appState.width, height: appState.height, }); @@ -297,6 +298,29 @@ export const zoomToFit = ({ }; }; +export const zoomToFit = ({ + targetElements, + appState, + fitToViewport, + viewportZoomFactor, +}: { + targetElements: readonly ExcalidrawElement[]; + appState: Readonly; + /** whether to fit content to viewport (beyond >100%) */ + fitToViewport: boolean; + /** zoom content to cover X of the viewport, when fitToViewport=true */ + viewportZoomFactor?: number; +}) => { + const commonBounds = getCommonBounds(getNonDeletedElements(targetElements)); + + return zoomToFitBounds({ + bounds: commonBounds, + appState, + fitToViewport, + viewportZoomFactor, + }); +}; + // Note, this action differs from actionZoomToFitSelection in that it doesn't // zoom beyond 100%. In other words, if the content is smaller than viewport // size, it won't be zoomed in. @@ -307,7 +331,10 @@ export const actionZoomToFitSelectionInViewport = register({ const selectedElements = app.scene.getSelectedElements(appState); return zoomToFit({ targetElements: selectedElements.length ? selectedElements : elements, - appState, + appState: { + ...appState, + userToFollow: null, + }, fitToViewport: false, }); }, @@ -327,7 +354,10 @@ export const actionZoomToFitSelection = register({ const selectedElements = app.scene.getSelectedElements(appState); return zoomToFit({ targetElements: selectedElements.length ? selectedElements : elements, - appState, + appState: { + ...appState, + userToFollow: null, + }, fitToViewport: true, }); }, @@ -344,7 +374,14 @@ export const actionZoomToFit = register({ viewMode: true, trackEvent: { category: "canvas" }, perform: (elements, appState) => - zoomToFit({ targetElements: elements, appState, fitToViewport: false }), + zoomToFit({ + targetElements: elements, + appState: { + ...appState, + userToFollow: null, + }, + fitToViewport: false, + }), keyTest: (event) => event.code === CODES.ONE && event.shiftKey && diff --git a/src/actions/actionClipboard.tsx b/packages/excalidraw/actions/actionClipboard.tsx similarity index 100% rename from src/actions/actionClipboard.tsx rename to packages/excalidraw/actions/actionClipboard.tsx diff --git a/src/actions/actionDeleteSelected.tsx b/packages/excalidraw/actions/actionDeleteSelected.tsx similarity index 100% rename from src/actions/actionDeleteSelected.tsx rename to packages/excalidraw/actions/actionDeleteSelected.tsx diff --git a/src/actions/actionDistribute.tsx b/packages/excalidraw/actions/actionDistribute.tsx similarity index 96% rename from src/actions/actionDistribute.tsx rename to packages/excalidraw/actions/actionDistribute.tsx index bf51bedf4..be48bc870 100644 --- a/src/actions/actionDistribute.tsx +++ b/packages/excalidraw/actions/actionDistribute.tsx @@ -32,7 +32,11 @@ const distributeSelectedElements = ( ) => { const selectedElements = app.scene.getSelectedElements(appState); - const updatedElements = distributeElements(selectedElements, distribution); + const updatedElements = distributeElements( + selectedElements, + app.scene.getNonDeletedElementsMap(), + distribution, + ); const updatedElementsMap = arrayToMap(updatedElements); diff --git a/src/actions/actionDuplicateSelection.tsx b/packages/excalidraw/actions/actionDuplicateSelection.tsx similarity index 99% rename from src/actions/actionDuplicateSelection.tsx rename to packages/excalidraw/actions/actionDuplicateSelection.tsx index ba079168e..7126f549e 100644 --- a/src/actions/actionDuplicateSelection.tsx +++ b/packages/excalidraw/actions/actionDuplicateSelection.tsx @@ -139,7 +139,7 @@ const duplicateElements = ( continue; } - const boundTextElement = getBoundTextElement(element); + const boundTextElement = getBoundTextElement(element, arrayToMap(elements)); const isElementAFrameLike = isFrameLikeElement(element); if (idsOfElementsToDuplicate.get(element.id)) { diff --git a/src/actions/actionElementLock.test.tsx b/packages/excalidraw/actions/actionElementLock.test.tsx similarity index 97% rename from src/actions/actionElementLock.test.tsx rename to packages/excalidraw/actions/actionElementLock.test.tsx index 19db5e325..244ccd08b 100644 --- a/src/actions/actionElementLock.test.tsx +++ b/packages/excalidraw/actions/actionElementLock.test.tsx @@ -1,4 +1,4 @@ -import { Excalidraw } from "../packages/excalidraw/index"; +import { Excalidraw } from "../index"; import { queryByTestId, fireEvent } from "@testing-library/react"; import { render } from "../tests/test-utils"; import { Pointer, UI } from "../tests/helpers/ui"; diff --git a/src/actions/actionElementLock.ts b/packages/excalidraw/actions/actionElementLock.ts similarity index 100% rename from src/actions/actionElementLock.ts rename to packages/excalidraw/actions/actionElementLock.ts diff --git a/src/actions/actionExport.tsx b/packages/excalidraw/actions/actionExport.tsx similarity index 100% rename from src/actions/actionExport.tsx rename to packages/excalidraw/actions/actionExport.tsx diff --git a/src/actions/actionFinalize.tsx b/packages/excalidraw/actions/actionFinalize.tsx similarity index 100% rename from src/actions/actionFinalize.tsx rename to packages/excalidraw/actions/actionFinalize.tsx diff --git a/src/actions/actionFlip.ts b/packages/excalidraw/actions/actionFlip.ts similarity index 72% rename from src/actions/actionFlip.ts rename to packages/excalidraw/actions/actionFlip.ts index 12d5e2e48..c760af44d 100644 --- a/src/actions/actionFlip.ts +++ b/packages/excalidraw/actions/actionFlip.ts @@ -1,9 +1,14 @@ import { register } from "./register"; import { getSelectedElements } from "../scene"; import { getNonDeletedElements } from "../element"; -import { ExcalidrawElement, NonDeleted } from "../element/types"; +import { + ExcalidrawElement, + NonDeleted, + NonDeletedElementsMap, + NonDeletedSceneElementsMap, +} from "../element/types"; import { resizeMultipleElements } from "../element/resizeElements"; -import { AppState, PointerDownState } from "../types"; +import { AppState } from "../types"; import { arrayToMap } from "../utils"; import { CODES, KEYS } from "../keys"; import { getCommonBoundingBox } from "../element/bounds"; @@ -20,7 +25,12 @@ export const actionFlipHorizontal = register({ perform: (elements, appState, _, app) => { return { elements: updateFrameMembershipOfSelectedElements( - flipSelectedElements(elements, appState, "horizontal"), + flipSelectedElements( + elements, + app.scene.getNonDeletedElementsMap(), + appState, + "horizontal", + ), appState, app, ), @@ -38,7 +48,12 @@ export const actionFlipVertical = register({ perform: (elements, appState, _, app) => { return { elements: updateFrameMembershipOfSelectedElements( - flipSelectedElements(elements, appState, "vertical"), + flipSelectedElements( + elements, + app.scene.getNonDeletedElementsMap(), + appState, + "vertical", + ), appState, app, ), @@ -53,6 +68,7 @@ export const actionFlipVertical = register({ const flipSelectedElements = ( elements: readonly ExcalidrawElement[], + elementsMap: NonDeletedElementsMap | NonDeletedSceneElementsMap, appState: Readonly, flipDirection: "horizontal" | "vertical", ) => { @@ -67,6 +83,7 @@ const flipSelectedElements = ( const updatedElements = flipElements( selectedElements, + elementsMap, appState, flipDirection, ); @@ -79,15 +96,17 @@ const flipSelectedElements = ( }; const flipElements = ( - elements: NonDeleted[], + selectedElements: NonDeleted[], + elementsMap: NonDeletedElementsMap | NonDeletedSceneElementsMap, appState: AppState, flipDirection: "horizontal" | "vertical", ): ExcalidrawElement[] => { - const { minX, minY, maxX, maxY } = getCommonBoundingBox(elements); + const { minX, minY, maxX, maxY } = getCommonBoundingBox(selectedElements); resizeMultipleElements( - { originalElements: arrayToMap(elements) } as PointerDownState, - elements, + elementsMap, + selectedElements, + elementsMap, "nw", true, flipDirection === "horizontal" ? maxX : minX, @@ -96,7 +115,7 @@ const flipElements = ( (isBindingEnabled(appState) ? bindOrUnbindSelectedElements - : unbindLinearElements)(elements); + : unbindLinearElements)(selectedElements); - return elements; + return selectedElements; }; diff --git a/src/actions/actionFrame.ts b/packages/excalidraw/actions/actionFrame.ts similarity index 96% rename from src/actions/actionFrame.ts rename to packages/excalidraw/actions/actionFrame.ts index 4cddb2ac0..8232db3cd 100644 --- a/src/actions/actionFrame.ts +++ b/packages/excalidraw/actions/actionFrame.ts @@ -63,11 +63,7 @@ export const actionRemoveAllElementsFromFrame = register({ if (isFrameLikeElement(selectedElement)) { return { - elements: removeAllElementsFromFrame( - elements, - selectedElement, - appState, - ), + elements: removeAllElementsFromFrame(elements, selectedElement), appState: { ...appState, selectedElementIds: { diff --git a/src/actions/actionGroup.tsx b/packages/excalidraw/actions/actionGroup.tsx similarity index 98% rename from src/actions/actionGroup.tsx rename to packages/excalidraw/actions/actionGroup.tsx index e6cb05840..44523857a 100644 --- a/src/actions/actionGroup.tsx +++ b/packages/excalidraw/actions/actionGroup.tsx @@ -105,10 +105,9 @@ export const actionGroup = register({ const frameElementsMap = groupByFrameLikes(selectedElements); frameElementsMap.forEach((elementsInFrame, frameId) => { - nextElements = removeElementsFromFrame( - nextElements, + removeElementsFromFrame( elementsInFrame, - appState, + app.scene.getNonDeletedElementsMap(), ); }); } @@ -229,7 +228,7 @@ export const actionUngroup = register({ nextElements, getElementsInResizingFrame(nextElements, frame, appState), frame, - appState, + app, ); } }); diff --git a/src/actions/actionHistory.tsx b/packages/excalidraw/actions/actionHistory.tsx similarity index 100% rename from src/actions/actionHistory.tsx rename to packages/excalidraw/actions/actionHistory.tsx diff --git a/src/actions/actionLinearEditor.ts b/packages/excalidraw/actions/actionLinearEditor.ts similarity index 100% rename from src/actions/actionLinearEditor.ts rename to packages/excalidraw/actions/actionLinearEditor.ts diff --git a/src/actions/actionMenu.tsx b/packages/excalidraw/actions/actionMenu.tsx similarity index 100% rename from src/actions/actionMenu.tsx rename to packages/excalidraw/actions/actionMenu.tsx diff --git a/packages/excalidraw/actions/actionNavigate.tsx b/packages/excalidraw/actions/actionNavigate.tsx new file mode 100644 index 000000000..ea65584fe --- /dev/null +++ b/packages/excalidraw/actions/actionNavigate.tsx @@ -0,0 +1,85 @@ +import { getClientColor } from "../clients"; +import { Avatar } from "../components/Avatar"; +import { GoToCollaboratorComponentProps } from "../components/UserList"; +import { eyeIcon } from "../components/icons"; +import { t } from "../i18n"; +import { Collaborator } from "../types"; +import { register } from "./register"; + +export const actionGoToCollaborator = register({ + name: "goToCollaborator", + viewMode: true, + trackEvent: { category: "collab" }, + perform: (_elements, appState, collaborator: Collaborator) => { + if ( + !collaborator.socketId || + appState.userToFollow?.socketId === collaborator.socketId || + collaborator.isCurrentUser + ) { + return { + appState: { + ...appState, + userToFollow: null, + }, + commitToHistory: false, + }; + } + + return { + appState: { + ...appState, + userToFollow: { + socketId: collaborator.socketId, + username: collaborator.username || "", + }, + // Close mobile menu + openMenu: appState.openMenu === "canvas" ? null : appState.openMenu, + }, + commitToHistory: false, + }; + }, + PanelComponent: ({ updateData, data, appState }) => { + const { clientId, collaborator, withName, isBeingFollowed } = + data as GoToCollaboratorComponentProps; + + const background = getClientColor(clientId); + + return withName ? ( +
updateData(collaborator)} + > + {}} + name={collaborator.username || ""} + src={collaborator.avatarUrl} + isBeingFollowed={isBeingFollowed} + isCurrentUser={collaborator.isCurrentUser === true} + /> +
+ {collaborator.username} +
+
+ {eyeIcon} +
+
+ ) : ( + { + updateData(collaborator); + }} + name={collaborator.username || ""} + src={collaborator.avatarUrl} + isBeingFollowed={isBeingFollowed} + isCurrentUser={collaborator.isCurrentUser === true} + /> + ); + }, +}); diff --git a/src/actions/actionProperties.test.tsx b/packages/excalidraw/actions/actionProperties.test.tsx similarity index 98% rename from src/actions/actionProperties.test.tsx rename to packages/excalidraw/actions/actionProperties.test.tsx index 3122cd108..2e1690107 100644 --- a/src/actions/actionProperties.test.tsx +++ b/packages/excalidraw/actions/actionProperties.test.tsx @@ -1,4 +1,4 @@ -import { Excalidraw } from "../packages/excalidraw/index"; +import { Excalidraw } from "../index"; import { queryByTestId } from "@testing-library/react"; import { render } from "../tests/test-utils"; import { UI } from "../tests/helpers/ui"; diff --git a/src/actions/actionProperties.tsx b/packages/excalidraw/actions/actionProperties.tsx similarity index 86% rename from src/actions/actionProperties.tsx rename to packages/excalidraw/actions/actionProperties.tsx index d17a87e36..79e50aa68 100644 --- a/src/actions/actionProperties.tsx +++ b/packages/excalidraw/actions/actionProperties.tsx @@ -1,4 +1,4 @@ -import { AppState, Primitive } from "../../src/types"; +import { AppClassProperties, AppState, Primitive } from "../types"; import { DEFAULT_ELEMENT_BACKGROUND_COLOR_PALETTE, DEFAULT_ELEMENT_BACKGROUND_PICKS, @@ -15,7 +15,7 @@ import { IconPicker } from "../components/IconPicker"; import { ArrowheadArrowIcon, ArrowheadBarIcon, - ArrowheadDotIcon, + ArrowheadCircleIcon, ArrowheadTriangleIcon, ArrowheadNoneIcon, StrokeStyleDashedIcon, @@ -45,6 +45,10 @@ import { TextAlignCenterIcon, TextAlignRightIcon, FillZigZagIcon, + ArrowheadTriangleOutlineIcon, + ArrowheadCircleOutlineIcon, + ArrowheadDiamondIcon, + ArrowheadDiamondOutlineIcon, } from "../components/icons"; import { DEFAULT_FONT_FAMILY, @@ -62,7 +66,6 @@ import { import { mutateElement, newElementWith } from "../element/mutateElement"; import { getBoundTextElement, - getContainerElement, getDefaultLineHeight, } from "../element/textElement"; import { @@ -185,6 +188,7 @@ const offsetElementAfterFontResize = ( const changeFontSize = ( elements: readonly ExcalidrawElement[], appState: AppState, + app: AppClassProperties, getNewFontSize: (element: ExcalidrawTextElement) => number, fallbackValue?: ExcalidrawTextElement["fontSize"], ) => { @@ -202,7 +206,10 @@ const changeFontSize = ( let newElement: ExcalidrawTextElement = newElementWith(oldElement, { fontSize: newFontSize, }); - redrawTextBoundingBox(newElement, getContainerElement(oldElement)); + redrawTextBoundingBox( + newElement, + app.scene.getContainerElement(oldElement), + ); newElement = offsetElementAfterFontResize(oldElement, newElement); @@ -596,10 +603,10 @@ export const actionChangeOpacity = register({ export const actionChangeFontSize = register({ name: "changeFontSize", trackEvent: false, - perform: (elements, appState, value) => { - return changeFontSize(elements, appState, () => value, value); + perform: (elements, appState, value, app) => { + return changeFontSize(elements, appState, app, () => value, value); }, - PanelComponent: ({ elements, appState, updateData }) => ( + PanelComponent: ({ elements, appState, updateData, app }) => (
{t("labels.fontSize")} - isTextElement(element) || getBoundTextElement(element) !== null, + isTextElement(element) || + getBoundTextElement( + element, + app.scene.getNonDeletedElementsMap(), + ) !== null, (hasSelection) => hasSelection ? null @@ -659,8 +673,8 @@ export const actionChangeFontSize = register({ export const actionDecreaseFontSize = register({ name: "decreaseFontSize", trackEvent: false, - perform: (elements, appState, value) => { - return changeFontSize(elements, appState, (element) => + perform: (elements, appState, value, app) => { + return changeFontSize(elements, appState, app, (element) => Math.round( // get previous value before relative increase (doesn't work fully // due to rounding and float precision issues) @@ -681,8 +695,8 @@ export const actionDecreaseFontSize = register({ export const actionIncreaseFontSize = register({ name: "increaseFontSize", trackEvent: false, - perform: (elements, appState, value) => { - return changeFontSize(elements, appState, (element) => + perform: (elements, appState, value, app) => { + return changeFontSize(elements, appState, app, (element) => Math.round(element.fontSize * (1 + FONT_SIZE_RELATIVE_INCREASE_STEP)), ); }, @@ -699,7 +713,7 @@ export const actionIncreaseFontSize = register({ export const actionChangeFontFamily = register({ name: "changeFontFamily", trackEvent: false, - perform: (elements, appState, value) => { + perform: (elements, appState, value, app) => { return { elements: changeProperty( elements, @@ -713,7 +727,10 @@ export const actionChangeFontFamily = register({ lineHeight: getDefaultLineHeight(value), }, ); - redrawTextBoundingBox(newElement, getContainerElement(oldElement)); + redrawTextBoundingBox( + newElement, + app.scene.getContainerElement(oldElement), + ); return newElement; } @@ -728,7 +745,7 @@ export const actionChangeFontFamily = register({ commitToHistory: true, }; }, - PanelComponent: ({ elements, appState, updateData }) => { + PanelComponent: ({ elements, appState, updateData, app }) => { const options: { value: FontFamilyValues; text: string; @@ -768,14 +785,21 @@ export const actionChangeFontFamily = register({ if (isTextElement(element)) { return element.fontFamily; } - const boundTextElement = getBoundTextElement(element); + const boundTextElement = getBoundTextElement( + element, + app.scene.getNonDeletedElementsMap(), + ); if (boundTextElement) { return boundTextElement.fontFamily; } return null; }, (element) => - isTextElement(element) || getBoundTextElement(element) !== null, + isTextElement(element) || + getBoundTextElement( + element, + app.scene.getNonDeletedElementsMap(), + ) !== null, (hasSelection) => hasSelection ? null @@ -791,7 +815,7 @@ export const actionChangeFontFamily = register({ export const actionChangeTextAlign = register({ name: "changeTextAlign", trackEvent: false, - perform: (elements, appState, value) => { + perform: (elements, appState, value, app) => { return { elements: changeProperty( elements, @@ -802,7 +826,10 @@ export const actionChangeTextAlign = register({ oldElement, { textAlign: value }, ); - redrawTextBoundingBox(newElement, getContainerElement(oldElement)); + redrawTextBoundingBox( + newElement, + app.scene.getContainerElement(oldElement), + ); return newElement; } @@ -817,7 +844,8 @@ export const actionChangeTextAlign = register({ commitToHistory: true, }; }, - PanelComponent: ({ elements, appState, updateData }) => { + PanelComponent: ({ elements, appState, updateData, app }) => { + const elementsMap = app.scene.getNonDeletedElementsMap(); return (
{t("labels.textAlign")} @@ -850,14 +878,18 @@ export const actionChangeTextAlign = register({ if (isTextElement(element)) { return element.textAlign; } - const boundTextElement = getBoundTextElement(element); + const boundTextElement = getBoundTextElement( + element, + elementsMap, + ); if (boundTextElement) { return boundTextElement.textAlign; } return null; }, (element) => - isTextElement(element) || getBoundTextElement(element) !== null, + isTextElement(element) || + getBoundTextElement(element, elementsMap) !== null, (hasSelection) => hasSelection ? null : appState.currentItemTextAlign, )} @@ -871,7 +903,7 @@ export const actionChangeTextAlign = register({ export const actionChangeVerticalAlign = register({ name: "changeVerticalAlign", trackEvent: { category: "element" }, - perform: (elements, appState, value) => { + perform: (elements, appState, value, app) => { return { elements: changeProperty( elements, @@ -883,7 +915,10 @@ export const actionChangeVerticalAlign = register({ { verticalAlign: value }, ); - redrawTextBoundingBox(newElement, getContainerElement(oldElement)); + redrawTextBoundingBox( + newElement, + app.scene.getContainerElement(oldElement), + ); return newElement; } @@ -897,7 +932,7 @@ export const actionChangeVerticalAlign = register({ commitToHistory: true, }; }, - PanelComponent: ({ elements, appState, updateData }) => { + PanelComponent: ({ elements, appState, updateData, app }) => { return (
@@ -929,14 +964,21 @@ export const actionChangeVerticalAlign = register({ if (isTextElement(element) && element.containerId) { return element.verticalAlign; } - const boundTextElement = getBoundTextElement(element); + const boundTextElement = getBoundTextElement( + element, + app.scene.getNonDeletedElementsMap(), + ); if (boundTextElement) { return boundTextElement.verticalAlign; } return null; }, (element) => - isTextElement(element) || getBoundTextElement(element) !== null, + isTextElement(element) || + getBoundTextElement( + element, + app.scene.getNonDeletedElementsMap(), + ) !== null, (hasSelection) => (hasSelection ? null : VERTICAL_ALIGN.MIDDLE), )} onChange={(value) => updateData(value)} @@ -1013,6 +1055,77 @@ export const actionChangeRoundness = register({ }, }); +const getArrowheadOptions = (flip: boolean) => { + return [ + { + value: null, + text: t("labels.arrowhead_none"), + keyBinding: "q", + icon: ArrowheadNoneIcon, + }, + { + value: "arrow", + text: t("labels.arrowhead_arrow"), + keyBinding: "w", + icon: , + }, + { + value: "bar", + text: t("labels.arrowhead_bar"), + keyBinding: "e", + icon: , + }, + { + value: "dot", + text: t("labels.arrowhead_circle"), + keyBinding: null, + icon: , + showInPicker: false, + }, + { + value: "circle", + text: t("labels.arrowhead_circle"), + keyBinding: "r", + icon: , + showInPicker: false, + }, + { + value: "circle_outline", + text: t("labels.arrowhead_circle_outline"), + keyBinding: null, + icon: , + showInPicker: false, + }, + { + value: "triangle", + text: t("labels.arrowhead_triangle"), + icon: , + keyBinding: "t", + }, + { + value: "triangle_outline", + text: t("labels.arrowhead_triangle_outline"), + icon: , + keyBinding: null, + showInPicker: false, + }, + { + value: "diamond", + text: t("labels.arrowhead_diamond"), + icon: , + keyBinding: null, + showInPicker: false, + }, + { + value: "diamond_outline", + text: t("labels.arrowhead_diamond_outline"), + icon: , + keyBinding: null, + showInPicker: false, + }, + ] as const; +}; + export const actionChangeArrowhead = register({ name: "changeArrowhead", trackEvent: false, @@ -1059,38 +1172,7 @@ export const actionChangeArrowhead = register({
, - keyBinding: "w", - }, - { - value: "bar", - text: t("labels.arrowhead_bar"), - icon: , - keyBinding: "e", - }, - { - value: "dot", - text: t("labels.arrowhead_dot"), - icon: , - keyBinding: "r", - }, - { - value: "triangle", - text: t("labels.arrowhead_triangle"), - icon: , - keyBinding: "t", - }, - ]} + options={getArrowheadOptions(!isRTL)} value={getFormValue( elements, appState, @@ -1106,38 +1188,7 @@ export const actionChangeArrowhead = register({ , - }, - { - value: "bar", - text: t("labels.arrowhead_bar"), - keyBinding: "e", - icon: , - }, - { - value: "dot", - text: t("labels.arrowhead_dot"), - keyBinding: "r", - icon: , - }, - { - value: "triangle", - text: t("labels.arrowhead_triangle"), - icon: , - keyBinding: "t", - }, - ]} + options={getArrowheadOptions(!!isRTL)} value={getFormValue( elements, appState, diff --git a/src/actions/actionSelectAll.ts b/packages/excalidraw/actions/actionSelectAll.ts similarity index 100% rename from src/actions/actionSelectAll.ts rename to packages/excalidraw/actions/actionSelectAll.ts diff --git a/src/actions/actionStyles.ts b/packages/excalidraw/actions/actionStyles.ts similarity index 95% rename from src/actions/actionStyles.ts rename to packages/excalidraw/actions/actionStyles.ts index 9c6589bbc..25a6baf2a 100644 --- a/src/actions/actionStyles.ts +++ b/packages/excalidraw/actions/actionStyles.ts @@ -32,12 +32,15 @@ export let copiedStyles: string = "{}"; export const actionCopyStyles = register({ name: "copyStyles", trackEvent: { category: "element" }, - perform: (elements, appState) => { + perform: (elements, appState, formData, app) => { const elementsCopied = []; const element = elements.find((el) => appState.selectedElementIds[el.id]); elementsCopied.push(element); if (element && hasBoundTextElement(element)) { - const boundTextElement = getBoundTextElement(element); + const boundTextElement = getBoundTextElement( + element, + app.scene.getNonDeletedElementsMap(), + ); elementsCopied.push(boundTextElement); } if (element) { @@ -59,7 +62,7 @@ export const actionCopyStyles = register({ export const actionPasteStyles = register({ name: "pasteStyles", trackEvent: { category: "element" }, - perform: (elements, appState) => { + perform: (elements, appState, formData, app) => { const elementsCopied = JSON.parse(copiedStyles); const pastedElement = elementsCopied[0]; const boundTextElement = elementsCopied[1]; diff --git a/src/actions/actionToggleGridMode.tsx b/packages/excalidraw/actions/actionToggleGridMode.tsx similarity index 100% rename from src/actions/actionToggleGridMode.tsx rename to packages/excalidraw/actions/actionToggleGridMode.tsx diff --git a/src/actions/actionToggleObjectsSnapMode.tsx b/packages/excalidraw/actions/actionToggleObjectsSnapMode.tsx similarity index 100% rename from src/actions/actionToggleObjectsSnapMode.tsx rename to packages/excalidraw/actions/actionToggleObjectsSnapMode.tsx diff --git a/src/actions/actionToggleStats.tsx b/packages/excalidraw/actions/actionToggleStats.tsx similarity index 100% rename from src/actions/actionToggleStats.tsx rename to packages/excalidraw/actions/actionToggleStats.tsx diff --git a/src/actions/actionToggleViewMode.tsx b/packages/excalidraw/actions/actionToggleViewMode.tsx similarity index 100% rename from src/actions/actionToggleViewMode.tsx rename to packages/excalidraw/actions/actionToggleViewMode.tsx diff --git a/src/actions/actionToggleZenMode.tsx b/packages/excalidraw/actions/actionToggleZenMode.tsx similarity index 100% rename from src/actions/actionToggleZenMode.tsx rename to packages/excalidraw/actions/actionToggleZenMode.tsx diff --git a/src/actions/actionZindex.tsx b/packages/excalidraw/actions/actionZindex.tsx similarity index 100% rename from src/actions/actionZindex.tsx rename to packages/excalidraw/actions/actionZindex.tsx diff --git a/src/actions/index.ts b/packages/excalidraw/actions/index.ts similarity index 100% rename from src/actions/index.ts rename to packages/excalidraw/actions/index.ts diff --git a/src/actions/manager.tsx b/packages/excalidraw/actions/manager.tsx similarity index 96% rename from src/actions/manager.tsx rename to packages/excalidraw/actions/manager.tsx index d935579a2..6a24cc801 100644 --- a/src/actions/manager.tsx +++ b/packages/excalidraw/actions/manager.tsx @@ -11,6 +11,7 @@ import { import { ExcalidrawElement } from "../element/types"; import { AppClassProperties, AppState } from "../types"; import { trackEvent } from "../analytics"; +import { isPromiseLike } from "../utils"; const trackAction = ( action: Action, @@ -57,7 +58,7 @@ export class ActionManager { app: AppClassProperties, ) { this.updater = (actionResult) => { - if (actionResult && "then" in actionResult) { + if (isPromiseLike(actionResult)) { actionResult.then((actionResult) => { return updater(actionResult); }); @@ -94,7 +95,7 @@ export class ActionManager { const actions: Action[] = []; for (const key in this.actions) { const action = this.actions[key as ActionName]; - if (filter(action, elements, appState, data)) { + if (filter(action, elements, appState, this.app, data)) { actions.push(action); } } @@ -233,7 +234,7 @@ export class ActionManager { } let enabled = true; this.actionPredicates.forEach((fn) => { - if (!fn(action, elements, appState, data)) { + if (!fn(action, elements, appState, this.app, data)) { enabled = false; } }); diff --git a/src/actions/register.ts b/packages/excalidraw/actions/register.ts similarity index 100% rename from src/actions/register.ts rename to packages/excalidraw/actions/register.ts diff --git a/src/actions/shortcuts.ts b/packages/excalidraw/actions/shortcuts.ts similarity index 100% rename from src/actions/shortcuts.ts rename to packages/excalidraw/actions/shortcuts.ts diff --git a/src/actions/types.ts b/packages/excalidraw/actions/types.ts similarity index 98% rename from src/actions/types.ts rename to packages/excalidraw/actions/types.ts index d2652d30b..b9f5e2b36 100644 --- a/src/actions/types.ts +++ b/packages/excalidraw/actions/types.ts @@ -38,6 +38,7 @@ export type ActionPredicateFn = ( action: Action, elements: readonly ExcalidrawElement[], appState: AppState, + app: AppClassProperties, data?: Record, ) => boolean; @@ -142,7 +143,7 @@ export type ActionName = export type PanelComponentProps = { elements: readonly ExcalidrawElement[]; appState: AppState; - updateData: (formData?: any) => void; + updateData: (formData?: T) => void; appProps: ExcalidrawProps; data?: Record; app: AppClassProperties; diff --git a/src/align.ts b/packages/excalidraw/align.ts similarity index 89% rename from src/align.ts rename to packages/excalidraw/align.ts index 06382838f..90ecabb11 100644 --- a/src/align.ts +++ b/packages/excalidraw/align.ts @@ -1,4 +1,4 @@ -import { ExcalidrawElement } from "./element/types"; +import { ElementsMap, ExcalidrawElement } from "./element/types"; import { newElementWith } from "./element/mutateElement"; import { BoundingBox, getCommonBoundingBox } from "./element/bounds"; import { getMaximumGroups } from "./groups"; @@ -10,10 +10,13 @@ export interface Alignment { export const alignElements = ( selectedElements: ExcalidrawElement[], + elementsMap: ElementsMap, alignment: Alignment, ): ExcalidrawElement[] => { - const groups: ExcalidrawElement[][] = getMaximumGroups(selectedElements); - + const groups: ExcalidrawElement[][] = getMaximumGroups( + selectedElements, + elementsMap, + ); const selectionBoundingBox = getCommonBoundingBox(selectedElements); return groups.flatMap((group) => { diff --git a/src/analytics.ts b/packages/excalidraw/analytics.ts similarity index 100% rename from src/analytics.ts rename to packages/excalidraw/analytics.ts diff --git a/packages/excalidraw/animated-trail.ts b/packages/excalidraw/animated-trail.ts new file mode 100644 index 000000000..de5fd08fd --- /dev/null +++ b/packages/excalidraw/animated-trail.ts @@ -0,0 +1,148 @@ +import { LaserPointer, LaserPointerOptions } from "@excalidraw/laser-pointer"; +import { AnimationFrameHandler } from "./animation-frame-handler"; +import { AppState } from "./types"; +import { getSvgPathFromStroke, sceneCoordsToViewportCoords } from "./utils"; +import type App from "./components/App"; +import { SVG_NS } from "./constants"; + +export interface Trail { + start(container: SVGSVGElement): void; + stop(): void; + + startPath(x: number, y: number): void; + addPointToPath(x: number, y: number): void; + endPath(): void; +} + +export interface AnimatedTrailOptions { + fill: (trail: AnimatedTrail) => string; +} + +export class AnimatedTrail implements Trail { + private currentTrail?: LaserPointer; + private pastTrails: LaserPointer[] = []; + + private container?: SVGSVGElement; + private trailElement: SVGPathElement; + + constructor( + private animationFrameHandler: AnimationFrameHandler, + private app: App, + private options: Partial & + Partial, + ) { + this.animationFrameHandler.register(this, this.onFrame.bind(this)); + + this.trailElement = document.createElementNS(SVG_NS, "path"); + } + + get hasCurrentTrail() { + return !!this.currentTrail; + } + + hasLastPoint(x: number, y: number) { + if (this.currentTrail) { + const len = this.currentTrail.originalPoints.length; + return ( + this.currentTrail.originalPoints[len - 1][0] === x && + this.currentTrail.originalPoints[len - 1][1] === y + ); + } + + return false; + } + + start(container?: SVGSVGElement) { + if (container) { + this.container = container; + } + + if (this.trailElement.parentNode !== this.container && this.container) { + this.container.appendChild(this.trailElement); + } + + this.animationFrameHandler.start(this); + } + + stop() { + this.animationFrameHandler.stop(this); + + if (this.trailElement.parentNode === this.container) { + this.container?.removeChild(this.trailElement); + } + } + + startPath(x: number, y: number) { + this.currentTrail = new LaserPointer(this.options); + + this.currentTrail.addPoint([x, y, performance.now()]); + + this.update(); + } + + addPointToPath(x: number, y: number) { + if (this.currentTrail) { + this.currentTrail.addPoint([x, y, performance.now()]); + this.update(); + } + } + + endPath() { + if (this.currentTrail) { + this.currentTrail.close(); + this.currentTrail.options.keepHead = false; + this.pastTrails.push(this.currentTrail); + this.currentTrail = undefined; + this.update(); + } + } + + private update() { + this.start(); + } + + private onFrame() { + const paths: string[] = []; + + for (const trail of this.pastTrails) { + paths.push(this.drawTrail(trail, this.app.state)); + } + + if (this.currentTrail) { + const currentPath = this.drawTrail(this.currentTrail, this.app.state); + + paths.push(currentPath); + } + + this.pastTrails = this.pastTrails.filter((trail) => { + return trail.getStrokeOutline().length !== 0; + }); + + if (paths.length === 0) { + this.stop(); + } + + const svgPaths = paths.join(" ").trim(); + + this.trailElement.setAttribute("d", svgPaths); + this.trailElement.setAttribute( + "fill", + (this.options.fill ?? (() => "black"))(this), + ); + } + + private drawTrail(trail: LaserPointer, state: AppState): string { + const stroke = trail + .getStrokeOutline(trail.options.size / state.zoom.value) + .map(([x, y]) => { + const result = sceneCoordsToViewportCoords( + { sceneX: x, sceneY: y }, + state, + ); + + return [result.x, result.y]; + }); + + return getSvgPathFromStroke(stroke, true); + } +} diff --git a/packages/excalidraw/animation-frame-handler.ts b/packages/excalidraw/animation-frame-handler.ts new file mode 100644 index 000000000..b1a984466 --- /dev/null +++ b/packages/excalidraw/animation-frame-handler.ts @@ -0,0 +1,79 @@ +export type AnimationCallback = (timestamp: number) => void | boolean; + +export type AnimationTarget = { + callback: AnimationCallback; + stopped: boolean; +}; + +export class AnimationFrameHandler { + private targets = new WeakMap(); + private rafIds = new WeakMap(); + + register(key: object, callback: AnimationCallback) { + this.targets.set(key, { callback, stopped: true }); + } + + start(key: object) { + const target = this.targets.get(key); + + if (!target) { + return; + } + + if (this.rafIds.has(key)) { + return; + } + + this.targets.set(key, { ...target, stopped: false }); + this.scheduleFrame(key); + } + + stop(key: object) { + const target = this.targets.get(key); + if (target && !target.stopped) { + this.targets.set(key, { ...target, stopped: true }); + } + + this.cancelFrame(key); + } + + private constructFrame(key: object): FrameRequestCallback { + return (timestamp: number) => { + const target = this.targets.get(key); + + if (!target) { + return; + } + + const shouldAbort = this.onFrame(target, timestamp); + + if (!target.stopped && !shouldAbort) { + this.scheduleFrame(key); + } else { + this.cancelFrame(key); + } + }; + } + + private scheduleFrame(key: object) { + const rafId = requestAnimationFrame(this.constructFrame(key)); + + this.rafIds.set(key, rafId); + } + + private cancelFrame(key: object) { + if (this.rafIds.has(key)) { + const rafId = this.rafIds.get(key)!; + + cancelAnimationFrame(rafId); + } + + this.rafIds.delete(key); + } + + private onFrame(target: AnimationTarget, timestamp: number): boolean { + const shouldAbort = target.callback(timestamp); + + return shouldAbort ?? false; + } +} diff --git a/src/appState.ts b/packages/excalidraw/appState.ts similarity index 98% rename from src/appState.ts rename to packages/excalidraw/appState.ts index 819c9438b..d3359f7f1 100644 --- a/src/appState.ts +++ b/packages/excalidraw/appState.ts @@ -105,6 +105,8 @@ export const getDefaultAppState = (): Omit< y: 0, }, objectsSnapModeEnabled: false, + userToFollow: null, + followedBy: new Set(), }; }; @@ -217,6 +219,8 @@ const APP_STATE_STORAGE_CONF = (< snapLines: { browser: false, export: false, server: false }, originSnapOffset: { browser: false, export: false, server: false }, objectsSnapModeEnabled: { browser: true, export: false, server: false }, + userToFollow: { browser: false, export: false, server: false }, + followedBy: { browser: false, export: false, server: false }, }); const _clearAppStateForStorage = < diff --git a/src/charts.test.ts b/packages/excalidraw/charts.test.ts similarity index 100% rename from src/charts.test.ts rename to packages/excalidraw/charts.test.ts diff --git a/src/charts.ts b/packages/excalidraw/charts.ts similarity index 100% rename from src/charts.ts rename to packages/excalidraw/charts.ts diff --git a/src/clients.ts b/packages/excalidraw/clients.ts similarity index 100% rename from src/clients.ts rename to packages/excalidraw/clients.ts diff --git a/src/clipboard.test.ts b/packages/excalidraw/clipboard.test.ts similarity index 100% rename from src/clipboard.test.ts rename to packages/excalidraw/clipboard.test.ts diff --git a/src/clipboard.ts b/packages/excalidraw/clipboard.ts similarity index 100% rename from src/clipboard.ts rename to packages/excalidraw/clipboard.ts diff --git a/src/colors.ts b/packages/excalidraw/colors.ts similarity index 100% rename from src/colors.ts rename to packages/excalidraw/colors.ts diff --git a/src/components/Actions.scss b/packages/excalidraw/components/Actions.scss similarity index 100% rename from src/components/Actions.scss rename to packages/excalidraw/components/Actions.scss diff --git a/src/components/Actions.tsx b/packages/excalidraw/components/Actions.tsx similarity index 96% rename from src/components/Actions.tsx rename to packages/excalidraw/components/Actions.tsx index 675a79f84..db8e2d20e 100644 --- a/src/components/Actions.tsx +++ b/packages/excalidraw/components/Actions.tsx @@ -1,9 +1,12 @@ -import React, { useState } from "react"; +import { useState } from "react"; import { ActionManager } from "../actions/manager"; -import { getNonDeletedElements } from "../element"; -import { ExcalidrawElement, ExcalidrawElementType } from "../element/types"; +import { + ExcalidrawElementType, + NonDeletedElementsMap, + NonDeletedSceneElementsMap, +} from "../element/types"; import { t } from "../i18n"; -import { useDevice } from "../components/App"; +import { useDevice } from "./App"; import { canChangeRoundness, canHaveArrowheads, @@ -45,17 +48,14 @@ import { useTunnels } from "../context/tunnels"; export const SelectedShapeActions = ({ appState, - elements, + elementsMap, renderAction, }: { appState: UIAppState; - elements: readonly ExcalidrawElement[]; + elementsMap: NonDeletedElementsMap | NonDeletedSceneElementsMap; renderAction: ActionManager["renderAction"]; }) => { - const targetElements = getTargetElements( - getNonDeletedElements(elements), - appState, - ); + const targetElements = getTargetElements(elementsMap, appState); let isSingleElementBoundContainer = false; if ( @@ -139,12 +139,12 @@ export const SelectedShapeActions = ({ {renderAction("changeFontFamily")} {(appState.activeTool.type === "text" || - suppportsHorizontalAlign(targetElements)) && + suppportsHorizontalAlign(targetElements, elementsMap)) && renderAction("changeTextAlign")} )} - {shouldAllowVerticalAlign(targetElements) && + {shouldAllowVerticalAlign(targetElements, elementsMap) && renderAction("changeVerticalAlign")} {(canHaveArrowheads(appState.activeTool.type) || targetElements.some((element) => canHaveArrowheads(element.type))) && ( diff --git a/src/components/ActiveConfirmDialog.tsx b/packages/excalidraw/components/ActiveConfirmDialog.tsx similarity index 100% rename from src/components/ActiveConfirmDialog.tsx rename to packages/excalidraw/components/ActiveConfirmDialog.tsx diff --git a/src/components/App.tsx b/packages/excalidraw/components/App.tsx similarity index 92% rename from src/components/App.tsx rename to packages/excalidraw/components/App.tsx index 502ec1f5c..33f120ad6 100644 --- a/src/components/App.tsx +++ b/packages/excalidraw/components/App.tsx @@ -5,7 +5,6 @@ import { RoughCanvas } from "roughjs/bin/canvas"; import rough from "roughjs/bin/rough"; import clsx from "clsx"; import { nanoid } from "nanoid"; - import { actionAddToLibrary, actionBringForward, @@ -58,7 +57,6 @@ import { DEFAULT_MAX_IMAGE_WIDTH_OR_HEIGHT, DEFAULT_VERTICAL_ALIGN, DRAGGING_THRESHOLD, - ELEMENT_READY_TO_ERASE_OPACITY, ELEMENT_SHIFT_TRANSLATE_AMOUNT, ELEMENT_TRANSLATE_AMOUNT, ENV, @@ -68,7 +66,6 @@ import { GRID_SIZE, IMAGE_MIME_TYPES, IMAGE_RENDER_TIMEOUT, - isAndroid, isBrave, LINE_CONFIRM_THRESHOLD, MAX_ALLOWED_FILE_BYTES, @@ -91,6 +88,7 @@ import { POINTER_EVENTS, TOOL_TYPE, EDITOR_LS_KEYS, + isIOS, } from "../constants"; import { ExportedElements, exportCanvas, loadFromBlob } from "../data"; import Library, { distributeLibraryItemsOnSquareGrid } from "../data/library"; @@ -117,7 +115,6 @@ import { newLinearElement, newTextElement, newImageElement, - textWysiwyg, transformElements, updateTextElement, redrawTextBoundingBox, @@ -183,6 +180,7 @@ import { ExcalidrawIframeLikeElement, IframeData, ExcalidrawIframeElement, + ExcalidrawEmbeddableElement, } from "../element/types"; import { getCenter, getDistance } from "../gesture"; import { @@ -218,7 +216,6 @@ import { getNormalizedZoom, getSelectedElements, hasBackground, - isOverScrollBars, isSomeElementSelected, } from "../scene"; import Scene from "../scene/Scene"; @@ -245,6 +242,10 @@ import { KeyboardModifiersObject, CollaboratorPointer, ToolType, + OnUserFollowedPayload, + UnsubscribeCallback, + EmbedsValidationStatus, + ElementsPendingErasure, } from "../types"; import { debounce, @@ -257,9 +258,7 @@ import { sceneCoordsToViewportCoords, tupleToCoors, viewportCoordsToSceneCoords, - withBatchedUpdates, wrapEvent, - withBatchedUpdatesThrottled, updateObject, updateActiveTool, getShortcutKey, @@ -268,11 +267,14 @@ import { muteFSAbortError, isTestEnv, easeOut, + updateStable, + addEventListener, + normalizeEOL, } from "../utils"; import { createSrcDoc, embeddableURLValidator, - extractSrc, + maybeParseEmbedSrc, getEmbedLink, } from "../element/embeddable"; import { @@ -354,6 +356,8 @@ import { updateFrameMembershipOfSelectedElements, isElementInFrame, getFrameLikeTitle, + getElementsOverlappingFrame, + filterElementsEligibleAsFrameChildren, } from "../frame"; import { excludeElementsInFramesFromSelection, @@ -391,8 +395,7 @@ import { isSidebarDockedAtom } from "./Sidebar/Sidebar"; import { StaticCanvas, InteractiveCanvas } from "./canvases"; import { Renderer } from "../scene/Renderer"; import { ShapeCache } from "../scene/ShapeCache"; -import { LaserToolOverlay } from "./LaserTool/LaserTool"; -import { LaserPathManager } from "./LaserTool/LaserPathManager"; +import { SVGLayer } from "./SVGLayer"; import { setEraserCursor, setCursor, @@ -402,11 +405,20 @@ import { import { Emitter } from "../emitter"; import { ElementCanvasButtons } from "../element/ElementCanvasButtons"; import { MagicCacheData, diagramToHTML } from "../data/magic"; -import { elementsOverlappingBBox, exportToBlob } from "../packages/utils"; +import { exportToBlob } from "../../utils/export"; import { COLOR_PALETTE } from "../colors"; import { ElementCanvasButton } from "./MagicButton"; import { MagicIcon, copyIcon, fullscreenIcon } from "./icons"; import { EditorLocalStorage } from "../data/EditorLocalStorage"; +import FollowMode from "./FollowMode/FollowMode"; + +import { AnimationFrameHandler } from "../animation-frame-handler"; +import { AnimatedTrail } from "../animated-trail"; +import { LaserTrails } from "../laser-trails"; +import { withBatchedUpdates, withBatchedUpdatesThrottled } from "../reactUtils"; +import { getRenderOpacity } from "../renderer/renderElement"; +import { textWysiwyg } from "../element/textWysiwyg"; +import { isOverScrollBars } from "../scene/scrollbars"; const AppContext = React.createContext(null!); const AppPropsContext = React.createContext(null!); @@ -448,7 +460,7 @@ ExcalidrawAppStateContext.displayName = "ExcalidrawAppStateContext"; const ExcalidrawSetAppStateContext = React.createContext< React.Component["setState"] >(() => { - console.warn("unitialized ExcalidrawSetAppStateContext context!"); + console.warn("Uninitialized ExcalidrawSetAppStateContext context!"); }); ExcalidrawSetAppStateContext.displayName = "ExcalidrawSetAppStateContext"; @@ -495,7 +507,7 @@ let IS_PLAIN_PASTE = false; let IS_PLAIN_PASTE_TIMER = 0; let PLAIN_PASTE_TOAST_SHOWN = false; -let lastPointerUp: ((event: any) => void) | null = null; +let lastPointerUp: (() => void) | null = null; const gesture: Gesture = { pointers: new Map(), lastCenter: null, @@ -530,14 +542,49 @@ class App extends React.Component { public files: BinaryFiles = {}; public imageCache: AppClassProperties["imageCache"] = new Map(); private iFrameRefs = new Map(); + /** + * Indicates whether the embeddable's url has been validated for rendering. + * If value not set, indicates that the validation is pending. + * Initially or on url change the flag is not reset so that we can guarantee + * the validation came from a trusted source (the editor). + **/ + private embedsValidationStatus: EmbedsValidationStatus = new Map(); + /** embeds that have been inserted to DOM (as a perf optim, we don't want to + * insert to DOM before user initially scrolls to them) */ + private initializedEmbeds = new Set(); + + private elementsPendingErasure: ElementsPendingErasure = new Set(); hitLinkElement?: NonDeletedExcalidrawElement; lastPointerDownEvent: React.PointerEvent | null = null; lastPointerUpEvent: React.PointerEvent | PointerEvent | null = null; + lastPointerMoveEvent: PointerEvent | null = null; lastViewportPosition = { x: 0, y: 0 }; - laserPathManager: LaserPathManager = new LaserPathManager(this); + animationFrameHandler = new AnimationFrameHandler(); + + laserTrails = new LaserTrails(this.animationFrameHandler, this); + eraserTrail = new AnimatedTrail(this.animationFrameHandler, this, { + streamline: 0.2, + size: 5, + keepHead: true, + sizeMapping: (c) => { + const DECAY_TIME = 200; + const DECAY_LENGTH = 10; + const t = Math.max(0, 1 - (performance.now() - c.pressure) / DECAY_TIME); + const l = + (DECAY_LENGTH - + Math.min(DECAY_LENGTH, c.totalLength - c.currentIndex)) / + DECAY_LENGTH; + + return Math.min(easeOut(l), easeOut(t)); + }, + fill: () => + this.state.theme === THEME.LIGHT + ? "rgba(0, 0, 0, 0.2)" + : "rgba(255, 255, 255, 0.2)", + }); onChangeEmitter = new Emitter< [ @@ -562,6 +609,15 @@ class App extends React.Component { event: PointerEvent, ] >(); + onUserFollowEmitter = new Emitter<[payload: OnUserFollowedPayload]>(); + onScrollChangeEmitter = new Emitter< + [scrollX: number, scrollY: number, zoom: AppState["zoom"]] + >(); + + missingPointerEventCleanupEmitter = new Emitter< + [event: PointerEvent | null] + >(); + onRemoveEventListenersEmitter = new Emitter<[]>(); constructor(props: AppProps) { super(props); @@ -590,7 +646,6 @@ class App extends React.Component { }; this.id = nanoid(); - this.library = new Library(this); this.actionManager = new ActionManager( this.syncActionResult, @@ -603,7 +658,6 @@ class App extends React.Component { this.canvas = document.createElement("canvas"); this.rc = rough.canvas(this.canvas); this.renderer = new Renderer(this.scene); - if (excalidrawAPI) { const api: ExcalidrawImperativeAPI = { updateScene: this.updateScene, @@ -633,6 +687,8 @@ class App extends React.Component { onChange: (cb) => this.onChangeEmitter.on(cb), onPointerDown: (cb) => this.onPointerDownEmitter.on(cb), onPointerUp: (cb) => this.onPointerUpEmitter.on(cb), + onScrollChange: (cb) => this.onScrollChangeEmitter.on(cb), + onUserFollow: (cb) => this.onUserFollowEmitter.on(cb), } as const; if (typeof excalidrawAPI === "function") { excalidrawAPI(api); @@ -849,6 +905,14 @@ class App extends React.Component { ); } + private updateEmbedValidationStatus = ( + element: ExcalidrawEmbeddableElement, + status: boolean, + ) => { + this.embedsValidationStatus.set(element.id, status); + ShapeCache.delete(element); + }; + private updateEmbeddables = () => { const iframeLikes = new Set(); @@ -856,7 +920,7 @@ class App extends React.Component { this.scene.getNonDeletedElements().filter((element) => { if (isEmbeddableElement(element)) { iframeLikes.add(element.id); - if (element.validated == null) { + if (!this.embedsValidationStatus.has(element.id)) { updated = true; const validated = embeddableURLValidator( @@ -864,8 +928,7 @@ class App extends React.Component { this.props.validateEmbeddable, ); - mutateElement(element, { validated }, false); - ShapeCache.delete(element); + this.updateEmbedValidationStatus(element, validated); } } else if (isIframeElement(element)) { iframeLikes.add(element.id); @@ -894,7 +957,9 @@ class App extends React.Component { .getNonDeletedElements() .filter( (el): el is NonDeleted => - (isEmbeddableElement(el) && !!el.validated) || isIframeElement(el), + (isEmbeddableElement(el) && + this.embedsValidationStatus.get(el.id) === true) || + isIframeElement(el), ); return ( @@ -905,6 +970,23 @@ class App extends React.Component { this.state, ); + const isVisible = isElementInViewport( + el, + normalizedWidth, + normalizedHeight, + this.state, + ); + const hasBeenInitialized = this.initializedEmbeds.has(el.id); + + if (isVisible && !hasBeenInitialized) { + this.initializedEmbeds.add(el.id); + } + const shouldRender = isVisible || hasBeenInitialized; + + if (!shouldRender) { + return null; + } + let src: IframeData | null; if (isIframeElement(el)) { @@ -1046,14 +1128,6 @@ class App extends React.Component { src = getEmbedLink(toValidURL(el.link || "")); } - // console.log({ src }); - - const isVisible = isElementInViewport( - el, - normalizedWidth, - normalizedHeight, - this.state, - ); const isActive = this.state.activeEmbeddable?.element === el && this.state.activeEmbeddable?.state === "active"; @@ -1074,7 +1148,11 @@ class App extends React.Component { }px) scale(${scale})` : "none", display: isVisible ? "block" : "none", - opacity: el.opacity / 100, + opacity: getRenderOpacity( + el, + getContainingFrame(el), + this.elementsPendingErasure, + ), ["--embeddable-radius" as string]: `${getCornerRadius( Math.min(el.width, el.height), el, @@ -1245,10 +1323,7 @@ class App extends React.Component { const FRAME_NAME_EDIT_PADDING = 6; const reset = () => { - if (f.name?.trim() === "") { - mutateElement(f, { name: null }); - } - + mutateElement(f, { name: f.name?.trim() || null }); this.setState({ editingFrame: null }); }; @@ -1271,6 +1346,7 @@ class App extends React.Component { name: e.target.value, }); }} + onFocus={(e) => e.target.select()} onBlur={() => reset()} onKeyDown={(event) => { // for some inexplicable reason, `onBlur` triggered on ESC @@ -1358,12 +1434,19 @@ class App extends React.Component { }); }; + private toggleOverscrollBehavior(event: React.PointerEvent) { + // when pointer inside editor, disable overscroll behavior to prevent + // panning to trigger history back/forward on MacOS Chrome + document.documentElement.style.overscrollBehaviorX = + event.type === "pointerenter" ? "none" : "auto"; + } + public render() { const selectedElements = this.scene.getSelectedElements(this.state); const { renderTopRightUI, renderCustomStats } = this.props; const versionNonce = this.scene.getVersionNonce(); - const { canvasElements, visibleElements } = + const { elementsMap, visibleElements } = this.renderer.getRenderableElements({ versionNonce, zoom: this.state.zoom, @@ -1377,6 +1460,8 @@ class App extends React.Component { pendingImageElementId: this.state.pendingImageElementId, }); + const allElementsMap = this.scene.getNonDeletedElementsMap(); + const shouldBlockPointerEvents = !( this.state.editingElement && isLinearElement(this.state.editingElement) @@ -1409,6 +1494,8 @@ class App extends React.Component { onKeyDown={ this.props.handleKeyboardGlobally ? undefined : this.onKeyDown } + onPointerEnter={this.toggleOverscrollBehavior} + onPointerLeave={this.toggleOverscrollBehavior} > @@ -1463,7 +1550,9 @@ class App extends React.Component {
- + {selectedElements.length === 1 && this.state.showHyperlinkPopup && ( { setAppState={this.setAppState} onLinkOpen={this.props.onLinkOpen} setToast={this.setToast} + updateEmbedValidationStatus={ + this.updateEmbedValidationStatus + } /> )} {this.props.aiEnabled !== false && @@ -1568,7 +1660,8 @@ class App extends React.Component { { imageCache: this.imageCache, isExporting: false, renderGrid: true, + canvasBackgroundColor: + this.state.viewBackgroundColor, + embedsValidationStatus: this.embedsValidationStatus, + elementsPendingErasure: this.elementsPendingErasure, }} /> { onPointerDown={this.handleCanvasPointerDown} onDoubleClick={this.handleCanvasDoubleClick} /> + {this.state.userToFollow && ( + + )} {this.renderFrameNames()} {this.renderEmbeddables()} @@ -1734,11 +1839,10 @@ class App extends React.Component { return; } - const magicFrameChildren = elementsOverlappingBBox({ - elements: this.scene.getNonDeletedElements(), - bounds: magicFrame, - type: "overlap", - }).filter((el) => !isMagicFrameElement(el)); + const magicFrameChildren = getElementsOverlappingFrame( + this.scene.getNonDeletedElements(), + magicFrame, + ).filter((el) => !isMagicFrameElement(el)); if (!magicFrameChildren.length) { if (source === "button") { @@ -2375,14 +2479,16 @@ class App extends React.Component { this.removeEventListeners(); this.scene.destroy(); this.library.destroy(); - this.laserPathManager.destroy(); - this.onChangeEmitter.destroy(); + this.laserTrails.stop(); + this.eraserTrail.stop(); + this.onChangeEmitter.clear(); ShapeCache.destroy(); SnapCache.destroy(); clearTimeout(touchTimeout); isSomeElementSelected.clearCache(); selectGroupsForSelectedElements.clearCache(); touchTimeout = 0; + document.documentElement.style.overscrollBehaviorX = ""; } private onResize = withBatchedUpdates(() => { @@ -2397,63 +2503,6 @@ class App extends React.Component { this.setState({}); }); - private removeEventListeners() { - document.removeEventListener(EVENT.POINTER_UP, this.removePointer); - document.removeEventListener(EVENT.COPY, this.onCopy); - document.removeEventListener(EVENT.PASTE, this.pasteFromClipboard); - document.removeEventListener(EVENT.CUT, this.onCut); - this.excalidrawContainerRef.current?.removeEventListener( - EVENT.WHEEL, - this.onWheel, - ); - this.nearestScrollableContainer?.removeEventListener( - EVENT.SCROLL, - this.onScroll, - ); - document.removeEventListener(EVENT.KEYDOWN, this.onKeyDown, false); - document.removeEventListener( - EVENT.MOUSE_MOVE, - this.updateCurrentCursorPosition, - false, - ); - document.removeEventListener(EVENT.KEYUP, this.onKeyUp); - window.removeEventListener(EVENT.RESIZE, this.onResize, false); - window.removeEventListener(EVENT.UNLOAD, this.onUnload, false); - window.removeEventListener(EVENT.BLUR, this.onBlur, false); - this.excalidrawContainerRef.current?.removeEventListener( - EVENT.DRAG_OVER, - this.disableEvent, - false, - ); - this.excalidrawContainerRef.current?.removeEventListener( - EVENT.DROP, - this.disableEvent, - false, - ); - - document.removeEventListener( - EVENT.GESTURE_START, - this.onGestureStart as any, - false, - ); - document.removeEventListener( - EVENT.GESTURE_CHANGE, - this.onGestureChange as any, - false, - ); - document.removeEventListener( - EVENT.GESTURE_END, - this.onGestureEnd as any, - false, - ); - document.removeEventListener( - EVENT.FULLSCREENCHANGE, - this.onFullscreenChange, - ); - - window.removeEventListener(EVENT.MESSAGE, this.onWindowMessage, false); - } - /** generally invoked only if fullscreen was invoked programmatically */ private onFullscreenChange = () => { if ( @@ -2467,76 +2516,111 @@ class App extends React.Component { } }; + private removeEventListeners() { + this.onRemoveEventListenersEmitter.trigger(); + } + private addEventListeners() { + // remove first as we can add event listeners multiple times this.removeEventListeners(); - window.addEventListener(EVENT.MESSAGE, this.onWindowMessage, false); - document.addEventListener(EVENT.POINTER_UP, this.removePointer); // #3553 - document.addEventListener(EVENT.COPY, this.onCopy); - this.excalidrawContainerRef.current?.addEventListener( - EVENT.WHEEL, - this.onWheel, - { passive: false }, - ); + + // ------------------------------------------------------------------------- + // view+edit mode listeners + // ------------------------------------------------------------------------- if (this.props.handleKeyboardGlobally) { - document.addEventListener(EVENT.KEYDOWN, this.onKeyDown, false); + this.onRemoveEventListenersEmitter.once( + addEventListener(document, EVENT.KEYDOWN, this.onKeyDown, false), + ); } - document.addEventListener(EVENT.KEYUP, this.onKeyUp, { passive: true }); - document.addEventListener( - EVENT.MOUSE_MOVE, - this.updateCurrentCursorPosition, - ); - // rerender text elements on font load to fix #637 && #1553 - document.fonts?.addEventListener?.("loadingdone", (event) => { - const loadedFontFaces = (event as FontFaceSetLoadEvent).fontfaces; - this.fonts.onFontsLoaded(loadedFontFaces); - }); - // Safari-only desktop pinch zoom - document.addEventListener( - EVENT.GESTURE_START, - this.onGestureStart as any, - false, - ); - document.addEventListener( - EVENT.GESTURE_CHANGE, - this.onGestureChange as any, - false, - ); - document.addEventListener( - EVENT.GESTURE_END, - this.onGestureEnd as any, - false, + this.onRemoveEventListenersEmitter.once( + addEventListener( + this.excalidrawContainerRef.current, + EVENT.WHEEL, + this.onWheel, + { passive: false }, + ), + addEventListener(window, EVENT.MESSAGE, this.onWindowMessage, false), + addEventListener(document, EVENT.POINTER_UP, this.removePointer), // #3553 + addEventListener(document, EVENT.COPY, this.onCopy), + addEventListener(document, EVENT.KEYUP, this.onKeyUp, { passive: true }), + addEventListener( + document, + EVENT.MOUSE_MOVE, + this.updateCurrentCursorPosition, + ), + // rerender text elements on font load to fix #637 && #1553 + addEventListener(document.fonts, "loadingdone", (event) => { + const loadedFontFaces = (event as FontFaceSetLoadEvent).fontfaces; + this.fonts.onFontsLoaded(loadedFontFaces); + }), + // Safari-only desktop pinch zoom + addEventListener( + document, + EVENT.GESTURE_START, + this.onGestureStart as any, + false, + ), + addEventListener( + document, + EVENT.GESTURE_CHANGE, + this.onGestureChange as any, + false, + ), + addEventListener( + document, + EVENT.GESTURE_END, + this.onGestureEnd as any, + false, + ), + addEventListener(window, EVENT.FOCUS, () => { + this.maybeCleanupAfterMissingPointerUp(null); + }), ); + if (this.state.viewModeEnabled) { return; } - document.addEventListener(EVENT.FULLSCREENCHANGE, this.onFullscreenChange); - document.addEventListener(EVENT.PASTE, this.pasteFromClipboard); - document.addEventListener(EVENT.CUT, this.onCut); + // ------------------------------------------------------------------------- + // edit-mode listeners only + // ------------------------------------------------------------------------- + + this.onRemoveEventListenersEmitter.once( + addEventListener( + document, + EVENT.FULLSCREENCHANGE, + this.onFullscreenChange, + ), + addEventListener(document, EVENT.PASTE, this.pasteFromClipboard), + addEventListener(document, EVENT.CUT, this.onCut), + addEventListener(window, EVENT.RESIZE, this.onResize, false), + addEventListener(window, EVENT.UNLOAD, this.onUnload, false), + addEventListener(window, EVENT.BLUR, this.onBlur, false), + addEventListener( + this.excalidrawContainerRef.current, + EVENT.DRAG_OVER, + this.disableEvent, + false, + ), + addEventListener( + this.excalidrawContainerRef.current, + EVENT.DROP, + this.disableEvent, + false, + ), + ); + if (this.props.detectScroll) { - this.nearestScrollableContainer = getNearestScrollableContainer( - this.excalidrawContainerRef.current!, - ); - this.nearestScrollableContainer.addEventListener( - EVENT.SCROLL, - this.onScroll, + this.onRemoveEventListenersEmitter.once( + addEventListener( + getNearestScrollableContainer(this.excalidrawContainerRef.current!), + EVENT.SCROLL, + this.onScroll, + ), ); } - window.addEventListener(EVENT.RESIZE, this.onResize, false); - window.addEventListener(EVENT.UNLOAD, this.onUnload, false); - window.addEventListener(EVENT.BLUR, this.onBlur, false); - this.excalidrawContainerRef.current?.addEventListener( - EVENT.DRAG_OVER, - this.disableEvent, - false, - ); - this.excalidrawContainerRef.current?.addEventListener( - EVENT.DROP, - this.disableEvent, - false, - ); } componentDidUpdate(prevProps: AppProps, prevState: AppState) { @@ -2555,11 +2639,45 @@ class App extends React.Component { this.refreshEditorBreakpoints(); } + const hasFollowedPersonLeft = + prevState.userToFollow && + !this.state.collaborators.has(prevState.userToFollow.socketId); + + if (hasFollowedPersonLeft) { + this.maybeUnfollowRemoteUser(); + } + if ( + prevState.zoom.value !== this.state.zoom.value || prevState.scrollX !== this.state.scrollX || prevState.scrollY !== this.state.scrollY ) { - this.props?.onScrollChange?.(this.state.scrollX, this.state.scrollY); + this.props?.onScrollChange?.( + this.state.scrollX, + this.state.scrollY, + this.state.zoom, + ); + this.onScrollChangeEmitter.trigger( + this.state.scrollX, + this.state.scrollY, + this.state.zoom, + ); + } + + if (prevState.userToFollow !== this.state.userToFollow) { + if (prevState.userToFollow) { + this.onUserFollowEmitter.trigger({ + userToFollow: prevState.userToFollow, + action: "UNFOLLOW", + }); + } + + if (this.state.userToFollow) { + this.onUserFollowEmitter.trigger({ + userToFollow: this.state.userToFollow, + action: "FOLLOW", + }); + } } if ( @@ -2588,6 +2706,10 @@ class App extends React.Component { this.updateLanguage(); } + if (isEraserActive(prevState) && !isEraserActive(this.state)) { + this.eraserTrail.endPath(); + } + if (prevProps.viewModeEnabled !== this.props.viewModeEnabled) { this.setState({ viewModeEnabled: !!this.props.viewModeEnabled }); } @@ -2693,7 +2815,7 @@ class App extends React.Component { private renderInteractiveSceneCallback = ({ atLeastOneVisibleElement, scrollBars, - elements, + elementsMap, }: RenderInteractiveSceneCallback) => { if (scrollBars) { currentScrollBars = scrollBars; @@ -2702,7 +2824,7 @@ class App extends React.Component { // hide when editing text isTextElement(this.state.editingElement) ? false - : !atLeastOneVisibleElement && elements.length > 0; + : !atLeastOneVisibleElement && elementsMap.size > 0; if (this.state.scrolledOutside !== scrolledOutside) { this.setState({ scrolledOutside }); } @@ -2751,9 +2873,8 @@ class App extends React.Component { } private onTouchStart = (event: TouchEvent) => { - // fix for Apple Pencil Scribble - // On Android, preventing the event would disable contextMenu on tap-hold - if (!isAndroid) { + // fix for Apple Pencil Scribble (do not prevent for other devices) + if (isIOS) { event.preventDefault(); } @@ -2778,9 +2899,6 @@ class App extends React.Component { didTapTwice = false; clearTimeout(tappedTwiceTimer); } - if (isAndroid) { - event.preventDefault(); - } if (event.touches.length === 2) { this.setState({ @@ -2841,7 +2959,6 @@ class App extends React.Component { // event else some browsers (FF...) will clear the clipboardData // (something something security) let file = event?.clipboardData?.files[0]; - const data = await parseClipboard(event, isPlainPaste, this.state); if (!file && !isPlainPaste) { if (data.mixedContent) { @@ -2917,21 +3034,49 @@ class App extends React.Component { retainSeed: isPlainPaste, }); } else if (data.text) { - const maybeUrl = extractSrc(data.text); + const nonEmptyLines = normalizeEOL(data.text) + .split(/\n+/) + .map((s) => s.trim()) + .filter(Boolean); + + const embbeddableUrls = nonEmptyLines + .map((str) => maybeParseEmbedSrc(str)) + .filter((string) => { + return ( + embeddableURLValidator(string, this.props.validateEmbeddable) && + (/^(http|https):\/\/[^\s/$.?#].[^\s]*$/.test(string) || + getEmbedLink(string)?.type === "video") + ); + }); if ( - !isPlainPaste && - embeddableURLValidator(maybeUrl, this.props.validateEmbeddable) && - (/^(http|https):\/\/[^\s/$.?#].[^\s]*$/.test(maybeUrl) || - getEmbedLink(maybeUrl)?.type === "video") + !IS_PLAIN_PASTE && + embbeddableUrls.length > 0 && + // if there were non-embeddable text (lines) mixed in with embeddable + // urls, ignore and paste as text + embbeddableUrls.length === nonEmptyLines.length ) { - const embeddable = this.insertEmbeddableElement({ - sceneX, - sceneY, - link: normalizeLink(maybeUrl), - }); - if (embeddable) { - this.setState({ selectedElementIds: { [embeddable.id]: true } }); + const embeddables: NonDeleted[] = []; + for (const url of embbeddableUrls) { + const prevEmbeddable: ExcalidrawEmbeddableElement | undefined = + embeddables[embeddables.length - 1]; + const embeddable = this.insertEmbeddableElement({ + sceneX: prevEmbeddable + ? prevEmbeddable.x + prevEmbeddable.width + 20 + : sceneX, + sceneY, + link: normalizeLink(url), + }); + if (embeddable) { + embeddables.push(embeddable); + } + } + if (embeddables.length) { + this.setState({ + selectedElementIds: Object.fromEntries( + embeddables.map((embeddable) => [embeddable.id, true]), + ), + }); } return; } @@ -2990,16 +3135,29 @@ class App extends React.Component { }, ); - const nextElements = [ + const allElements = [ ...this.scene.getElementsIncludingDeleted(), ...newElements, ]; - this.scene.replaceAllElements(nextElements); + const topLayerFrame = this.getTopLayerFrameAtSceneCoords({ x, y }); + + if (topLayerFrame) { + const eligibleElements = filterElementsEligibleAsFrameChildren( + newElements, + topLayerFrame, + ); + addElementsToFrame(allElements, eligibleElements, topLayerFrame); + } + + this.scene.replaceAllElements(allElements); newElements.forEach((newElement) => { if (isTextElement(newElement) && isBoundToContainer(newElement)) { - const container = getContainerElement(newElement); + const container = getContainerElement( + newElement, + this.scene.getElementsMapIncludingDeleted(), + ); redrawTextBoundingBox(newElement, container); } }); @@ -3345,7 +3503,7 @@ class App extends React.Component { }); }; - private cancelInProgresAnimation: (() => void) | null = null; + private cancelInProgressAnimation: (() => void) | null = null; scrollToContent = ( target: @@ -3370,7 +3528,7 @@ class App extends React.Component { duration?: number; }, ) => { - this.cancelInProgresAnimation?.(); + this.cancelInProgressAnimation?.(); // convert provided target into ExcalidrawElement[] if necessary const targetElements = Array.isArray(target) ? target : [target]; @@ -3437,20 +3595,27 @@ class App extends React.Component { duration: opts?.duration ?? 500, }); - this.cancelInProgresAnimation = () => { + this.cancelInProgressAnimation = () => { cancel(); - this.cancelInProgresAnimation = null; + this.cancelInProgressAnimation = null; }; } else { this.setState({ scrollX, scrollY, zoom }); } }; + private maybeUnfollowRemoteUser = () => { + if (this.state.userToFollow) { + this.setState({ userToFollow: null }); + } + }; + /** use when changing scrollX/scrollY/zoom based on user interaction */ private translateCanvas: React.Component["setState"] = ( state, ) => { - this.cancelInProgresAnimation?.(); + this.cancelInProgressAnimation?.(); + this.maybeUnfollowRemoteUser(); this.setState(state); }; @@ -3740,7 +3905,11 @@ class App extends React.Component { if (!isTextElement(selectedElement)) { container = selectedElement as ExcalidrawTextContainer; } - const midPoint = getContainerCenter(selectedElement, this.state); + const midPoint = getContainerCenter( + selectedElement, + this.state, + this.scene.getNonDeletedElementsMap(), + ); const sceneX = midPoint.x; const sceneY = midPoint.y; this.startTextEditing({ @@ -4057,11 +4226,18 @@ class App extends React.Component { this.scene.replaceAllElements([ ...this.scene.getElementsIncludingDeleted().map((_element) => { if (_element.id === element.id && isTextElement(_element)) { - return updateTextElement(_element, { - text, - isDeleted, - originalText, - }); + return updateTextElement( + _element, + getContainerElement( + _element, + this.scene.getElementsMapIncludingDeleted(), + ), + { + text, + isDeleted, + originalText, + }, + ); } return _element; }), @@ -4197,6 +4373,7 @@ class App extends React.Component { this.frameNameBoundsCache, x, y, + this.scene.getNonDeletedElementsMap(), ) ? allHitElements[allHitElements.length - 2] : elementWithHighestZIndex; @@ -4226,7 +4403,14 @@ class App extends React.Component { ); return getElementsAtPosition(elements, (element) => - hitTest(element, this.state, this.frameNameBoundsCache, x, y), + hitTest( + element, + this.state, + this.frameNameBoundsCache, + x, + y, + this.scene.getNonDeletedElementsMap(), + ), ).filter((element) => { // hitting a frame's element from outside the frame is not considered a hit const containingFrame = getContainingFrame(element); @@ -4263,7 +4447,10 @@ class App extends React.Component { container, ); if (container && parentCenterPosition) { - const boundTextElementToContainer = getBoundTextElement(container); + const boundTextElementToContainer = getBoundTextElement( + container, + this.scene.getNonDeletedElementsMap(), + ); if (!boundTextElementToContainer) { shouldBindToContainer = true; } @@ -4276,7 +4463,10 @@ class App extends React.Component { if (isTextElement(selectedElements[0])) { existingTextElement = selectedElements[0]; } else if (container) { - existingTextElement = getBoundTextElement(selectedElements[0]); + existingTextElement = getBoundTextElement( + selectedElements[0], + this.scene.getNonDeletedElementsMap(), + ); } else { existingTextElement = this.getTextElementAtPosition(sceneX, sceneY); } @@ -4486,7 +4676,11 @@ class App extends React.Component { [sceneX, sceneY], ) ) { - const midPoint = getContainerCenter(container, this.state); + const midPoint = getContainerCenter( + container, + this.state, + this.scene.getNonDeletedElementsMap(), + ); sceneX = midPoint.x; sceneY = midPoint.y; @@ -4611,6 +4805,7 @@ class App extends React.Component { event: React.PointerEvent, ) => { this.savePointer(event.clientX, event.clientY, this.state.cursorButton); + this.lastPointerMoveEvent = event.nativeEvent; if (gesture.pointers.has(event.pointerId)) { gesture.pointers.set(event.pointerId, { @@ -4706,13 +4901,31 @@ class App extends React.Component { event, ); - this.setState({ - snapLines, - originSnapOffset: originOffset, + this.setState((prevState) => { + const nextSnapLines = updateStable(prevState.snapLines, snapLines); + const nextOriginOffset = prevState.originSnapOffset + ? updateStable(prevState.originSnapOffset, originOffset) + : originOffset; + + if ( + prevState.snapLines === nextSnapLines && + prevState.originSnapOffset === nextOriginOffset + ) { + return null; + } + return { + snapLines: nextSnapLines, + originSnapOffset: nextOriginOffset, + }; }); } else if (!this.state.draggingElement) { - this.setState({ - snapLines: [], + this.setState((prevState) => { + if (prevState.snapLines.length) { + return { + snapLines: [], + }; + } + return null; }); } @@ -4992,30 +5205,48 @@ class App extends React.Component { pointerDownState: PointerDownState, scenePointer: { x: number; y: number }, ) => { - const updateElementIds = (elements: ExcalidrawElement[]) => { - elements.forEach((element) => { + this.eraserTrail.addPointToPath(scenePointer.x, scenePointer.y); + + let didChange = false; + + const processedGroups = new Set(); + const nonDeletedElements = this.scene.getNonDeletedElements(); + + const processElements = (elements: ExcalidrawElement[]) => { + for (const element of elements) { if (element.locked) { return; } - idsToUpdate.push(element.id); if (event.altKey) { - if ( - pointerDownState.elementIdsToErase[element.id] && - pointerDownState.elementIdsToErase[element.id].erase - ) { - pointerDownState.elementIdsToErase[element.id].erase = false; + if (this.elementsPendingErasure.delete(element.id)) { + didChange = true; } - } else if (!pointerDownState.elementIdsToErase[element.id]) { - pointerDownState.elementIdsToErase[element.id] = { - erase: true, - opacity: element.opacity, - }; + } else if (!this.elementsPendingErasure.has(element.id)) { + didChange = true; + this.elementsPendingErasure.add(element.id); } - }); - }; - const idsToUpdate: Array = []; + // (un)erase groups atomically + if (didChange && element.groupIds?.length) { + const shallowestGroupId = element.groupIds.at(-1)!; + if (!processedGroups.has(shallowestGroupId)) { + processedGroups.add(shallowestGroupId); + const elems = getElementsInGroup( + nonDeletedElements, + shallowestGroupId, + ); + for (const elem of elems) { + if (event.altKey) { + this.elementsPendingErasure.delete(elem.id); + } else { + this.elementsPendingErasure.add(elem.id); + } + } + } + } + } + }; const distance = distance2d( pointerDownState.lastCoords.x, @@ -5028,7 +5259,7 @@ class App extends React.Component { let samplingInterval = 0; while (samplingInterval <= distance) { const hitElements = this.getElementsAtPosition(point.x, point.y); - updateElementIds(hitElements); + processElements(hitElements); // Exit since we reached current point if (samplingInterval === distance) { @@ -5047,35 +5278,31 @@ class App extends React.Component { point.y = nextY; } - const elements = this.scene.getElementsIncludingDeleted().map((ele) => { - const id = - isBoundToContainer(ele) && idsToUpdate.includes(ele.containerId) - ? ele.containerId - : ele.id; - if (idsToUpdate.includes(id)) { - if (event.altKey) { - if ( - pointerDownState.elementIdsToErase[id] && - pointerDownState.elementIdsToErase[id].erase === false - ) { - return newElementWith(ele, { - opacity: pointerDownState.elementIdsToErase[id].opacity, - }); - } - } else { - return newElementWith(ele, { - opacity: ELEMENT_READY_TO_ERASE_OPACITY, - }); - } - } - return ele; - }); - - this.scene.replaceAllElements(elements); - pointerDownState.lastCoords.x = scenePointer.x; pointerDownState.lastCoords.y = scenePointer.y; + + if (didChange) { + for (const element of this.scene.getNonDeletedElements()) { + if ( + isBoundToContainer(element) && + (this.elementsPendingErasure.has(element.id) || + this.elementsPendingErasure.has(element.containerId)) + ) { + if (event.altKey) { + this.elementsPendingErasure.delete(element.id); + this.elementsPendingErasure.delete(element.containerId); + } else { + this.elementsPendingErasure.add(element.id); + this.elementsPendingErasure.add(element.containerId); + } + } + } + + this.elementsPendingErasure = new Set(this.elementsPendingErasure); + this.onSceneUpdated(); + } }; + // set touch moving for mobile context menu private handleTouchMove = (event: React.TouchEvent) => { invalidateContextMenu = true; @@ -5089,8 +5316,8 @@ class App extends React.Component { const element = LinearElementEditor.getElement( linearElementEditor.elementId, ); - - const boundTextElement = getBoundTextElement(element); + const elementsMap = this.scene.getNonDeletedElementsMap(); + const boundTextElement = getBoundTextElement(element, elementsMap); if (!element) { return; @@ -5117,6 +5344,7 @@ class App extends React.Component { linearElementEditor, { x: scenePointerX, y: scenePointerY }, this.state, + this.scene.getNonDeletedElementsMap(), ); if (hoverPointIndex >= 0 || segmentMidPointHoveredCoords) { @@ -5132,6 +5360,7 @@ class App extends React.Component { this.frameNameBoundsCache, scenePointerX, scenePointerY, + elementsMap, ) ) { setCursor(this.interactiveCanvas, CURSOR_TYPE.MOVE); @@ -5143,6 +5372,7 @@ class App extends React.Component { this.frameNameBoundsCache, scenePointerX, scenePointerY, + this.scene.getNonDeletedElementsMap(), ) ) { setCursor(this.interactiveCanvas, CURSOR_TYPE.MOVE); @@ -5180,6 +5410,9 @@ class App extends React.Component { private handleCanvasPointerDown = ( event: React.PointerEvent, ) => { + this.maybeCleanupAfterMissingPointerUp(event.nativeEvent); + this.maybeUnfollowRemoteUser(); + // since contextMenu options are potentially evaluated on each render, // and an contextMenu action may depend on selection state, we must // close the contextMenu before we update the selection on pointerDown @@ -5240,7 +5473,6 @@ class App extends React.Component { selection.removeAllRanges(); } this.maybeOpenContextMenuAfterPointerDownOnTouchDevices(event); - this.maybeCleanupAfterMissingPointerUp(event); //fires only once, if pen is detected, penMode is enabled //the user can disable this by toggling the penMode button @@ -5279,10 +5511,60 @@ class App extends React.Component { }); this.savePointer(event.clientX, event.clientY, "down"); + if ( + event.button === POINTER_BUTTON.ERASER && + this.state.activeTool.type !== TOOL_TYPE.eraser + ) { + this.setState( + { + activeTool: updateActiveTool(this.state, { + type: TOOL_TYPE.eraser, + lastActiveToolBeforeEraser: this.state.activeTool, + }), + }, + () => { + this.handleCanvasPointerDown(event); + const onPointerUp = () => { + unsubPointerUp(); + unsubCleanup?.(); + if (isEraserActive(this.state)) { + this.setState({ + activeTool: updateActiveTool(this.state, { + ...(this.state.activeTool.lastActiveTool || { + type: TOOL_TYPE.selection, + }), + lastActiveToolBeforeEraser: null, + }), + }); + } + }; + + const unsubPointerUp = addEventListener( + window, + EVENT.POINTER_UP, + onPointerUp, + { + once: true, + }, + ); + let unsubCleanup: UnsubscribeCallback | undefined; + // subscribe inside rAF lest it'd be triggered on the same pointerdown + // if we start erasing while coming from blurred document since + // we cleanup pointer events on focus + requestAnimationFrame(() => { + unsubCleanup = + this.missingPointerEventCleanupEmitter.once(onPointerUp); + }); + }, + ); + return; + } + // only handle left mouse button or touch if ( event.button !== POINTER_BUTTON.MAIN && - event.button !== POINTER_BUTTON.TOUCH + event.button !== POINTER_BUTTON.TOUCH && + event.button !== POINTER_BUTTON.ERASER ) { return; } @@ -5380,7 +5662,7 @@ class App extends React.Component { this.state.activeTool.type, ); } else if (this.state.activeTool.type === "laser") { - this.laserPathManager.startPath( + this.laserTrails.startPath( pointerDownState.lastCoords.x, pointerDownState.lastCoords.y, ); @@ -5401,6 +5683,13 @@ class App extends React.Component { event, ); + if (this.state.activeTool.type === "eraser") { + this.eraserTrail.startPath( + pointerDownState.lastCoords.x, + pointerDownState.lastCoords.y, + ); + } + const onPointerMove = this.onPointerMoveFromPointerDownHandler(pointerDownState); @@ -5410,7 +5699,9 @@ class App extends React.Component { const onKeyDown = this.onKeyDownFromPointerDownHandler(pointerDownState); const onKeyUp = this.onKeyUpFromPointerDownHandler(pointerDownState); - lastPointerUp = onPointerUp; + this.missingPointerEventCleanupEmitter.once((_event) => + onPointerUp(_event || event.nativeEvent), + ); if (!this.state.viewModeEnabled || this.state.activeTool.type === "laser") { window.addEventListener(EVENT.POINTER_MOVE, onPointerMove); @@ -5521,16 +5812,15 @@ class App extends React.Component { invalidateContextMenu = false; }; - private maybeCleanupAfterMissingPointerUp( - event: React.PointerEvent, - ): void { - if (lastPointerUp !== null) { - // Unfortunately, sometimes we don't get a pointerup after a pointerdown, - // this can happen when a contextual menu or alert is triggered. In order to avoid - // being in a weird state, we clean up on the next pointerdown - lastPointerUp(event); - } - } + /** + * pointerup may not fire in certian cases (user tabs away...), so in order + * to properly cleanup pointerdown state, we need to fire any hanging + * pointerup handlers manually + */ + private maybeCleanupAfterMissingPointerUp = (event: PointerEvent | null) => { + lastPointerUp?.(); + this.missingPointerEventCleanupEmitter.trigger(event).clear(); + }; // Returns whether the event is a panning private handleCanvasPanUsingWheelOrSpaceDrag = ( @@ -5552,7 +5842,10 @@ class App extends React.Component { event.preventDefault(); let nextPastePrevented = false; - const isLinux = /Linux/.test(window.navigator.platform); + const isLinux = + typeof window === undefined + ? false + : /Linux/.test(window.navigator.platform); setCursor(this.interactiveCanvas, CURSOR_TYPE.GRABBING); let { clientX: lastX, clientY: lastY } = event; @@ -5708,7 +6001,6 @@ class App extends React.Component { boxSelection: { hasOccurred: false, }, - elementIdsToErase: {}, }; } @@ -5733,11 +6025,10 @@ class App extends React.Component { this.handlePointerMoveOverScrollbars(event, pointerDownState); }); - const onPointerUp = withBatchedUpdates(() => { + lastPointerUp = null; isDraggingScrollBar = false; setCursorForShape(this.interactiveCanvas, this.state); - lastPointerUp = null; this.setState({ cursorButton: "up", }); @@ -5831,6 +6122,7 @@ class App extends React.Component { this.history, pointerDownState.origin, linearElementEditor, + this.scene.getNonDeletedElementsMap(), ); if (ret.hitElement) { pointerDownState.hit.element = ret.hitElement; @@ -6245,8 +6537,11 @@ class App extends React.Component { return; } - if (embedLink.warning) { - this.setToast({ message: embedLink.warning, closable: true }); + if (embedLink.error instanceof URIError) { + this.setToast({ + message: t("toast.unrecognizedLinkFormat"), + closable: true, + }); } const element = newEmbeddableElement({ @@ -6265,7 +6560,6 @@ class App extends React.Component { width: embedLink.intrinsicSize.w, height: embedLink.intrinsicSize.h, link, - validated: null, }); this.scene.replaceAllElements([ @@ -6502,7 +6796,6 @@ class App extends React.Component { if (elementType === "embeddable") { element = newEmbeddableElement({ type: "embeddable", - validated: null, ...baseElementAttributes, }); } else { @@ -6668,7 +6961,7 @@ class App extends React.Component { } if (this.state.activeTool.type === "laser") { - this.laserPathManager.addPointToPath(pointerCoords.x, pointerCoords.y); + this.laserTrails.addPointToPath(pointerCoords.x, pointerCoords.y); } const [gridX, gridY] = getGridPoint( @@ -6771,6 +7064,7 @@ class App extends React.Component { ); }, linearElementEditor, + this.scene.getNonDeletedElementsMap(), ); if (didDrag) { pointerDownState.lastCoords.x = pointerCoords.x; @@ -7186,6 +7480,7 @@ class App extends React.Component { pointerDownState: PointerDownState, ): (event: PointerEvent) => void { return withBatchedUpdates((childEvent: PointerEvent) => { + this.removePointer(childEvent); if (pointerDownState.eventListeners.onMove) { pointerDownState.eventListeners.onMove.flush(); } @@ -7198,7 +7493,7 @@ class App extends React.Component { isRotating, } = this.state; - this.setState({ + this.setState((prevState) => ({ isResizing: false, isRotating: false, resizingElement: null, @@ -7212,10 +7507,10 @@ class App extends React.Component { multiElement || isTextElement(this.state.editingElement) ? this.state.editingElement : null, - snapLines: [], + snapLines: updateStable(prevState.snapLines, []), originSnapOffset: null, - }); + })); SnapCache.setReferenceSnapPoints(null); SnapCache.setVisibleGaps(null); @@ -7288,7 +7583,7 @@ class App extends React.Component { } } - lastPointerUp = null; + this.missingPointerEventCleanupEmitter.clear(); window.removeEventListener( EVENT.POINTER_MOVE, @@ -7311,6 +7606,7 @@ class App extends React.Component { this.setState({ pendingImageElementId: null }); } + this.props?.onPointerUp?.(activeTool, pointerDownState); this.onPointerUpEmitter.trigger( this.state.activeTool, pointerDownState, @@ -7488,13 +7784,12 @@ class App extends React.Component { groupIds: [], }); - this.scene.replaceAllElements( - removeElementsFromFrame( - this.scene.getElementsIncludingDeleted(), - [linearElement], - this.state, - ), + removeElementsFromFrame( + [linearElement], + this.scene.getNonDeletedElementsMap(), ); + + this.scene.informMutation(); } } } @@ -7504,7 +7799,7 @@ class App extends React.Component { this.getTopLayerFrameAtSceneCoords(sceneCoords); const selectedElements = this.scene.getSelectedElements(this.state); - let nextElements = this.scene.getElementsIncludingDeleted(); + let nextElements = this.scene.getElementsMapIncludingDeleted(); const updateGroupIdsAfterEditingGroup = ( elements: ExcalidrawElement[], @@ -7597,7 +7892,7 @@ class App extends React.Component { this.scene.replaceAllElements( addElementsToFrame( - this.scene.getElementsIncludingDeleted(), + this.scene.getElementsMapIncludingDeleted(), elementsInsideFrame, draggingElement, ), @@ -7645,7 +7940,7 @@ class App extends React.Component { this.state, ), frame, - this.state, + this, ); } @@ -7671,19 +7966,25 @@ class App extends React.Component { }); } } - if (isEraserActive(this.state)) { + + const pointerStart = this.lastPointerDownEvent; + const pointerEnd = this.lastPointerUpEvent || this.lastPointerMoveEvent; + + if (isEraserActive(this.state) && pointerStart && pointerEnd) { + this.eraserTrail.endPath(); + const draggedDistance = distance2d( - this.lastPointerDownEvent!.clientX, - this.lastPointerDownEvent!.clientY, - this.lastPointerUpEvent!.clientX, - this.lastPointerUpEvent!.clientY, + pointerStart.clientX, + pointerStart.clientY, + pointerEnd.clientX, + pointerEnd.clientY, ); if (draggedDistance === 0) { const scenePointer = viewportCoordsToSceneCoords( { - clientX: this.lastPointerUpEvent!.clientX, - clientY: this.lastPointerUpEvent!.clientY, + clientX: pointerEnd.clientX, + clientY: pointerEnd.clientY, }, this.state, ); @@ -7691,18 +7992,14 @@ class App extends React.Component { scenePointer.x, scenePointer.y, ); - hitElements.forEach( - (hitElement) => - (pointerDownState.elementIdsToErase[hitElement.id] = { - erase: true, - opacity: hitElement.opacity, - }), + hitElements.forEach((hitElement) => + this.elementsPendingErasure.add(hitElement.id), ); } - this.eraseElements(pointerDownState); + this.eraseElements(); return; - } else if (Object.keys(pointerDownState.elementIdsToErase).length) { - this.restoreReadyToEraseElements(pointerDownState); + } else if (this.elementsPendingErasure.size) { + this.restoreReadyToEraseElements(); } if ( @@ -7749,7 +8046,7 @@ class App extends React.Component { ), }; }); - // if not gragging a linear element point (outside editor) + // if not dragging a linear element point (outside editor) } else if (!this.state.selectedLinearElement?.isDragging) { // remove element from selection while // keeping prev elements selected @@ -7871,6 +8168,7 @@ class App extends React.Component { this.frameNameBoundsCache, pointerDownState.origin.x, pointerDownState.origin.y, + this.scene.getNonDeletedElementsMap(), )) || (!hitElement && pointerDownState.hit.hasHitCommonBoundingBoxOfSelectedElements)) @@ -7924,7 +8222,7 @@ class App extends React.Component { } if (activeTool.type === "laser") { - this.laserPathManager.endPath(); + this.laserTrails.endPath(); return; } @@ -7963,65 +8261,32 @@ class App extends React.Component { }); } - private restoreReadyToEraseElements = ( - pointerDownState: PointerDownState, - ) => { - const elements = this.scene.getElementsIncludingDeleted().map((ele) => { - if ( - pointerDownState.elementIdsToErase[ele.id] && - pointerDownState.elementIdsToErase[ele.id].erase - ) { - return newElementWith(ele, { - opacity: pointerDownState.elementIdsToErase[ele.id].opacity, - }); - } else if ( - isBoundToContainer(ele) && - pointerDownState.elementIdsToErase[ele.containerId] && - pointerDownState.elementIdsToErase[ele.containerId].erase - ) { - return newElementWith(ele, { - opacity: pointerDownState.elementIdsToErase[ele.containerId].opacity, - }); - } else if ( - ele.frameId && - pointerDownState.elementIdsToErase[ele.frameId] && - pointerDownState.elementIdsToErase[ele.frameId].erase - ) { - return newElementWith(ele, { - opacity: pointerDownState.elementIdsToErase[ele.frameId].opacity, - }); - } - return ele; - }); - - this.scene.replaceAllElements(elements); + private restoreReadyToEraseElements = () => { + this.elementsPendingErasure = new Set(); + this.onSceneUpdated(); }; - private eraseElements = (pointerDownState: PointerDownState) => { + private eraseElements = () => { + let didChange = false; const elements = this.scene.getElementsIncludingDeleted().map((ele) => { if ( - pointerDownState.elementIdsToErase[ele.id] && - pointerDownState.elementIdsToErase[ele.id].erase - ) { - return newElementWith(ele, { isDeleted: true }); - } else if ( - isBoundToContainer(ele) && - pointerDownState.elementIdsToErase[ele.containerId] && - pointerDownState.elementIdsToErase[ele.containerId].erase - ) { - return newElementWith(ele, { isDeleted: true }); - } else if ( - ele.frameId && - pointerDownState.elementIdsToErase[ele.frameId] && - pointerDownState.elementIdsToErase[ele.frameId].erase + this.elementsPendingErasure.has(ele.id) || + (ele.frameId && this.elementsPendingErasure.has(ele.frameId)) || + (isBoundToContainer(ele) && + this.elementsPendingErasure.has(ele.containerId)) ) { + didChange = true; return newElementWith(ele, { isDeleted: true }); } return ele; }); - this.history.resumeRecording(); - this.scene.replaceAllElements(elements); + this.elementsPendingErasure = new Set(); + + if (didChange) { + this.history.resumeRecording(); + this.scene.replaceAllElements(elements); + } }; private initializeImage = async ({ @@ -8074,7 +8339,10 @@ class App extends React.Component { maxWidthOrHeight: DEFAULT_MAX_IMAGE_WIDTH_OR_HEIGHT, }); } catch (error: any) { - console.error("error trying to resing image file on insertion", error); + console.error( + "Error trying to resizing image file on insertion", + error, + ); } if (imageFile.size > MAX_ALLOWED_FILE_BYTES) { @@ -8617,7 +8885,7 @@ class App extends React.Component { } if (file) { - // atetmpt to parse an excalidraw/excalidrawlib file + // Attempt to parse an excalidraw/excalidrawlib file await this.loadFileToCanvas(file, fileHandle); } @@ -8716,13 +8984,13 @@ class App extends React.Component { }); const selectedElements = this.scene.getSelectedElements(this.state); - const isHittignCommonBoundBox = + const isHittingCommonBoundBox = this.isHittingCommonBoundingBoxOfSelectedElements( { x, y }, selectedElements, ); - const type = element || isHittignCommonBoundBox ? "element" : "canvas"; + const type = element || isHittingCommonBoundBox ? "element" : "canvas"; const container = this.excalidrawContainerRef.current!; const { top: offsetTop, left: offsetLeft } = @@ -8953,10 +9221,10 @@ class App extends React.Component { if ( transformElements( - pointerDownState, + pointerDownState.originalElements, transformHandleType, selectedElements, - pointerDownState.resize.arrowDirection, + this.scene.getElementsMapIncludingDeleted(), shouldRotateWithDiscreteAngle(event), shouldResizeFromCenter(event), selectedElements.length === 1 && isImageElement(selectedElements[0]) @@ -8966,7 +9234,6 @@ class App extends React.Component { resizeY, pointerDownState.resize.center.x, pointerDownState.resize.center.y, - this.state, ) ) { this.maybeSuggestBindingForAll(selectedElements); @@ -9199,7 +9466,11 @@ class App extends React.Component { let elementCenterX = container.x + container.width / 2; let elementCenterY = container.y + container.height / 2; - const elementCenter = getContainerCenter(container, appState); + const elementCenter = getContainerCenter( + container, + appState, + this.scene.getNonDeletedElementsMap(), + ); if (elementCenter) { elementCenterX = elementCenter.x; elementCenterY = elementCenter.y; diff --git a/packages/excalidraw/components/Avatar.scss b/packages/excalidraw/components/Avatar.scss new file mode 100644 index 000000000..6565816e3 --- /dev/null +++ b/packages/excalidraw/components/Avatar.scss @@ -0,0 +1,7 @@ +@import "../css/variables.module.scss"; + +.excalidraw { + .Avatar { + @include avatarStyles; + } +} diff --git a/src/components/Avatar.tsx b/packages/excalidraw/components/Avatar.tsx similarity index 64% rename from src/components/Avatar.tsx rename to packages/excalidraw/components/Avatar.tsx index 8b4624b7f..b7b1bf962 100644 --- a/src/components/Avatar.tsx +++ b/packages/excalidraw/components/Avatar.tsx @@ -2,21 +2,38 @@ import "./Avatar.scss"; import React, { useState } from "react"; import { getNameInitial } from "../clients"; +import clsx from "clsx"; type AvatarProps = { onClick: (e: React.MouseEvent) => void; color: string; name: string; src?: string; + isBeingFollowed?: boolean; + isCurrentUser: boolean; }; -export const Avatar = ({ color, onClick, name, src }: AvatarProps) => { +export const Avatar = ({ + color, + onClick, + name, + src, + isBeingFollowed, + isCurrentUser, +}: AvatarProps) => { const shortName = getNameInitial(name); const [error, setError] = useState(false); const loadImg = !error && src; const style = loadImg ? undefined : { background: color }; return ( -
+
{loadImg ? ( void; + + variant?: ButtonVariant; + color?: ButtonColor; + size?: ButtonSize; + className?: string; + fullWidth?: boolean; + + icon?: React.ReactNode; +}; + +export const FilledButton = forwardRef( + ( + { + children, + icon, + onClick, + label, + variant = "filled", + color = "primary", + size = "medium", + fullWidth, + className, + }, + ref, + ) => { + const [isLoading, setIsLoading] = useState(false); + + const _onClick = async (event: React.MouseEvent) => { + const ret = onClick?.(event); + + if (isPromiseLike(ret)) { + try { + setIsLoading(true); + await ret; + } catch (error: any) { + if (!(error instanceof AbortError)) { + throw error; + } else { + console.warn(error); + } + } finally { + setIsLoading(false); + } + } + }; + + return ( + + ); + }, +); diff --git a/src/components/FixedSideContainer.scss b/packages/excalidraw/components/FixedSideContainer.scss similarity index 95% rename from src/components/FixedSideContainer.scss rename to packages/excalidraw/components/FixedSideContainer.scss index 62d77d505..87819ef68 100644 --- a/src/components/FixedSideContainer.scss +++ b/packages/excalidraw/components/FixedSideContainer.scss @@ -1,4 +1,4 @@ -@import "../css/variables.module"; +@import "../css/variables.module.scss"; .excalidraw { .FixedSideContainer { diff --git a/src/components/FixedSideContainer.tsx b/packages/excalidraw/components/FixedSideContainer.tsx similarity index 100% rename from src/components/FixedSideContainer.tsx rename to packages/excalidraw/components/FixedSideContainer.tsx diff --git a/packages/excalidraw/components/FollowMode/FollowMode.scss b/packages/excalidraw/components/FollowMode/FollowMode.scss new file mode 100644 index 000000000..383b3ceed --- /dev/null +++ b/packages/excalidraw/components/FollowMode/FollowMode.scss @@ -0,0 +1,59 @@ +.excalidraw { + .follow-mode { + position: absolute; + box-sizing: border-box; + pointer-events: none; + border: 2px solid var(--color-primary-hover); + z-index: 9999; + display: flex; + align-items: flex-end; + justify-content: center; + + &__badge { + background-color: var(--color-primary-hover); + color: var(--color-primary-light); + padding: 0.25rem 0.5rem; + margin-bottom: 0.5rem; + border-radius: 0.5rem; + pointer-events: all; + font-size: 0.75rem; + display: flex; + gap: 0.5rem; + align-items: center; + + &__label { + display: flex; + white-space: pre-wrap; + line-height: 1; + } + + &__username { + display: block; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 100px; + } + } + + &__disconnect-btn { + all: unset; + cursor: pointer; + border-radius: 0.25rem; + + &:hover { + background-color: var(--color-primary-darker); + } + + &:active { + background-color: var(--color-primary-darkest); + } + + svg { + display: block; + width: 1rem; + height: 1rem; + } + } + } +} diff --git a/packages/excalidraw/components/FollowMode/FollowMode.tsx b/packages/excalidraw/components/FollowMode/FollowMode.tsx new file mode 100644 index 000000000..dc1746ca8 --- /dev/null +++ b/packages/excalidraw/components/FollowMode/FollowMode.tsx @@ -0,0 +1,38 @@ +import { UserToFollow } from "../../types"; +import { CloseIcon } from "../icons"; +import "./FollowMode.scss"; + +interface FollowModeProps { + width: number; + height: number; + userToFollow: UserToFollow; + onDisconnect: () => void; +} + +const FollowMode = ({ + height, + width, + userToFollow, + onDisconnect, +}: FollowModeProps) => { + return ( +
+
+
+ Following{" "} + + {userToFollow.username} + +
+ +
+
+ ); +}; + +export default FollowMode; diff --git a/src/components/HandButton.tsx b/packages/excalidraw/components/HandButton.tsx similarity index 100% rename from src/components/HandButton.tsx rename to packages/excalidraw/components/HandButton.tsx diff --git a/src/components/HelpButton.tsx b/packages/excalidraw/components/HelpButton.tsx similarity index 100% rename from src/components/HelpButton.tsx rename to packages/excalidraw/components/HelpButton.tsx diff --git a/src/components/HelpDialog.scss b/packages/excalidraw/components/HelpDialog.scss similarity index 98% rename from src/components/HelpDialog.scss rename to packages/excalidraw/components/HelpDialog.scss index 0722c9723..7a3224beb 100644 --- a/src/components/HelpDialog.scss +++ b/packages/excalidraw/components/HelpDialog.scss @@ -1,4 +1,4 @@ -@import "../css/variables.module"; +@import "../css/variables.module.scss"; .excalidraw { .HelpDialog { diff --git a/src/components/HelpDialog.tsx b/packages/excalidraw/components/HelpDialog.tsx similarity index 100% rename from src/components/HelpDialog.tsx rename to packages/excalidraw/components/HelpDialog.tsx diff --git a/src/components/HintViewer.scss b/packages/excalidraw/components/HintViewer.scss similarity index 93% rename from src/components/HintViewer.scss rename to packages/excalidraw/components/HintViewer.scss index fdbbd0fe6..58a376e16 100644 --- a/src/components/HintViewer.scss +++ b/packages/excalidraw/components/HintViewer.scss @@ -1,4 +1,4 @@ -@import "../css/variables.module"; +@import "../css/variables.module.scss"; // this is loosely based on the longest hint text $wide-viewport-width: 1000px; diff --git a/src/components/HintViewer.tsx b/packages/excalidraw/components/HintViewer.tsx similarity index 100% rename from src/components/HintViewer.tsx rename to packages/excalidraw/components/HintViewer.tsx diff --git a/src/components/IconPicker.scss b/packages/excalidraw/components/IconPicker.scss similarity index 98% rename from src/components/IconPicker.scss rename to packages/excalidraw/components/IconPicker.scss index 23ec7c36c..2cf87c474 100644 --- a/src/components/IconPicker.scss +++ b/packages/excalidraw/components/IconPicker.scss @@ -1,4 +1,4 @@ -@import "../css/variables.module"; +@import "../css/variables.module.scss"; .excalidraw { .picker-container { diff --git a/src/components/IconPicker.tsx b/packages/excalidraw/components/IconPicker.tsx similarity index 88% rename from src/components/IconPicker.tsx rename to packages/excalidraw/components/IconPicker.tsx index 30b8a97b1..3295c4a04 100644 --- a/src/components/IconPicker.tsx +++ b/packages/excalidraw/components/IconPicker.tsx @@ -15,7 +15,12 @@ function Picker({ }: { label: string; value: T; - options: { value: T; text: string; icon: JSX.Element; keyBinding: string }[]; + options: { + value: T; + text: string; + icon: JSX.Element; + keyBinding: string | null; + }[]; onChange: (value: T) => void; onClose: () => void; }) { @@ -110,9 +115,11 @@ function Picker({ (event.currentTarget as HTMLButtonElement).focus(); onChange(option.value); }} - title={`${option.text} — ${option.keyBinding.toUpperCase()}`} + title={`${option.text} ${ + option.keyBinding && `— ${option.keyBinding.toUpperCase()}` + }`} aria-label={option.text || "none"} - aria-keyshortcuts={option.keyBinding} + aria-keyshortcuts={option.keyBinding || undefined} key={option.text} ref={(el) => { if (el && i === 0) { @@ -127,7 +134,9 @@ function Picker({ }} > {option.icon} - {option.keyBinding} + {option.keyBinding && ( + {option.keyBinding} + )} ))}
@@ -144,7 +153,13 @@ export function IconPicker({ }: { label: string; value: T; - options: { value: T; text: string; icon: JSX.Element; keyBinding: string }[]; + options: readonly { + value: T; + text: string; + icon: JSX.Element; + keyBinding: string | null; + showInPicker?: boolean; + }[]; onChange: (value: T) => void; group?: string; }) { @@ -173,7 +188,7 @@ export function IconPicker({ {...(isRTL ? { right: 5.5 } : { left: -5.5 })} > opt.showInPicker !== false)} value={value} label={label} onChange={onChange} diff --git a/src/components/ImageExportDialog.scss b/packages/excalidraw/components/ImageExportDialog.scss similarity index 98% rename from src/components/ImageExportDialog.scss rename to packages/excalidraw/components/ImageExportDialog.scss index 093e1a76f..ea9e74f80 100644 --- a/src/components/ImageExportDialog.scss +++ b/packages/excalidraw/components/ImageExportDialog.scss @@ -1,4 +1,4 @@ -@import "../css/variables.module"; +@import "../css/variables.module.scss"; .excalidraw { --ImageExportModal-preview-border: #d6d6d6; @@ -12,6 +12,8 @@ flex-direction: row; justify-content: space-between; + user-select: none; + & h3 { font-family: "Assistant"; font-style: normal; diff --git a/src/components/ImageExportDialog.tsx b/packages/excalidraw/components/ImageExportDialog.tsx similarity index 98% rename from src/components/ImageExportDialog.tsx rename to packages/excalidraw/components/ImageExportDialog.tsx index c85272abf..7ca54e985 100644 --- a/src/components/ImageExportDialog.tsx +++ b/packages/excalidraw/components/ImageExportDialog.tsx @@ -23,7 +23,7 @@ import { nativeFileSystemSupported } from "../data/filesystem"; import { NonDeletedExcalidrawElement } from "../element/types"; import { t } from "../i18n"; import { isSomeElementSelected } from "../scene"; -import { exportToCanvas } from "../packages/utils"; +import { exportToCanvas } from "../../utils/export"; import { copyIcon, downloadIcon, helpIcon } from "./icons"; import { Dialog } from "./Dialog"; @@ -271,7 +271,7 @@ const ImageExportModal = ({ exportingFrame, }) } - startIcon={downloadIcon} + icon={downloadIcon} > {t("imageExportDialog.button.exportToPng")} @@ -283,7 +283,7 @@ const ImageExportModal = ({ exportingFrame, }) } - startIcon={downloadIcon} + icon={downloadIcon} > {t("imageExportDialog.button.exportToSvg")} @@ -296,7 +296,7 @@ const ImageExportModal = ({ exportingFrame, }) } - startIcon={copyIcon} + icon={copyIcon} > {t("imageExportDialog.button.copyPngToClipboard")} diff --git a/src/components/InitializeApp.tsx b/packages/excalidraw/components/InitializeApp.tsx similarity index 100% rename from src/components/InitializeApp.tsx rename to packages/excalidraw/components/InitializeApp.tsx diff --git a/src/components/InlineIcon.tsx b/packages/excalidraw/components/InlineIcon.tsx similarity index 100% rename from src/components/InlineIcon.tsx rename to packages/excalidraw/components/InlineIcon.tsx diff --git a/src/components/Island.scss b/packages/excalidraw/components/Island.scss similarity index 100% rename from src/components/Island.scss rename to packages/excalidraw/components/Island.scss diff --git a/src/components/Island.tsx b/packages/excalidraw/components/Island.tsx similarity index 100% rename from src/components/Island.tsx rename to packages/excalidraw/components/Island.tsx diff --git a/src/components/JSONExportDialog.tsx b/packages/excalidraw/components/JSONExportDialog.tsx similarity index 99% rename from src/components/JSONExportDialog.tsx rename to packages/excalidraw/components/JSONExportDialog.tsx index b5cea4af6..95f4117fc 100644 --- a/src/components/JSONExportDialog.tsx +++ b/packages/excalidraw/components/JSONExportDialog.tsx @@ -78,7 +78,7 @@ const JSONExportModal = ({ onClick={async () => { try { trackEvent("export", "link", `ui (${getFrame()})`); - await onExportToBackend(elements, appState, files, canvas); + await onExportToBackend(elements, appState, files); onCloseRequest(); } catch (error: any) { setAppState({ errorMessage: error.message }); diff --git a/src/components/LaserTool/LaserPointerButton.tsx b/packages/excalidraw/components/LaserPointerButton.tsx similarity index 87% rename from src/components/LaserTool/LaserPointerButton.tsx rename to packages/excalidraw/components/LaserPointerButton.tsx index dbb843293..ae3cfb31a 100644 --- a/src/components/LaserTool/LaserPointerButton.tsx +++ b/packages/excalidraw/components/LaserPointerButton.tsx @@ -1,8 +1,8 @@ -import "../ToolIcon.scss"; +import "./ToolIcon.scss"; import clsx from "clsx"; -import { ToolButtonSize } from "../ToolButton"; -import { laserPointerToolIcon } from "../icons"; +import { ToolButtonSize } from "./ToolButton"; +import { laserPointerToolIcon } from "./icons"; type LaserPointerIconProps = { title?: string; diff --git a/src/components/LayerUI.scss b/packages/excalidraw/components/LayerUI.scss similarity index 98% rename from src/components/LayerUI.scss rename to packages/excalidraw/components/LayerUI.scss index 8898b0f83..17b277a2a 100644 --- a/src/components/LayerUI.scss +++ b/packages/excalidraw/components/LayerUI.scss @@ -1,5 +1,5 @@ @import "open-color/open-color"; -@import "../css/variables.module"; +@import "../css/variables.module.scss"; .excalidraw { .layer-ui__wrapper.animate { diff --git a/src/components/LayerUI.tsx b/packages/excalidraw/components/LayerUI.tsx similarity index 98% rename from src/components/LayerUI.tsx rename to packages/excalidraw/components/LayerUI.tsx index 204ff1ee5..068c7f93c 100644 --- a/src/components/LayerUI.tsx +++ b/packages/excalidraw/components/LayerUI.tsx @@ -37,7 +37,7 @@ import { UserList } from "./UserList"; import { JSONExportDialog } from "./JSONExportDialog"; import { PenModeButton } from "./PenModeButton"; import { trackEvent } from "../analytics"; -import { useDevice } from "../components/App"; +import { useDevice } from "./App"; import { Stats } from "./Stats"; import { actionToggleStats } from "../actions/actionToggleStats"; import Footer from "./footer/Footer"; @@ -61,7 +61,7 @@ import { mutateElement } from "../element/mutateElement"; import { ShapeCache } from "../scene/ShapeCache"; import Scene from "../scene/Scene"; import { SubtypeToggles } from "./Subtypes"; -import { LaserPointerButton } from "./LaserTool/LaserPointerButton"; +import { LaserPointerButton } from "./LaserPointerButton"; import { MagicSettings } from "./MagicSettings"; import { TTDDialog } from "./TTDDialog/TTDDialog"; @@ -227,7 +227,7 @@ const LayerUI = ({ > @@ -340,7 +340,12 @@ const LayerUI = ({ }, )} > - + {appState.collaborators.size > 0 && ( + + )} {renderTopRightUI?.(device.editor.isMobile, appState)} {!appState.viewModeEnabled && // hide button when sidebar docked diff --git a/src/components/LibraryMenu.scss b/packages/excalidraw/components/LibraryMenu.scss similarity index 100% rename from src/components/LibraryMenu.scss rename to packages/excalidraw/components/LibraryMenu.scss diff --git a/src/components/LibraryMenu.tsx b/packages/excalidraw/components/LibraryMenu.tsx similarity index 100% rename from src/components/LibraryMenu.tsx rename to packages/excalidraw/components/LibraryMenu.tsx diff --git a/src/components/LibraryMenuBrowseButton.tsx b/packages/excalidraw/components/LibraryMenuBrowseButton.tsx similarity index 100% rename from src/components/LibraryMenuBrowseButton.tsx rename to packages/excalidraw/components/LibraryMenuBrowseButton.tsx diff --git a/src/components/LibraryMenuControlButtons.tsx b/packages/excalidraw/components/LibraryMenuControlButtons.tsx similarity index 100% rename from src/components/LibraryMenuControlButtons.tsx rename to packages/excalidraw/components/LibraryMenuControlButtons.tsx diff --git a/src/components/LibraryMenuHeaderContent.tsx b/packages/excalidraw/components/LibraryMenuHeaderContent.tsx similarity index 100% rename from src/components/LibraryMenuHeaderContent.tsx rename to packages/excalidraw/components/LibraryMenuHeaderContent.tsx diff --git a/src/components/LibraryMenuItems.scss b/packages/excalidraw/components/LibraryMenuItems.scss similarity index 100% rename from src/components/LibraryMenuItems.scss rename to packages/excalidraw/components/LibraryMenuItems.scss diff --git a/src/components/LibraryMenuItems.tsx b/packages/excalidraw/components/LibraryMenuItems.tsx similarity index 100% rename from src/components/LibraryMenuItems.tsx rename to packages/excalidraw/components/LibraryMenuItems.tsx diff --git a/src/components/LibraryMenuSection.tsx b/packages/excalidraw/components/LibraryMenuSection.tsx similarity index 100% rename from src/components/LibraryMenuSection.tsx rename to packages/excalidraw/components/LibraryMenuSection.tsx diff --git a/src/components/LibraryUnit.scss b/packages/excalidraw/components/LibraryUnit.scss similarity index 98% rename from src/components/LibraryUnit.scss rename to packages/excalidraw/components/LibraryUnit.scss index 8a8ac5d3e..5ebe83f41 100644 --- a/src/components/LibraryUnit.scss +++ b/packages/excalidraw/components/LibraryUnit.scss @@ -1,4 +1,4 @@ -@import "../css/variables.module"; +@import "../css/variables.module.scss"; .excalidraw { .library-unit { diff --git a/src/components/LibraryUnit.tsx b/packages/excalidraw/components/LibraryUnit.tsx similarity index 98% rename from src/components/LibraryUnit.tsx rename to packages/excalidraw/components/LibraryUnit.tsx index 9c9934c21..42fb29149 100644 --- a/src/components/LibraryUnit.tsx +++ b/packages/excalidraw/components/LibraryUnit.tsx @@ -1,6 +1,6 @@ import clsx from "clsx"; import { memo, useEffect, useRef, useState } from "react"; -import { useDevice } from "../components/App"; +import { useDevice } from "./App"; import { LibraryItem } from "../types"; import "./LibraryUnit.scss"; import { CheckboxItem } from "./CheckboxItem"; diff --git a/src/components/LoadingMessage.tsx b/packages/excalidraw/components/LoadingMessage.tsx similarity index 100% rename from src/components/LoadingMessage.tsx rename to packages/excalidraw/components/LoadingMessage.tsx diff --git a/src/components/LockButton.tsx b/packages/excalidraw/components/LockButton.tsx similarity index 100% rename from src/components/LockButton.tsx rename to packages/excalidraw/components/LockButton.tsx diff --git a/src/components/MagicButton.tsx b/packages/excalidraw/components/MagicButton.tsx similarity index 100% rename from src/components/MagicButton.tsx rename to packages/excalidraw/components/MagicButton.tsx diff --git a/src/components/MagicSettings.scss b/packages/excalidraw/components/MagicSettings.scss similarity index 100% rename from src/components/MagicSettings.scss rename to packages/excalidraw/components/MagicSettings.scss diff --git a/src/components/MagicSettings.tsx b/packages/excalidraw/components/MagicSettings.tsx similarity index 100% rename from src/components/MagicSettings.tsx rename to packages/excalidraw/components/MagicSettings.tsx diff --git a/src/components/MobileMenu.tsx b/packages/excalidraw/components/MobileMenu.tsx similarity index 99% rename from src/components/MobileMenu.tsx rename to packages/excalidraw/components/MobileMenu.tsx index 0fc103c98..c8d0ce5c4 100644 --- a/src/components/MobileMenu.tsx +++ b/packages/excalidraw/components/MobileMenu.tsx @@ -185,7 +185,7 @@ export const MobileMenu = ({
diff --git a/src/components/Modal.scss b/packages/excalidraw/components/Modal.scss similarity index 98% rename from src/components/Modal.scss rename to packages/excalidraw/components/Modal.scss index 7dc62c113..a4cf56643 100644 --- a/src/components/Modal.scss +++ b/packages/excalidraw/components/Modal.scss @@ -1,4 +1,4 @@ -@import "../css/variables.module"; +@import "../css/variables.module.scss"; .excalidraw { &.excalidraw-modal-container { diff --git a/src/components/Modal.tsx b/packages/excalidraw/components/Modal.tsx similarity index 98% rename from src/components/Modal.tsx rename to packages/excalidraw/components/Modal.tsx index 3dd737e45..e02583ef0 100644 --- a/src/components/Modal.tsx +++ b/packages/excalidraw/components/Modal.tsx @@ -1,6 +1,5 @@ import "./Modal.scss"; -import React from "react"; import { createPortal } from "react-dom"; import clsx from "clsx"; import { KEYS } from "../keys"; diff --git a/src/components/OverwriteConfirm/OverwriteConfirm.scss b/packages/excalidraw/components/OverwriteConfirm/OverwriteConfirm.scss similarity index 98% rename from src/components/OverwriteConfirm/OverwriteConfirm.scss rename to packages/excalidraw/components/OverwriteConfirm/OverwriteConfirm.scss index 23dd4f411..4aad0cbdf 100644 --- a/src/components/OverwriteConfirm/OverwriteConfirm.scss +++ b/packages/excalidraw/components/OverwriteConfirm/OverwriteConfirm.scss @@ -1,4 +1,4 @@ -@import "../../css/variables.module"; +@import "../../css/variables.module.scss"; .excalidraw { .OverwriteConfirm { diff --git a/src/components/OverwriteConfirm/OverwriteConfirm.tsx b/packages/excalidraw/components/OverwriteConfirm/OverwriteConfirm.tsx similarity index 100% rename from src/components/OverwriteConfirm/OverwriteConfirm.tsx rename to packages/excalidraw/components/OverwriteConfirm/OverwriteConfirm.tsx diff --git a/src/components/OverwriteConfirm/OverwriteConfirmActions.tsx b/packages/excalidraw/components/OverwriteConfirm/OverwriteConfirmActions.tsx similarity index 100% rename from src/components/OverwriteConfirm/OverwriteConfirmActions.tsx rename to packages/excalidraw/components/OverwriteConfirm/OverwriteConfirmActions.tsx diff --git a/src/components/OverwriteConfirm/OverwriteConfirmState.ts b/packages/excalidraw/components/OverwriteConfirm/OverwriteConfirmState.ts similarity index 100% rename from src/components/OverwriteConfirm/OverwriteConfirmState.ts rename to packages/excalidraw/components/OverwriteConfirm/OverwriteConfirmState.ts diff --git a/src/components/Paragraph.tsx b/packages/excalidraw/components/Paragraph.tsx similarity index 100% rename from src/components/Paragraph.tsx rename to packages/excalidraw/components/Paragraph.tsx diff --git a/src/components/PasteChartDialog.scss b/packages/excalidraw/components/PasteChartDialog.scss similarity index 95% rename from src/components/PasteChartDialog.scss rename to packages/excalidraw/components/PasteChartDialog.scss index 2bc49560c..855b4ad7c 100644 --- a/src/components/PasteChartDialog.scss +++ b/packages/excalidraw/components/PasteChartDialog.scss @@ -1,4 +1,4 @@ -@import "../css/variables.module"; +@import "../css/variables.module.scss"; .excalidraw { .PasteChartDialog { diff --git a/src/components/PasteChartDialog.tsx b/packages/excalidraw/components/PasteChartDialog.tsx similarity index 92% rename from src/components/PasteChartDialog.tsx rename to packages/excalidraw/components/PasteChartDialog.tsx index f30ded91a..b01144a8a 100644 --- a/src/components/PasteChartDialog.tsx +++ b/packages/excalidraw/components/PasteChartDialog.tsx @@ -2,7 +2,7 @@ import oc from "open-color"; import React, { useLayoutEffect, useRef, useState } from "react"; import { trackEvent } from "../analytics"; import { ChartElements, renderSpreadsheet, Spreadsheet } from "../charts"; -import { ChartType } from "../element/types"; +import { ChartType, ElementsMap } from "../element/types"; import { t } from "../i18n"; import { exportToSvg } from "../scene/export"; import { UIAppState } from "../types"; @@ -49,10 +49,16 @@ const ChartPreviewBtn = (props: { 0, 0, ); + const elementsMap = new Map() as ElementsMap; + for (const element of elements) { + if (!element.isDeleted) { + elementsMap.set(element.id, element); + } + } elements.forEach( (el) => isTextElement(el) && - redrawTextBoundingBox(el, getContainerElement(el)), + redrawTextBoundingBox(el, getContainerElement(el, elementsMap)), ); setChartElements(elements); }, diff --git a/src/components/PenModeButton.tsx b/packages/excalidraw/components/PenModeButton.tsx similarity index 100% rename from src/components/PenModeButton.tsx rename to packages/excalidraw/components/PenModeButton.tsx diff --git a/src/components/Popover.scss b/packages/excalidraw/components/Popover.scss similarity index 100% rename from src/components/Popover.scss rename to packages/excalidraw/components/Popover.scss diff --git a/src/components/Popover.tsx b/packages/excalidraw/components/Popover.tsx similarity index 100% rename from src/components/Popover.tsx rename to packages/excalidraw/components/Popover.tsx diff --git a/src/components/ProjectName.scss b/packages/excalidraw/components/ProjectName.scss similarity index 100% rename from src/components/ProjectName.scss rename to packages/excalidraw/components/ProjectName.scss diff --git a/src/components/ProjectName.tsx b/packages/excalidraw/components/ProjectName.tsx similarity index 100% rename from src/components/ProjectName.tsx rename to packages/excalidraw/components/ProjectName.tsx diff --git a/src/components/PublishLibrary.scss b/packages/excalidraw/components/PublishLibrary.scss similarity index 98% rename from src/components/PublishLibrary.scss rename to packages/excalidraw/components/PublishLibrary.scss index fd7db0fe4..2c166900e 100644 --- a/src/components/PublishLibrary.scss +++ b/packages/excalidraw/components/PublishLibrary.scss @@ -1,4 +1,4 @@ -@import "../css/variables.module"; +@import "../css/variables.module.scss"; .excalidraw { .publish-library { diff --git a/src/components/PublishLibrary.tsx b/packages/excalidraw/components/PublishLibrary.tsx similarity index 99% rename from src/components/PublishLibrary.tsx rename to packages/excalidraw/components/PublishLibrary.tsx index 6ec501a59..51e14febc 100644 --- a/src/components/PublishLibrary.tsx +++ b/packages/excalidraw/components/PublishLibrary.tsx @@ -6,7 +6,7 @@ import { t } from "../i18n"; import Trans from "./Trans"; import { LibraryItems, LibraryItem, UIAppState } from "../types"; -import { exportToCanvas, exportToSvg } from "../packages/utils"; +import { exportToCanvas, exportToSvg } from "../../utils/export"; import { EDITOR_LS_KEYS, EXPORT_DATA_TYPES, diff --git a/src/components/RadioGroup.scss b/packages/excalidraw/components/RadioGroup.scss similarity index 98% rename from src/components/RadioGroup.scss rename to packages/excalidraw/components/RadioGroup.scss index eb70f4a82..76ee20a16 100644 --- a/src/components/RadioGroup.scss +++ b/packages/excalidraw/components/RadioGroup.scss @@ -1,4 +1,4 @@ -@import "../css/variables.module"; +@import "../css/variables.module.scss"; .excalidraw { --RadioGroup-background: var(--island-bg-color); diff --git a/src/components/RadioGroup.tsx b/packages/excalidraw/components/RadioGroup.tsx similarity index 100% rename from src/components/RadioGroup.tsx rename to packages/excalidraw/components/RadioGroup.tsx diff --git a/src/components/LaserTool/LaserToolOverlay.scss b/packages/excalidraw/components/SVGLayer.scss similarity index 80% rename from src/components/LaserTool/LaserToolOverlay.scss rename to packages/excalidraw/components/SVGLayer.scss index da874b452..5eb0353aa 100644 --- a/src/components/LaserTool/LaserToolOverlay.scss +++ b/packages/excalidraw/components/SVGLayer.scss @@ -1,5 +1,5 @@ .excalidraw { - .LaserToolOverlay { + .SVGLayer { pointer-events: none; width: 100vw; height: 100vh; @@ -9,10 +9,12 @@ z-index: 2; - .LaserToolOverlayCanvas { + & svg { image-rendering: auto; overflow: visible; position: absolute; + width: 100%; + height: 100%; top: 0; left: 0; } diff --git a/packages/excalidraw/components/SVGLayer.tsx b/packages/excalidraw/components/SVGLayer.tsx new file mode 100644 index 000000000..feaebaf94 --- /dev/null +++ b/packages/excalidraw/components/SVGLayer.tsx @@ -0,0 +1,33 @@ +import { useEffect, useRef } from "react"; +import { Trail } from "../animated-trail"; + +import "./SVGLayer.scss"; + +type SVGLayerProps = { + trails: Trail[]; +}; + +export const SVGLayer = ({ trails }: SVGLayerProps) => { + const svgRef = useRef(null); + + useEffect(() => { + if (svgRef.current) { + for (const trail of trails) { + trail.start(svgRef.current); + } + } + + return () => { + for (const trail of trails) { + trail.stop(); + } + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, trails); + + return ( +
+ +
+ ); +}; diff --git a/src/components/Section.tsx b/packages/excalidraw/components/Section.tsx similarity index 100% rename from src/components/Section.tsx rename to packages/excalidraw/components/Section.tsx diff --git a/src/components/ShareableLinkDialog.scss b/packages/excalidraw/components/ShareableLinkDialog.scss similarity index 90% rename from src/components/ShareableLinkDialog.scss rename to packages/excalidraw/components/ShareableLinkDialog.scss index 595acf7dd..2429d50ca 100644 --- a/src/components/ShareableLinkDialog.scss +++ b/packages/excalidraw/components/ShareableLinkDialog.scss @@ -1,4 +1,4 @@ -@import "../css/variables.module"; +@import "../css/variables.module.scss"; .excalidraw { .ShareableLinkDialog { @@ -22,7 +22,7 @@ } &__popover { - @keyframes RoomDialog__popover__scaleIn { + @keyframes ShareableLinkDialog__popover__scaleIn { from { opacity: 0; } @@ -61,7 +61,7 @@ } transform-origin: var(--radix-popover-content-transform-origin); - animation: RoomDialog__popover__scaleIn 150ms ease-out; + animation: ShareableLinkDialog__popover__scaleIn 150ms ease-out; } &__linkRow { diff --git a/src/components/ShareableLinkDialog.tsx b/packages/excalidraw/components/ShareableLinkDialog.tsx similarity index 98% rename from src/components/ShareableLinkDialog.tsx rename to packages/excalidraw/components/ShareableLinkDialog.tsx index 7a53a4a82..cb8ba4cef 100644 --- a/src/components/ShareableLinkDialog.tsx +++ b/packages/excalidraw/components/ShareableLinkDialog.tsx @@ -66,7 +66,7 @@ export const ShareableLinkDialog = ({ diff --git a/src/components/Sidebar/Sidebar.scss b/packages/excalidraw/components/Sidebar/Sidebar.scss similarity index 98% rename from src/components/Sidebar/Sidebar.scss rename to packages/excalidraw/components/Sidebar/Sidebar.scss index abb6f0095..2571d128f 100644 --- a/src/components/Sidebar/Sidebar.scss +++ b/packages/excalidraw/components/Sidebar/Sidebar.scss @@ -1,5 +1,5 @@ @import "open-color/open-color"; -@import "../../css/variables.module"; +@import "../../css/variables.module.scss"; .excalidraw { .sidebar { diff --git a/src/components/Sidebar/Sidebar.test.tsx b/packages/excalidraw/components/Sidebar/Sidebar.test.tsx similarity index 99% rename from src/components/Sidebar/Sidebar.test.tsx rename to packages/excalidraw/components/Sidebar/Sidebar.test.tsx index 6e73689dd..9787f9a73 100644 --- a/src/components/Sidebar/Sidebar.test.tsx +++ b/packages/excalidraw/components/Sidebar/Sidebar.test.tsx @@ -1,6 +1,6 @@ import React from "react"; import { DEFAULT_SIDEBAR } from "../../constants"; -import { Excalidraw, Sidebar } from "../../packages/excalidraw/index"; +import { Excalidraw, Sidebar } from "../../index"; import { fireEvent, GlobalTestState, diff --git a/src/components/Sidebar/Sidebar.tsx b/packages/excalidraw/components/Sidebar/Sidebar.tsx similarity index 99% rename from src/components/Sidebar/Sidebar.tsx rename to packages/excalidraw/components/Sidebar/Sidebar.tsx index 9e417c04d..ae75f570f 100644 --- a/src/components/Sidebar/Sidebar.tsx +++ b/packages/excalidraw/components/Sidebar/Sidebar.tsx @@ -7,7 +7,7 @@ import React, { useImperativeHandle, useCallback, } from "react"; -import { Island } from ".././Island"; +import { Island } from "../Island"; import { atom, useSetAtom } from "jotai"; import { jotaiScope } from "../../jotai"; import { diff --git a/src/components/Sidebar/SidebarHeader.tsx b/packages/excalidraw/components/Sidebar/SidebarHeader.tsx similarity index 100% rename from src/components/Sidebar/SidebarHeader.tsx rename to packages/excalidraw/components/Sidebar/SidebarHeader.tsx diff --git a/src/components/Sidebar/SidebarTab.tsx b/packages/excalidraw/components/Sidebar/SidebarTab.tsx similarity index 100% rename from src/components/Sidebar/SidebarTab.tsx rename to packages/excalidraw/components/Sidebar/SidebarTab.tsx diff --git a/src/components/Sidebar/SidebarTabTrigger.tsx b/packages/excalidraw/components/Sidebar/SidebarTabTrigger.tsx similarity index 100% rename from src/components/Sidebar/SidebarTabTrigger.tsx rename to packages/excalidraw/components/Sidebar/SidebarTabTrigger.tsx diff --git a/src/components/Sidebar/SidebarTabTriggers.tsx b/packages/excalidraw/components/Sidebar/SidebarTabTriggers.tsx similarity index 100% rename from src/components/Sidebar/SidebarTabTriggers.tsx rename to packages/excalidraw/components/Sidebar/SidebarTabTriggers.tsx diff --git a/src/components/Sidebar/SidebarTabs.tsx b/packages/excalidraw/components/Sidebar/SidebarTabs.tsx similarity index 100% rename from src/components/Sidebar/SidebarTabs.tsx rename to packages/excalidraw/components/Sidebar/SidebarTabs.tsx diff --git a/src/components/Sidebar/SidebarTrigger.scss b/packages/excalidraw/components/Sidebar/SidebarTrigger.scss similarity index 81% rename from src/components/Sidebar/SidebarTrigger.scss rename to packages/excalidraw/components/Sidebar/SidebarTrigger.scss index 834df6563..5b003cdc5 100644 --- a/src/components/Sidebar/SidebarTrigger.scss +++ b/packages/excalidraw/components/Sidebar/SidebarTrigger.scss @@ -1,4 +1,4 @@ -@import "../../css/variables.module"; +@import "../../css/variables.module.scss"; .excalidraw { .sidebar-trigger { @@ -21,10 +21,15 @@ width: var(--lg-icon-size); height: var(--lg-icon-size); } + + &__label-element { + align-self: flex-start; + } } .default-sidebar-trigger .sidebar-trigger__label { display: block; + white-space: nowrap; } &.excalidraw--mobile .default-sidebar-trigger .sidebar-trigger__label { diff --git a/src/components/Sidebar/SidebarTrigger.tsx b/packages/excalidraw/components/Sidebar/SidebarTrigger.tsx similarity index 94% rename from src/components/Sidebar/SidebarTrigger.tsx rename to packages/excalidraw/components/Sidebar/SidebarTrigger.tsx index 711432818..889156eba 100644 --- a/src/components/Sidebar/SidebarTrigger.tsx +++ b/packages/excalidraw/components/Sidebar/SidebarTrigger.tsx @@ -19,7 +19,7 @@ export const SidebarTrigger = ({ const appState = useUIAppState(); return ( -
@@ -103,22 +99,21 @@ const MermaidToExcalidraw = ({ input={text} placeholder={"Write Mermaid diagram defintion here..."} onChange={(event) => setText(event.target.value)} + onKeyboardSubmit={() => { + onInsertToEditor(); + }} /> { - insertToEditor({ - app, - data, - text, - shouldSaveMermaidDataToStorage: true, - }); + onInsertToEditor(); }, label: t("mermaid.button"), icon: ArrowRightIcon, }} + renderSubmitShortcut={() => } > (null); +const ttdGenerationAtom = atom<{ + generatedResponse: string | null; + prompt: string | null; +} | null>(null); + type OnTestSubmitRetValue = { rateLimit?: number | null; rateLimitRemaining?: number | null; @@ -80,10 +87,13 @@ export const TTDDialogBase = withInternalFallback( | { __fallback: true } )) => { const app = useApp(); + const setAppState = useExcalidrawSetAppState(); const someRandomDivRef = useRef(null); - const [text, setText] = useState(""); + const [ttdGeneration, setTtdGeneration] = useAtom(ttdGenerationAtom); + + const [text, setText] = useState(ttdGeneration?.prompt ?? ""); const prompt = text.trim(); @@ -91,6 +101,10 @@ export const TTDDialogBase = withInternalFallback( event, ) => { setText(event.target.value); + setTtdGeneration((s) => ({ + generatedResponse: s?.generatedResponse ?? null, + prompt: event.target.value, + })); }; const [onTextSubmitInProgess, setOnTextSubmitInProgess] = useState(false); @@ -131,6 +145,13 @@ export const TTDDialogBase = withInternalFallback( const { generatedResponse, error, rateLimit, rateLimitRemaining } = await rest.onTextSubmit(prompt); + if (typeof generatedResponse === "string") { + setTtdGeneration((s) => ({ + generatedResponse, + prompt: s?.prompt ?? null, + })); + } + if (isFiniteNumber(rateLimit) && isFiniteNumber(rateLimitRemaining)) { setRateLimits({ rateLimit, rateLimitRemaining }); } @@ -153,7 +174,6 @@ export const TTDDialogBase = withInternalFallback( mermaidDefinition: generatedResponse, }); trackEvent("ai", "mermaid parse success", "ttd"); - saveMermaidDataToStorage(generatedResponse); } catch (error: any) { console.info( `%cTTD mermaid render errror: ${error.message}`, @@ -187,9 +207,7 @@ export const TTDDialogBase = withInternalFallback( const [mermaidToExcalidrawLib, setMermaidToExcalidrawLib] = useState({ loaded: false, - api: import( - /* webpackChunkName:"mermaid-to-excalidraw" */ "@excalidraw/mermaid-to-excalidraw" - ), + api: import("@excalidraw/mermaid-to-excalidraw"), }); useEffect(() => { @@ -293,7 +311,32 @@ export const TTDDialogBase = withInternalFallback(
); }} + renderSubmitShortcut={() => } renderBottomRight={() => { + if (typeof ttdGeneration?.generatedResponse === "string") { + return ( +
{ + if ( + typeof ttdGeneration?.generatedResponse === + "string" + ) { + saveMermaidDataToStorage( + ttdGeneration.generatedResponse, + ); + setAppState({ + openDialog: { name: "ttd", tab: "mermaid" }, + }); + } + }} + > + View as Mermaid + +
+ ); + } const ratio = prompt.length / MAX_PROMPT_LENGTH; if (ratio > 0.8) { return ( diff --git a/src/components/TTDDialog/TTDDialogInput.tsx b/packages/excalidraw/components/TTDDialog/TTDDialogInput.tsx similarity index 100% rename from src/components/TTDDialog/TTDDialogInput.tsx rename to packages/excalidraw/components/TTDDialog/TTDDialogInput.tsx diff --git a/src/components/TTDDialog/TTDDialogOutput.tsx b/packages/excalidraw/components/TTDDialog/TTDDialogOutput.tsx similarity index 100% rename from src/components/TTDDialog/TTDDialogOutput.tsx rename to packages/excalidraw/components/TTDDialog/TTDDialogOutput.tsx diff --git a/src/components/TTDDialog/TTDDialogPanel.tsx b/packages/excalidraw/components/TTDDialog/TTDDialogPanel.tsx similarity index 89% rename from src/components/TTDDialog/TTDDialogPanel.tsx rename to packages/excalidraw/components/TTDDialog/TTDDialogPanel.tsx index b74b74890..5c7fba6da 100644 --- a/src/components/TTDDialog/TTDDialogPanel.tsx +++ b/packages/excalidraw/components/TTDDialog/TTDDialogPanel.tsx @@ -14,6 +14,7 @@ interface TTDDialogPanelProps { panelActionDisabled?: boolean; onTextSubmitInProgess?: boolean; renderTopRight?: () => ReactNode; + renderSubmitShortcut?: () => ReactNode; renderBottomRight?: () => ReactNode; } @@ -24,6 +25,7 @@ export const TTDDialogPanel = ({ panelActionDisabled = false, onTextSubmitInProgess, renderTopRight, + renderSubmitShortcut, renderBottomRight, }: TTDDialogPanelProps) => { return ( @@ -51,6 +53,9 @@ export const TTDDialogPanel = ({
{onTextSubmitInProgess && } + {!panelActionDisabled && + !onTextSubmitInProgess && + renderSubmitShortcut?.()} {renderBottomRight?.()}
diff --git a/src/components/TTDDialog/TTDDialogPanels.tsx b/packages/excalidraw/components/TTDDialog/TTDDialogPanels.tsx similarity index 100% rename from src/components/TTDDialog/TTDDialogPanels.tsx rename to packages/excalidraw/components/TTDDialog/TTDDialogPanels.tsx diff --git a/packages/excalidraw/components/TTDDialog/TTDDialogSubmitShortcut.tsx b/packages/excalidraw/components/TTDDialog/TTDDialogSubmitShortcut.tsx new file mode 100644 index 000000000..a8831e3a0 --- /dev/null +++ b/packages/excalidraw/components/TTDDialog/TTDDialogSubmitShortcut.tsx @@ -0,0 +1,14 @@ +import { getShortcutKey } from "../../utils"; + +export const TTDDialogSubmitShortcut = () => { + return ( +
+
+ {getShortcutKey("CtrlOrCmd")} +
+
+ {getShortcutKey("Enter")} +
+
+ ); +}; diff --git a/src/components/TTDDialog/TTDDialogTab.tsx b/packages/excalidraw/components/TTDDialog/TTDDialogTab.tsx similarity index 100% rename from src/components/TTDDialog/TTDDialogTab.tsx rename to packages/excalidraw/components/TTDDialog/TTDDialogTab.tsx diff --git a/src/components/TTDDialog/TTDDialogTabTrigger.tsx b/packages/excalidraw/components/TTDDialog/TTDDialogTabTrigger.tsx similarity index 100% rename from src/components/TTDDialog/TTDDialogTabTrigger.tsx rename to packages/excalidraw/components/TTDDialog/TTDDialogTabTrigger.tsx diff --git a/src/components/TTDDialog/TTDDialogTabTriggers.tsx b/packages/excalidraw/components/TTDDialog/TTDDialogTabTriggers.tsx similarity index 100% rename from src/components/TTDDialog/TTDDialogTabTriggers.tsx rename to packages/excalidraw/components/TTDDialog/TTDDialogTabTriggers.tsx diff --git a/src/components/TTDDialog/TTDDialogTabs.tsx b/packages/excalidraw/components/TTDDialog/TTDDialogTabs.tsx similarity index 100% rename from src/components/TTDDialog/TTDDialogTabs.tsx rename to packages/excalidraw/components/TTDDialog/TTDDialogTabs.tsx diff --git a/src/components/TTDDialog/TTDDialogTrigger.tsx b/packages/excalidraw/components/TTDDialog/TTDDialogTrigger.tsx similarity index 100% rename from src/components/TTDDialog/TTDDialogTrigger.tsx rename to packages/excalidraw/components/TTDDialog/TTDDialogTrigger.tsx diff --git a/src/components/TTDDialog/common.ts b/packages/excalidraw/components/TTDDialog/common.ts similarity index 87% rename from src/components/TTDDialog/common.ts rename to packages/excalidraw/components/TTDDialog/common.ts index 9d90a0432..636d160a8 100644 --- a/src/components/TTDDialog/common.ts +++ b/packages/excalidraw/components/TTDDialog/common.ts @@ -1,13 +1,15 @@ import { MermaidOptions } from "@excalidraw/mermaid-to-excalidraw"; import { MermaidToExcalidrawResult } from "@excalidraw/mermaid-to-excalidraw/dist/interfaces"; -import { DEFAULT_EXPORT_PADDING, DEFAULT_FONT_SIZE } from "../../constants"; import { - convertToExcalidrawElements, - exportToCanvas, -} from "../../packages/excalidraw/index"; + DEFAULT_EXPORT_PADDING, + DEFAULT_FONT_SIZE, + EDITOR_LS_KEYS, +} from "../../constants"; +import { convertToExcalidrawElements, exportToCanvas } from "../../index"; import { NonDeletedExcalidrawElement } from "../../element/types"; import { AppClassProperties, BinaryFiles } from "../../types"; import { canvasToBlob } from "../../data/blob"; +import { EditorLocalStorage } from "../../data/EditorLocalStorage"; const resetPreview = ({ canvasRef, @@ -110,7 +112,6 @@ export const convertMermaidToExcalidraw = async ({ parent.style.background = "var(--default-bg-color)"; canvasNode.replaceChildren(canvas); } catch (err: any) { - console.error(err); parent.style.background = "var(--default-bg-color)"; if (mermaidDefinition) { setError(err); @@ -120,14 +121,11 @@ export const convertMermaidToExcalidraw = async ({ } }; -export const LOCAL_STORAGE_KEY_MERMAID_TO_EXCALIDRAW = "mermaid-to-excalidraw"; -export const saveMermaidDataToStorage = (data: string) => { - try { - localStorage.setItem(LOCAL_STORAGE_KEY_MERMAID_TO_EXCALIDRAW, data); - } catch (error: any) { - // Unable to access window.localStorage - console.error(error); - } +export const saveMermaidDataToStorage = (mermaidDefinition: string) => { + EditorLocalStorage.set( + EDITOR_LS_KEYS.MERMAID_TO_EXCALIDRAW, + mermaidDefinition, + ); }; export const insertToEditor = ({ diff --git a/src/components/TextField.scss b/packages/excalidraw/components/TextField.scss similarity index 98% rename from src/components/TextField.scss rename to packages/excalidraw/components/TextField.scss index 74518d218..952c97592 100644 --- a/src/components/TextField.scss +++ b/packages/excalidraw/components/TextField.scss @@ -1,4 +1,4 @@ -@import "../css/variables.module"; +@import "../css/variables.module.scss"; .excalidraw { --ExcTextField--color: var(--color-on-surface); diff --git a/src/components/TextField.tsx b/packages/excalidraw/components/TextField.tsx similarity index 84% rename from src/components/TextField.tsx rename to packages/excalidraw/components/TextField.tsx index 10b3d9b53..44a7c25ff 100644 --- a/src/components/TextField.tsx +++ b/packages/excalidraw/components/TextField.tsx @@ -13,8 +13,6 @@ import { Button } from "./Button"; import { eyeIcon, eyeClosedIcon } from "./icons"; type TextFieldProps = { - value?: string; - onChange?: (value: string) => void; onClick?: () => void; onKeyDown?: (event: KeyboardEvent) => void; @@ -26,12 +24,11 @@ type TextFieldProps = { label?: string; placeholder?: string; isRedacted?: boolean; -}; +} & ({ value: string } | { defaultValue: string }); export const TextField = forwardRef( ( { - value, onChange, label, fullWidth, @@ -40,6 +37,7 @@ export const TextField = forwardRef( selectOnRender, onKeyDown, isRedacted = false, + ...rest }, ref, ) => { @@ -73,10 +71,17 @@ export const TextField = forwardRef( > onChange?.(event.target.value)} diff --git a/src/components/TextInput.scss b/packages/excalidraw/components/TextInput.scss similarity index 61% rename from src/components/TextInput.scss rename to packages/excalidraw/components/TextInput.scss index 511934aa3..60a03ca78 100644 --- a/src/components/TextInput.scss +++ b/packages/excalidraw/components/TextInput.scss @@ -1,4 +1,4 @@ -@import "../css/variables.module"; +@import "../css/variables.module.scss"; .excalidraw { .TextInput { diff --git a/src/components/Toast.scss b/packages/excalidraw/components/Toast.scss similarity index 95% rename from src/components/Toast.scss rename to packages/excalidraw/components/Toast.scss index d236edd1f..853871fd4 100644 --- a/src/components/Toast.scss +++ b/packages/excalidraw/components/Toast.scss @@ -1,4 +1,4 @@ -@import "../css/variables.module"; +@import "../css/variables.module.scss"; .excalidraw { .Toast { diff --git a/src/components/Toast.tsx b/packages/excalidraw/components/Toast.tsx similarity index 100% rename from src/components/Toast.tsx rename to packages/excalidraw/components/Toast.tsx diff --git a/src/components/ToolButton.tsx b/packages/excalidraw/components/ToolButton.tsx similarity index 98% rename from src/components/ToolButton.tsx rename to packages/excalidraw/components/ToolButton.tsx index ffe9a382c..2dace89d7 100644 --- a/src/components/ToolButton.tsx +++ b/packages/excalidraw/components/ToolButton.tsx @@ -6,6 +6,7 @@ import { useExcalidrawContainer } from "./App"; import { AbortError } from "../errors"; import Spinner from "./Spinner"; import { PointerType } from "../element/types"; +import { isPromiseLike } from "../utils"; export type ToolButtonSize = "small" | "medium"; @@ -65,7 +66,7 @@ export const ToolButton = React.forwardRef((props: ToolButtonProps, ref) => { const onClick = async (event: React.MouseEvent) => { const ret = "onClick" in props && props.onClick?.(event); - if (ret && "then" in ret) { + if (isPromiseLike(ret)) { try { setIsLoading(true); await ret; diff --git a/src/components/ToolIcon.scss b/packages/excalidraw/components/ToolIcon.scss similarity index 98% rename from src/components/ToolIcon.scss rename to packages/excalidraw/components/ToolIcon.scss index 8e857dd54..59e45455c 100644 --- a/src/components/ToolIcon.scss +++ b/packages/excalidraw/components/ToolIcon.scss @@ -1,5 +1,5 @@ @import "open-color/open-color.scss"; -@import "../css/variables.module"; +@import "../css/variables.module.scss"; .excalidraw { .ToolIcon { diff --git a/src/components/Toolbar.scss b/packages/excalidraw/components/Toolbar.scss similarity index 95% rename from src/components/Toolbar.scss rename to packages/excalidraw/components/Toolbar.scss index 33f106ac2..1565120ac 100644 --- a/src/components/Toolbar.scss +++ b/packages/excalidraw/components/Toolbar.scss @@ -1,5 +1,5 @@ @import "open-color/open-color.scss"; -@import "../css/variables.module"; +@import "../css/variables.module.scss"; .excalidraw { .App-toolbar { diff --git a/src/components/Tooltip.scss b/packages/excalidraw/components/Tooltip.scss similarity index 95% rename from src/components/Tooltip.scss rename to packages/excalidraw/components/Tooltip.scss index 07350b7e9..37fc3368e 100644 --- a/src/components/Tooltip.scss +++ b/packages/excalidraw/components/Tooltip.scss @@ -1,4 +1,4 @@ -@import "../css/variables.module"; +@import "../css/variables.module.scss"; // container in body where the actual tooltip is appended to .excalidraw-tooltip { diff --git a/src/components/Tooltip.tsx b/packages/excalidraw/components/Tooltip.tsx similarity index 97% rename from src/components/Tooltip.tsx rename to packages/excalidraw/components/Tooltip.tsx index 220de2831..38c04ef23 100644 --- a/src/components/Tooltip.tsx +++ b/packages/excalidraw/components/Tooltip.tsx @@ -80,6 +80,7 @@ type TooltipProps = { label: string; long?: boolean; style?: React.CSSProperties; + disabled?: boolean; }; export const Tooltip = ({ @@ -87,11 +88,15 @@ export const Tooltip = ({ label, long = false, style, + disabled, }: TooltipProps) => { useEffect(() => { return () => getTooltipDiv().classList.remove("excalidraw-tooltip--visible"); }, []); + if (disabled) { + return null; + } return (
* { + pointer-events: var(--ui-pointerEvents); + } + + .UserList_mobile { + padding: 0; + justify-content: normal; + margin: 0.5rem 0; + max-width: none; + max-height: none; + } + + .UserList__more { + @include avatarStyles; + background-color: var(--color-gray-20); + border: 0 !important; + font-size: 0.5rem; + font-weight: 400; + flex-shrink: 0; + color: var(--color-gray-100); + } + + .UserList__collaborator-name { + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + } + + .UserList__collaborator-follow-status-icon { + margin-left: auto; + flex: 0 0 auto; + width: 1rem; + display: flex; + } + + --userlist-hint-bg-color: var(--color-gray-10); + --userlist-hint-heading-color: var(--color-gray-80); + --userlist-hint-text-color: var(--color-gray-60); + --userlist-collaborators-border-color: var(--color-gray-20); + + &.theme--dark { + --userlist-hint-bg-color: var(--color-gray-90); + --userlist-hint-heading-color: var(--color-gray-30); + --userlist-hint-text-color: var(--color-gray-40); + --userlist-collaborators-border-color: var(--color-gray-80); + } + + .UserList__collaborators { + position: static; + top: auto; + margin-top: 0; + max-height: 12rem; + overflow-y: auto; + padding: 0.25rem 0.5rem; + border-top: 1px solid var(--userlist-collaborators-border-color); + border-bottom: 1px solid var(--userlist-collaborators-border-color); + + &__empty { + color: var(--color-gray-60); + font-size: 0.75rem; + line-height: 150%; + padding: 0.5rem 0; + } + } + + .UserList__hint { + padding: 0.5rem 0.75rem; + overflow: hidden; + text-align: center; + color: var(--userlist-hint-text-color); + font-size: 0.75rem; + line-height: 150%; + } + + .UserList__search-wrapper { + position: relative; + height: 2.5rem; + + svg { + position: absolute; + top: 50%; + transform: translateY(-50%); + left: 0.75rem; + width: 1.25rem; + height: 1.25rem; + color: var(--color-gray-40); + z-index: 1; + } + } + + .UserList__search { + position: absolute; + top: 0; + left: 0; + width: 100%; + box-sizing: border-box; + border: 0 !important; + border-radius: 0 !important; + font-size: 0.875rem; + padding-left: 2.5rem !important; + padding-right: 0.75rem !important; + + &::placeholder { + color: var(--color-gray-40); + } + + &:focus { + box-shadow: none !important; + } + } +} diff --git a/packages/excalidraw/components/UserList.tsx b/packages/excalidraw/components/UserList.tsx new file mode 100644 index 000000000..ba01b52dc --- /dev/null +++ b/packages/excalidraw/components/UserList.tsx @@ -0,0 +1,254 @@ +import "./UserList.scss"; + +import React from "react"; +import clsx from "clsx"; +import { Collaborator, SocketId } from "../types"; +import { Tooltip } from "./Tooltip"; +import { useExcalidrawActionManager } from "./App"; +import { ActionManager } from "../actions/manager"; + +import * as Popover from "@radix-ui/react-popover"; +import { Island } from "./Island"; +import { searchIcon } from "./icons"; +import { t } from "../i18n"; +import { isShallowEqual } from "../utils"; + +export type GoToCollaboratorComponentProps = { + clientId: ClientId; + collaborator: Collaborator; + withName: boolean; + isBeingFollowed: boolean; +}; + +/** collaborator user id or socket id (fallback) */ +type ClientId = string & { _brand: "UserId" }; + +const FIRST_N_AVATARS = 3; +const SHOW_COLLABORATORS_FILTER_AT = 8; + +const ConditionalTooltipWrapper = ({ + shouldWrap, + children, + clientId, + username, +}: { + shouldWrap: boolean; + children: React.ReactNode; + username?: string | null; + clientId: ClientId; +}) => + shouldWrap ? ( + + {children} + + ) : ( + {children} + ); + +const renderCollaborator = ({ + actionManager, + collaborator, + clientId, + withName = false, + shouldWrapWithTooltip = false, + isBeingFollowed, +}: { + actionManager: ActionManager; + collaborator: Collaborator; + clientId: ClientId; + withName?: boolean; + shouldWrapWithTooltip?: boolean; + isBeingFollowed: boolean; +}) => { + const data: GoToCollaboratorComponentProps = { + clientId, + collaborator, + withName, + isBeingFollowed, + }; + const avatarJSX = actionManager.renderAction("goToCollaborator", data); + + return ( + + {avatarJSX} + + ); +}; + +type UserListUserObject = Pick< + Collaborator, + "avatarUrl" | "id" | "socketId" | "username" +>; + +type UserListProps = { + className?: string; + mobile?: boolean; + collaborators: Map; + userToFollow: SocketId | null; +}; + +const collaboratorComparatorKeys = [ + "avatarUrl", + "id", + "socketId", + "username", +] as const; + +export const UserList = React.memo( + ({ className, mobile, collaborators, userToFollow }: UserListProps) => { + const actionManager = useExcalidrawActionManager(); + + const uniqueCollaboratorsMap = new Map(); + + collaborators.forEach((collaborator, socketId) => { + const userId = (collaborator.id || socketId) as ClientId; + uniqueCollaboratorsMap.set( + // filter on user id, else fall back on unique socketId + userId, + { ...collaborator, socketId }, + ); + }); + + const uniqueCollaboratorsArray = Array.from(uniqueCollaboratorsMap).filter( + ([_, collaborator]) => collaborator.username?.trim(), + ); + + const [searchTerm, setSearchTerm] = React.useState(""); + + if (uniqueCollaboratorsArray.length === 0) { + return null; + } + + const searchTermNormalized = searchTerm.trim().toLowerCase(); + + const filteredCollaborators = searchTermNormalized + ? uniqueCollaboratorsArray.filter(([, collaborator]) => + collaborator.username?.toLowerCase().includes(searchTerm), + ) + : uniqueCollaboratorsArray; + + const firstNCollaborators = uniqueCollaboratorsArray.slice( + 0, + FIRST_N_AVATARS, + ); + + const firstNAvatarsJSX = firstNCollaborators.map( + ([clientId, collaborator]) => + renderCollaborator({ + actionManager, + collaborator, + clientId, + shouldWrapWithTooltip: true, + isBeingFollowed: collaborator.socketId === userToFollow, + }), + ); + + return mobile ? ( +
+ {uniqueCollaboratorsArray.map(([clientId, collaborator]) => + renderCollaborator({ + actionManager, + collaborator, + clientId, + shouldWrapWithTooltip: true, + isBeingFollowed: collaborator.socketId === userToFollow, + }), + )} +
+ ) : ( +
+ {firstNAvatarsJSX} + + {uniqueCollaboratorsArray.length > FIRST_N_AVATARS && ( + { + if (!isOpen) { + setSearchTerm(""); + } + }} + > + + +{uniqueCollaboratorsArray.length - FIRST_N_AVATARS} + + + + {uniqueCollaboratorsArray.length >= + SHOW_COLLABORATORS_FILTER_AT && ( +
+ {searchIcon} + { + setSearchTerm(e.target.value); + }} + /> +
+ )} +
+ {filteredCollaborators.length === 0 && ( +
+ {t("userList.search.empty")} +
+ )} +
+ {t("userList.hint.text")} +
+ {filteredCollaborators.map(([clientId, collaborator]) => + renderCollaborator({ + actionManager, + collaborator, + clientId, + withName: true, + isBeingFollowed: collaborator.socketId === userToFollow, + }), + )} +
+
+
+
+ )} +
+ ); + }, + (prev, next) => { + if ( + prev.collaborators.size !== next.collaborators.size || + prev.mobile !== next.mobile || + prev.className !== next.className || + prev.userToFollow !== next.userToFollow + ) { + return false; + } + + for (const [socketId, collaborator] of prev.collaborators) { + const nextCollaborator = next.collaborators.get(socketId); + if ( + !nextCollaborator || + !isShallowEqual( + collaborator, + nextCollaborator, + collaboratorComparatorKeys, + ) + ) { + return false; + } + } + return true; + }, +); diff --git a/src/components/__snapshots__/App.test.tsx.snap b/packages/excalidraw/components/__snapshots__/App.test.tsx.snap similarity index 100% rename from src/components/__snapshots__/App.test.tsx.snap rename to packages/excalidraw/components/__snapshots__/App.test.tsx.snap diff --git a/src/components/canvases/InteractiveCanvas.tsx b/packages/excalidraw/components/canvases/InteractiveCanvas.tsx similarity index 95% rename from src/components/canvases/InteractiveCanvas.tsx rename to packages/excalidraw/components/canvases/InteractiveCanvas.tsx index 5a524921a..0782b92b9 100644 --- a/src/components/canvases/InteractiveCanvas.tsx +++ b/packages/excalidraw/components/canvases/InteractiveCanvas.tsx @@ -1,24 +1,22 @@ import React, { useEffect, useRef } from "react"; import { renderInteractiveScene } from "../../renderer/renderScene"; -import { - isRenderThrottlingEnabled, - isShallowEqual, - sceneCoordsToViewportCoords, -} from "../../utils"; +import { isShallowEqual, sceneCoordsToViewportCoords } from "../../utils"; import { CURSOR_TYPE } from "../../constants"; import { t } from "../../i18n"; import type { DOMAttributes } from "react"; import type { AppState, InteractiveCanvasAppState } from "../../types"; import type { InteractiveCanvasRenderConfig, + RenderableElementsMap, RenderInteractiveSceneCallback, } from "../../scene/types"; import type { NonDeletedExcalidrawElement } from "../../element/types"; +import { isRenderThrottlingEnabled } from "../../reactUtils"; type InteractiveCanvasProps = { containerRef: React.RefObject; canvas: HTMLCanvasElement | null; - elements: readonly NonDeletedExcalidrawElement[]; + elementsMap: RenderableElementsMap; visibleElements: readonly NonDeletedExcalidrawElement[]; selectedElements: readonly NonDeletedExcalidrawElement[]; versionNonce: number | undefined; @@ -116,7 +114,7 @@ const InteractiveCanvas = (props: InteractiveCanvasProps) => { renderInteractiveScene( { canvas: props.canvas, - elements: props.elements, + elementsMap: props.elementsMap, visibleElements: props.visibleElements, selectedElements: props.selectedElements, scale: window.devicePixelRatio, @@ -204,10 +202,10 @@ const areEqual = ( prevProps.selectionNonce !== nextProps.selectionNonce || prevProps.versionNonce !== nextProps.versionNonce || prevProps.scale !== nextProps.scale || - // we need to memoize on element arrays because they may have renewed + // we need to memoize on elementsMap because they may have renewed // even if versionNonce didn't change (e.g. we filter elements out based // on appState) - prevProps.elements !== nextProps.elements || + prevProps.elementsMap !== nextProps.elementsMap || prevProps.visibleElements !== nextProps.visibleElements || prevProps.selectedElements !== nextProps.selectedElements ) { diff --git a/src/components/canvases/StaticCanvas.tsx b/packages/excalidraw/components/canvases/StaticCanvas.tsx similarity index 86% rename from src/components/canvases/StaticCanvas.tsx rename to packages/excalidraw/components/canvases/StaticCanvas.tsx index 38b9baade..bfdb669e6 100644 --- a/src/components/canvases/StaticCanvas.tsx +++ b/packages/excalidraw/components/canvases/StaticCanvas.tsx @@ -1,15 +1,23 @@ import React, { useEffect, useRef } from "react"; import { RoughCanvas } from "roughjs/bin/canvas"; import { renderStaticScene } from "../../renderer/renderScene"; -import { isRenderThrottlingEnabled, isShallowEqual } from "../../utils"; +import { isShallowEqual } from "../../utils"; import type { AppState, StaticCanvasAppState } from "../../types"; -import type { StaticCanvasRenderConfig } from "../../scene/types"; -import type { NonDeletedExcalidrawElement } from "../../element/types"; +import type { + RenderableElementsMap, + StaticCanvasRenderConfig, +} from "../../scene/types"; +import type { + NonDeletedExcalidrawElement, + NonDeletedSceneElementsMap, +} from "../../element/types"; +import { isRenderThrottlingEnabled } from "../../reactUtils"; type StaticCanvasProps = { canvas: HTMLCanvasElement; rc: RoughCanvas; - elements: readonly NonDeletedExcalidrawElement[]; + elementsMap: RenderableElementsMap; + allElementsMap: NonDeletedSceneElementsMap; visibleElements: readonly NonDeletedExcalidrawElement[]; versionNonce: number | undefined; selectionNonce: number | undefined; @@ -62,7 +70,8 @@ const StaticCanvas = (props: StaticCanvasProps) => { canvas, rc: props.rc, scale: props.scale, - elements: props.elements, + elementsMap: props.elementsMap, + allElementsMap: props.allElementsMap, visibleElements: props.visibleElements, appState: props.appState, renderConfig: props.renderConfig, @@ -105,10 +114,10 @@ const areEqual = ( if ( prevProps.versionNonce !== nextProps.versionNonce || prevProps.scale !== nextProps.scale || - // we need to memoize on element arrays because they may have renewed + // we need to memoize on elementsMap because they may have renewed // even if versionNonce didn't change (e.g. we filter elements out based // on appState) - prevProps.elements !== nextProps.elements || + prevProps.elementsMap !== nextProps.elementsMap || prevProps.visibleElements !== nextProps.visibleElements ) { return false; diff --git a/src/components/canvases/index.tsx b/packages/excalidraw/components/canvases/index.tsx similarity index 100% rename from src/components/canvases/index.tsx rename to packages/excalidraw/components/canvases/index.tsx diff --git a/src/components/dropdownMenu/DropdownMenu.scss b/packages/excalidraw/components/dropdownMenu/DropdownMenu.scss similarity index 98% rename from src/components/dropdownMenu/DropdownMenu.scss rename to packages/excalidraw/components/dropdownMenu/DropdownMenu.scss index 5af7b2ecd..dbeab41e1 100644 --- a/src/components/dropdownMenu/DropdownMenu.scss +++ b/packages/excalidraw/components/dropdownMenu/DropdownMenu.scss @@ -1,4 +1,4 @@ -@import "../../css/variables.module"; +@import "../../css/variables.module.scss"; .excalidraw { .dropdown-menu { diff --git a/src/components/dropdownMenu/DropdownMenu.tsx b/packages/excalidraw/components/dropdownMenu/DropdownMenu.tsx similarity index 100% rename from src/components/dropdownMenu/DropdownMenu.tsx rename to packages/excalidraw/components/dropdownMenu/DropdownMenu.tsx diff --git a/src/components/dropdownMenu/DropdownMenuContent.tsx b/packages/excalidraw/components/dropdownMenu/DropdownMenuContent.tsx similarity index 100% rename from src/components/dropdownMenu/DropdownMenuContent.tsx rename to packages/excalidraw/components/dropdownMenu/DropdownMenuContent.tsx diff --git a/src/components/dropdownMenu/DropdownMenuGroup.tsx b/packages/excalidraw/components/dropdownMenu/DropdownMenuGroup.tsx similarity index 100% rename from src/components/dropdownMenu/DropdownMenuGroup.tsx rename to packages/excalidraw/components/dropdownMenu/DropdownMenuGroup.tsx diff --git a/src/components/dropdownMenu/DropdownMenuItem.tsx b/packages/excalidraw/components/dropdownMenu/DropdownMenuItem.tsx similarity index 93% rename from src/components/dropdownMenu/DropdownMenuItem.tsx rename to packages/excalidraw/components/dropdownMenu/DropdownMenuItem.tsx index 1d92e1f18..c3a165eab 100644 --- a/src/components/dropdownMenu/DropdownMenuItem.tsx +++ b/packages/excalidraw/components/dropdownMenu/DropdownMenuItem.tsx @@ -49,12 +49,12 @@ export const DropDownMenuItemBadge = ({ style={{ display: "inline-flex", marginLeft: "auto", - padding: "1px 4px", + padding: "2px 4px", background: "pink", borderRadius: 6, - fontSize: 11, + fontSize: 9, color: "black", - fontFamily: "monospace", + fontFamily: "Cascadia, monospace", }} > {children} diff --git a/src/components/dropdownMenu/DropdownMenuItemContent.tsx b/packages/excalidraw/components/dropdownMenu/DropdownMenuItemContent.tsx similarity index 100% rename from src/components/dropdownMenu/DropdownMenuItemContent.tsx rename to packages/excalidraw/components/dropdownMenu/DropdownMenuItemContent.tsx diff --git a/src/components/dropdownMenu/DropdownMenuItemCustom.tsx b/packages/excalidraw/components/dropdownMenu/DropdownMenuItemCustom.tsx similarity index 100% rename from src/components/dropdownMenu/DropdownMenuItemCustom.tsx rename to packages/excalidraw/components/dropdownMenu/DropdownMenuItemCustom.tsx diff --git a/src/components/dropdownMenu/DropdownMenuItemLink.tsx b/packages/excalidraw/components/dropdownMenu/DropdownMenuItemLink.tsx similarity index 100% rename from src/components/dropdownMenu/DropdownMenuItemLink.tsx rename to packages/excalidraw/components/dropdownMenu/DropdownMenuItemLink.tsx diff --git a/src/components/dropdownMenu/DropdownMenuSeparator.tsx b/packages/excalidraw/components/dropdownMenu/DropdownMenuSeparator.tsx similarity index 100% rename from src/components/dropdownMenu/DropdownMenuSeparator.tsx rename to packages/excalidraw/components/dropdownMenu/DropdownMenuSeparator.tsx diff --git a/src/components/dropdownMenu/DropdownMenuTrigger.tsx b/packages/excalidraw/components/dropdownMenu/DropdownMenuTrigger.tsx similarity index 100% rename from src/components/dropdownMenu/DropdownMenuTrigger.tsx rename to packages/excalidraw/components/dropdownMenu/DropdownMenuTrigger.tsx diff --git a/src/components/dropdownMenu/common.ts b/packages/excalidraw/components/dropdownMenu/common.ts similarity index 100% rename from src/components/dropdownMenu/common.ts rename to packages/excalidraw/components/dropdownMenu/common.ts diff --git a/src/components/dropdownMenu/dropdownMenuUtils.ts b/packages/excalidraw/components/dropdownMenu/dropdownMenuUtils.ts similarity index 100% rename from src/components/dropdownMenu/dropdownMenuUtils.ts rename to packages/excalidraw/components/dropdownMenu/dropdownMenuUtils.ts diff --git a/src/components/footer/Footer.tsx b/packages/excalidraw/components/footer/Footer.tsx similarity index 100% rename from src/components/footer/Footer.tsx rename to packages/excalidraw/components/footer/Footer.tsx diff --git a/src/components/footer/FooterCenter.scss b/packages/excalidraw/components/footer/FooterCenter.scss similarity index 100% rename from src/components/footer/FooterCenter.scss rename to packages/excalidraw/components/footer/FooterCenter.scss diff --git a/src/components/footer/FooterCenter.tsx b/packages/excalidraw/components/footer/FooterCenter.tsx similarity index 100% rename from src/components/footer/FooterCenter.tsx rename to packages/excalidraw/components/footer/FooterCenter.tsx diff --git a/src/components/hoc/withInternalFallback.test.tsx b/packages/excalidraw/components/hoc/withInternalFallback.test.tsx similarity index 97% rename from src/components/hoc/withInternalFallback.test.tsx rename to packages/excalidraw/components/hoc/withInternalFallback.test.tsx index 363923b44..68cc56d34 100644 --- a/src/components/hoc/withInternalFallback.test.tsx +++ b/packages/excalidraw/components/hoc/withInternalFallback.test.tsx @@ -1,5 +1,5 @@ import { render, queryAllByTestId } from "../../tests/test-utils"; -import { Excalidraw, MainMenu } from "../../packages/excalidraw/index"; +import { Excalidraw, MainMenu } from "../../index"; describe("Test internal component fallback rendering", () => { it("should render only one menu per excalidraw instance (custom menu first scenario)", async () => { diff --git a/src/components/hoc/withInternalFallback.tsx b/packages/excalidraw/components/hoc/withInternalFallback.tsx similarity index 100% rename from src/components/hoc/withInternalFallback.tsx rename to packages/excalidraw/components/hoc/withInternalFallback.tsx diff --git a/src/components/icons.tsx b/packages/excalidraw/components/icons.tsx similarity index 96% rename from src/components/icons.tsx rename to packages/excalidraw/components/icons.tsx index 3449ccda0..fcf8df4a6 100644 --- a/src/components/icons.tsx +++ b/packages/excalidraw/components/icons.tsx @@ -486,10 +486,11 @@ export const DiscordIcon = createIcon( modifiedTablerIconProps, ); -export const TwitterIcon = createIcon( +export const XBrandIcon = createIcon( - - + + + , tablerIconProps, ); @@ -1281,7 +1282,7 @@ export const ArrowheadArrowIcon = React.memo( ), ); -export const ArrowheadDotIcon = React.memo( +export const ArrowheadCircleIcon = React.memo( ({ flip = false }: { flip?: boolean }) => createIcon( + createIcon( + + + + , + { width: 40, height: 20 }, + ), +); + export const ArrowheadBarIcon = React.memo( ({ flip = false }: { flip?: boolean }) => createIcon( @@ -1326,6 +1343,58 @@ export const ArrowheadTriangleIcon = React.memo( ), ); +export const ArrowheadTriangleOutlineIcon = React.memo( + ({ flip = false }: { flip?: boolean }) => + createIcon( + + + + , + + { width: 40, height: 20 }, + ), +); + +export const ArrowheadDiamondIcon = React.memo( + ({ flip = false }: { flip?: boolean }) => + createIcon( + + + + , + { width: 40, height: 20 }, + ), +); + +export const ArrowheadDiamondOutlineIcon = React.memo( + ({ flip = false }: { flip?: boolean }) => + createIcon( + + + + , + { width: 40, height: 20 }, + ), +); + export const FontSizeSmallIcon = createIcon( <> @@ -1755,3 +1824,12 @@ export const brainIcon = createIcon( , tablerIconProps, ); + +export const searchIcon = createIcon( + + + + + , + tablerIconProps, +); diff --git a/src/components/live-collaboration/LiveCollaborationTrigger.scss b/packages/excalidraw/components/live-collaboration/LiveCollaborationTrigger.scss similarity index 89% rename from src/components/live-collaboration/LiveCollaborationTrigger.scss rename to packages/excalidraw/components/live-collaboration/LiveCollaborationTrigger.scss index 763193341..573fbccce 100644 --- a/src/components/live-collaboration/LiveCollaborationTrigger.scss +++ b/packages/excalidraw/components/live-collaboration/LiveCollaborationTrigger.scss @@ -1,9 +1,9 @@ -@import "../../css/variables.module"; +@import "../../css/variables.module.scss"; .excalidraw { .collab-button { --button-bg: var(--color-primary); - --button-color: white; + --button-color: var(--color-surface-lowest); --button-border: var(--color-primary); --button-width: var(--lg-button-size); @@ -35,12 +35,6 @@ } } - &.theme--dark { - .collab-button { - color: var(--color-gray-90); - } - } - .CollabButton.is-collaborating { background-color: var(--button-special-active-bg-color); diff --git a/src/components/live-collaboration/LiveCollaborationTrigger.tsx b/packages/excalidraw/components/live-collaboration/LiveCollaborationTrigger.tsx similarity index 81% rename from src/components/live-collaboration/LiveCollaborationTrigger.tsx rename to packages/excalidraw/components/live-collaboration/LiveCollaborationTrigger.tsx index 3111680cb..a22bc523a 100644 --- a/src/components/live-collaboration/LiveCollaborationTrigger.tsx +++ b/packages/excalidraw/components/live-collaboration/LiveCollaborationTrigger.tsx @@ -1,5 +1,5 @@ import { t } from "../../i18n"; -import { usersIcon } from "../icons"; +import { share } from "../icons"; import { Button } from "../Button"; import clsx from "clsx"; @@ -17,16 +17,18 @@ const LiveCollaborationTrigger = ({ } & React.ButtonHTMLAttributes) => { const appState = useUIAppState(); + const showIconOnly = appState.width < 830; + return (
)} diff --git a/src/components/welcome-screen/WelcomeScreen.Center.tsx b/packages/excalidraw/components/welcome-screen/WelcomeScreen.Center.tsx similarity index 100% rename from src/components/welcome-screen/WelcomeScreen.Center.tsx rename to packages/excalidraw/components/welcome-screen/WelcomeScreen.Center.tsx diff --git a/src/components/welcome-screen/WelcomeScreen.Hints.tsx b/packages/excalidraw/components/welcome-screen/WelcomeScreen.Hints.tsx similarity index 100% rename from src/components/welcome-screen/WelcomeScreen.Hints.tsx rename to packages/excalidraw/components/welcome-screen/WelcomeScreen.Hints.tsx diff --git a/src/components/welcome-screen/WelcomeScreen.scss b/packages/excalidraw/components/welcome-screen/WelcomeScreen.scss similarity index 100% rename from src/components/welcome-screen/WelcomeScreen.scss rename to packages/excalidraw/components/welcome-screen/WelcomeScreen.scss diff --git a/src/components/welcome-screen/WelcomeScreen.tsx b/packages/excalidraw/components/welcome-screen/WelcomeScreen.tsx similarity index 100% rename from src/components/welcome-screen/WelcomeScreen.tsx rename to packages/excalidraw/components/welcome-screen/WelcomeScreen.tsx diff --git a/src/constants.ts b/packages/excalidraw/constants.ts similarity index 97% rename from src/constants.ts rename to packages/excalidraw/constants.ts index 902706201..021c706a9 100644 --- a/src/constants.ts +++ b/packages/excalidraw/constants.ts @@ -2,7 +2,6 @@ import cssVariables from "./css/variables.module.scss"; import { AppProps } from "./types"; import { ExcalidrawElement, FontFamilyValues } from "./element/types"; import { COLOR_PALETTE } from "./colors"; - export const isDarwin = /Mac|iPod|iPhone|iPad/.test(navigator.platform); export const isWindows = /^Win/.test(navigator.platform); export const isAndroid = /\b(android)\b/i.test(navigator.userAgent); @@ -13,6 +12,10 @@ export const isFirefox = export const isChrome = navigator.userAgent.indexOf("Chrome") !== -1; export const isSafari = !isChrome && navigator.userAgent.indexOf("Safari") !== -1; +export const isIOS = + /iPad|iPhone/.test(navigator.platform) || + // iPadOS 13+ + (navigator.userAgent.includes("Mac") && "ontouchend" in document); // keeping function so it can be mocked in test export const isBrave = () => (navigator as any).brave?.isBrave?.name === "isBrave"; @@ -39,6 +42,7 @@ export const POINTER_BUTTON = { WHEEL: 1, SECONDARY: 2, TOUCH: -1, + ERASER: 5, } as const; export const POINTER_EVENTS = { @@ -138,6 +142,7 @@ export const DEFAULT_FONT_FAMILY: FontFamilyValues = FONT_FAMILY.Virgil; export const DEFAULT_TEXT_ALIGN = "left"; export const DEFAULT_VERTICAL_ALIGN = "top"; export const DEFAULT_VERSION = "{version}"; +export const DEFAULT_TRANSFORM_HANDLE_SPACING = 2; export const CANVAS_ONLY_ACTIONS = ["selectAll"]; @@ -373,5 +378,6 @@ export const TOOL_TYPE = { export const EDITOR_LS_KEYS = { OAI_API_KEY: "excalidraw-oai-api-key", // legacy naming (non)scheme + MERMAID_TO_EXCALIDRAW: "mermaid-to-excalidraw", PUBLISH_LIBRARY: "publish-library-data", } as const; diff --git a/src/context/tunnels.ts b/packages/excalidraw/context/tunnels.ts similarity index 100% rename from src/context/tunnels.ts rename to packages/excalidraw/context/tunnels.ts diff --git a/src/context/ui-appState.ts b/packages/excalidraw/context/ui-appState.ts similarity index 100% rename from src/context/ui-appState.ts rename to packages/excalidraw/context/ui-appState.ts diff --git a/src/css.d.ts b/packages/excalidraw/css.d.ts similarity index 100% rename from src/css.d.ts rename to packages/excalidraw/css.d.ts diff --git a/src/css/app.scss b/packages/excalidraw/css/app.scss similarity index 100% rename from src/css/app.scss rename to packages/excalidraw/css/app.scss diff --git a/src/css/styles.scss b/packages/excalidraw/css/styles.scss similarity index 98% rename from src/css/styles.scss rename to packages/excalidraw/css/styles.scss index d7202c6ac..87168520e 100644 --- a/src/css/styles.scss +++ b/packages/excalidraw/css/styles.scss @@ -1,4 +1,4 @@ -@import "./variables.module"; +@import "./variables.module.scss"; @import "./theme"; :root { @@ -53,14 +53,20 @@ // component (e.g. if you select text in a sidebar) user-select: none; + .excalidraw-link, a { font-weight: 500; text-decoration: none; color: var(--link-color); + user-select: none; + cursor: pointer; &:hover { text-decoration: underline; } + &:active { + text-decoration: none; + } } canvas { diff --git a/src/css/theme.scss b/packages/excalidraw/css/theme.scss similarity index 99% rename from src/css/theme.scss rename to packages/excalidraw/css/theme.scss index 4fb8bf81f..4bcf71754 100644 --- a/src/css/theme.scss +++ b/packages/excalidraw/css/theme.scss @@ -84,6 +84,7 @@ --color-primary-darkest: #4a47b1; --color-primary-light: #e3e2fe; --color-primary-light-darker: #d7d5ff; + --color-primary-hover: #5753d0; --color-gray-10: #f5f5f5; --color-gray-20: #ebebeb; @@ -205,6 +206,7 @@ --color-primary-darkest: #beb9ff; --color-primary-light: #4f4d6f; --color-primary-light-darker: #43415e; + --color-primary-hover: #bbb8ff; --color-text-warning: var(--color-gray-80); diff --git a/src/css/variables.module.scss b/packages/excalidraw/css/variables.module.scss similarity index 80% rename from src/css/variables.module.scss rename to packages/excalidraw/css/variables.module.scss index 634752dfa..247e3f840 100644 --- a/src/css/variables.module.scss +++ b/packages/excalidraw/css/variables.module.scss @@ -68,6 +68,7 @@ cursor: pointer; background-color: var(--button-bg, var(--island-bg-color)); color: var(--button-color, var(--color-on-surface)); + font-family: var(--ui-font); svg { width: var(--button-width, var(--lg-icon-size)); @@ -114,6 +115,47 @@ } } +@mixin avatarStyles { + width: 1.25rem; + height: 1.25rem; + position: relative; + border-radius: 100%; + outline-offset: 2px; + display: flex; + justify-content: center; + align-items: center; + cursor: pointer; + font-size: 0.75rem; + font-weight: 800; + line-height: 1; + color: var(--color-gray-90); + flex: 0 0 auto; + + &-img { + width: 100%; + height: 100%; + border-radius: 100%; + } + + &::before { + content: ""; + position: absolute; + top: -3px; + right: -3px; + bottom: -3px; + left: -3px; + border: 1px solid var(--avatar-border-color); + border-radius: 100%; + } + + &--is-followed::before { + border-color: var(--color-primary-hover); + } + &--is-current-user { + cursor: auto; + } +} + @mixin filledButtonOnCanvas { border: none; box-shadow: 0 0 0 1px var(--color-surface-lowest); diff --git a/src/cursor.ts b/packages/excalidraw/cursor.ts similarity index 100% rename from src/cursor.ts rename to packages/excalidraw/cursor.ts diff --git a/src/data/EditorLocalStorage.ts b/packages/excalidraw/data/EditorLocalStorage.ts similarity index 100% rename from src/data/EditorLocalStorage.ts rename to packages/excalidraw/data/EditorLocalStorage.ts diff --git a/src/data/__snapshots__/transform.test.ts.snap b/packages/excalidraw/data/__snapshots__/transform.test.ts.snap similarity index 96% rename from src/data/__snapshots__/transform.test.ts.snap rename to packages/excalidraw/data/__snapshots__/transform.test.ts.snap index dcd48f8b5..450fce7de 100644 --- a/src/data/__snapshots__/transform.test.ts.snap +++ b/packages/excalidraw/data/__snapshots__/transform.test.ts.snap @@ -14,6 +14,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s "type": "arrow", }, ], + "customData": undefined, "fillStyle": "solid", "frameId": null, "groupIds": [], @@ -49,6 +50,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s "type": "arrow", }, ], + "customData": undefined, "fillStyle": "solid", "frameId": null, "groupIds": [], @@ -79,6 +81,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "customData": undefined, "endArrowhead": "arrow", "endBinding": { "elementId": "ellipse-1", @@ -132,6 +135,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "customData": undefined, "endArrowhead": "arrow", "endBinding": { "elementId": "ellipse-1", @@ -190,6 +194,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s "type": "arrow", }, ], + "customData": undefined, "fillStyle": "solid", "frameId": null, "groupIds": [], @@ -227,6 +232,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t }, ], "containerId": null, + "customData": undefined, "fillStyle": "solid", "fontFamily": 1, "fontSize": 20, @@ -271,6 +277,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t }, ], "containerId": null, + "customData": undefined, "fillStyle": "solid", "fontFamily": 1, "fontSize": 20, @@ -313,6 +320,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t "type": "text", }, ], + "customData": undefined, "endArrowhead": "arrow", "endBinding": { "elementId": "text-2", @@ -368,6 +376,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t "baseline": 0, "boundElements": null, "containerId": "id48", + "customData": undefined, "fillStyle": "solid", "fontFamily": 1, "fontSize": 20, @@ -410,6 +419,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe "type": "text", }, ], + "customData": undefined, "endArrowhead": "arrow", "endBinding": { "elementId": "id40", @@ -465,6 +475,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe "baseline": 0, "boundElements": null, "containerId": "id37", + "customData": undefined, "fillStyle": "solid", "fontFamily": 1, "fontSize": 20, @@ -507,6 +518,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe "type": "arrow", }, ], + "customData": undefined, "fillStyle": "solid", "frameId": null, "groupIds": [], @@ -542,6 +554,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe "type": "arrow", }, ], + "customData": undefined, "fillStyle": "solid", "frameId": null, "groupIds": [], @@ -577,6 +590,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when "type": "text", }, ], + "customData": undefined, "endArrowhead": "arrow", "endBinding": { "elementId": "id44", @@ -632,6 +646,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when "baseline": 0, "boundElements": null, "containerId": "id41", + "customData": undefined, "fillStyle": "solid", "fontFamily": 1, "fontSize": 20, @@ -676,6 +691,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when }, ], "containerId": null, + "customData": undefined, "fillStyle": "solid", "fontFamily": 1, "fontSize": 20, @@ -720,6 +736,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when }, ], "containerId": null, + "customData": undefined, "fillStyle": "solid", "fontFamily": 1, "fontSize": 20, @@ -757,6 +774,7 @@ exports[`Test Transform > should not allow duplicate ids 1`] = ` "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "customData": undefined, "fillStyle": "solid", "frameId": null, "groupIds": [], @@ -787,6 +805,7 @@ exports[`Test Transform > should transform linear elements 1`] = ` "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "customData": undefined, "endArrowhead": "arrow", "endBinding": null, "fillStyle": "solid", @@ -832,6 +851,7 @@ exports[`Test Transform > should transform linear elements 2`] = ` "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "customData": undefined, "endArrowhead": "triangle", "endBinding": null, "fillStyle": "solid", @@ -877,6 +897,7 @@ exports[`Test Transform > should transform linear elements 3`] = ` "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "customData": undefined, "endArrowhead": null, "endBinding": null, "fillStyle": "solid", @@ -922,6 +943,7 @@ exports[`Test Transform > should transform linear elements 4`] = ` "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "customData": undefined, "endArrowhead": null, "endBinding": null, "fillStyle": "solid", @@ -967,6 +989,7 @@ exports[`Test Transform > should transform regular shapes 1`] = ` "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "customData": undefined, "fillStyle": "solid", "frameId": null, "groupIds": [], @@ -997,6 +1020,7 @@ exports[`Test Transform > should transform regular shapes 2`] = ` "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "customData": undefined, "fillStyle": "solid", "frameId": null, "groupIds": [], @@ -1027,6 +1051,7 @@ exports[`Test Transform > should transform regular shapes 3`] = ` "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "customData": undefined, "fillStyle": "solid", "frameId": null, "groupIds": [], @@ -1057,6 +1082,7 @@ exports[`Test Transform > should transform regular shapes 4`] = ` "angle": 0, "backgroundColor": "#c0eb75", "boundElements": null, + "customData": undefined, "fillStyle": "solid", "frameId": null, "groupIds": [], @@ -1087,6 +1113,7 @@ exports[`Test Transform > should transform regular shapes 5`] = ` "angle": 0, "backgroundColor": "#ffc9c9", "boundElements": null, + "customData": undefined, "fillStyle": "solid", "frameId": null, "groupIds": [], @@ -1117,6 +1144,7 @@ exports[`Test Transform > should transform regular shapes 6`] = ` "angle": 0, "backgroundColor": "#a5d8ff", "boundElements": null, + "customData": undefined, "fillStyle": "cross-hatch", "frameId": null, "groupIds": [], @@ -1149,6 +1177,7 @@ exports[`Test Transform > should transform text element 1`] = ` "baseline": 0, "boundElements": null, "containerId": null, + "customData": undefined, "fillStyle": "solid", "fontFamily": 1, "fontSize": 20, @@ -1188,6 +1217,7 @@ exports[`Test Transform > should transform text element 2`] = ` "baseline": 0, "boundElements": null, "containerId": null, + "customData": undefined, "fillStyle": "solid", "fontFamily": 1, "fontSize": 20, @@ -1230,6 +1260,7 @@ exports[`Test Transform > should transform to labelled arrows when label provide "type": "text", }, ], + "customData": undefined, "endArrowhead": "arrow", "endBinding": null, "fillStyle": "solid", @@ -1280,6 +1311,7 @@ exports[`Test Transform > should transform to labelled arrows when label provide "type": "text", }, ], + "customData": undefined, "endArrowhead": "arrow", "endBinding": null, "fillStyle": "solid", @@ -1330,6 +1362,7 @@ exports[`Test Transform > should transform to labelled arrows when label provide "type": "text", }, ], + "customData": undefined, "endArrowhead": "arrow", "endBinding": null, "fillStyle": "solid", @@ -1380,6 +1413,7 @@ exports[`Test Transform > should transform to labelled arrows when label provide "type": "text", }, ], + "customData": undefined, "endArrowhead": "arrow", "endBinding": null, "fillStyle": "solid", @@ -1427,6 +1461,7 @@ exports[`Test Transform > should transform to labelled arrows when label provide "baseline": 0, "boundElements": null, "containerId": "id25", + "customData": undefined, "fillStyle": "solid", "fontFamily": 1, "fontSize": 20, @@ -1466,6 +1501,7 @@ exports[`Test Transform > should transform to labelled arrows when label provide "baseline": 0, "boundElements": null, "containerId": "id26", + "customData": undefined, "fillStyle": "solid", "fontFamily": 1, "fontSize": 20, @@ -1505,6 +1541,7 @@ exports[`Test Transform > should transform to labelled arrows when label provide "baseline": 0, "boundElements": null, "containerId": "id27", + "customData": undefined, "fillStyle": "solid", "fontFamily": 1, "fontSize": 20, @@ -1545,6 +1582,7 @@ exports[`Test Transform > should transform to labelled arrows when label provide "baseline": 0, "boundElements": null, "containerId": "id28", + "customData": undefined, "fillStyle": "solid", "fontFamily": 1, "fontSize": 20, @@ -1588,6 +1626,7 @@ exports[`Test Transform > should transform to text containers when label provide "type": "text", }, ], + "customData": undefined, "fillStyle": "solid", "frameId": null, "groupIds": [], @@ -1623,6 +1662,7 @@ exports[`Test Transform > should transform to text containers when label provide "type": "text", }, ], + "customData": undefined, "fillStyle": "solid", "frameId": null, "groupIds": [], @@ -1658,6 +1698,7 @@ exports[`Test Transform > should transform to text containers when label provide "type": "text", }, ], + "customData": undefined, "fillStyle": "solid", "frameId": null, "groupIds": [], @@ -1693,6 +1734,7 @@ exports[`Test Transform > should transform to text containers when label provide "type": "text", }, ], + "customData": undefined, "fillStyle": "solid", "frameId": null, "groupIds": [], @@ -1728,6 +1770,7 @@ exports[`Test Transform > should transform to text containers when label provide "type": "text", }, ], + "customData": undefined, "fillStyle": "solid", "frameId": null, "groupIds": [], @@ -1763,6 +1806,7 @@ exports[`Test Transform > should transform to text containers when label provide "type": "text", }, ], + "customData": undefined, "fillStyle": "solid", "frameId": null, "groupIds": [], @@ -1795,6 +1839,7 @@ exports[`Test Transform > should transform to text containers when label provide "baseline": 0, "boundElements": null, "containerId": "id13", + "customData": undefined, "fillStyle": "solid", "fontFamily": 1, "fontSize": 20, @@ -1834,6 +1879,7 @@ exports[`Test Transform > should transform to text containers when label provide "baseline": 0, "boundElements": null, "containerId": "id14", + "customData": undefined, "fillStyle": "solid", "fontFamily": 1, "fontSize": 20, @@ -1874,6 +1920,7 @@ exports[`Test Transform > should transform to text containers when label provide "baseline": 0, "boundElements": null, "containerId": "id15", + "customData": undefined, "fillStyle": "solid", "fontFamily": 1, "fontSize": 20, @@ -1916,6 +1963,7 @@ exports[`Test Transform > should transform to text containers when label provide "baseline": 0, "boundElements": null, "containerId": "id16", + "customData": undefined, "fillStyle": "solid", "fontFamily": 1, "fontSize": 20, @@ -1956,6 +2004,7 @@ exports[`Test Transform > should transform to text containers when label provide "baseline": 0, "boundElements": null, "containerId": "id17", + "customData": undefined, "fillStyle": "solid", "fontFamily": 1, "fontSize": 20, @@ -1997,6 +2046,7 @@ exports[`Test Transform > should transform to text containers when label provide "baseline": 0, "boundElements": null, "containerId": "id18", + "customData": undefined, "fillStyle": "solid", "fontFamily": 1, "fontSize": 20, diff --git a/src/data/ai/types.ts b/packages/excalidraw/data/ai/types.ts similarity index 100% rename from src/data/ai/types.ts rename to packages/excalidraw/data/ai/types.ts diff --git a/src/data/blob.ts b/packages/excalidraw/data/blob.ts similarity index 98% rename from src/data/blob.ts rename to packages/excalidraw/data/blob.ts index b1b625700..2f8c0db96 100644 --- a/src/data/blob.ts +++ b/packages/excalidraw/data/blob.ts @@ -19,9 +19,7 @@ const parseFileContents = async (blob: Blob | File) => { if (blob.type === MIME_TYPES.png) { try { - return await ( - await import(/* webpackChunkName: "image" */ "./image") - ).decodePngMetadata(blob); + return await (await import("./image")).decodePngMetadata(blob); } catch (error: any) { if (error.message === "INVALID") { throw new ImageSceneDataError( @@ -49,7 +47,7 @@ const parseFileContents = async (blob: Blob | File) => { if (blob.type === MIME_TYPES.svg) { try { return await ( - await import(/* webpackChunkName: "image" */ "./image") + await import("./image") ).decodeSvgMetadata({ svg: contents, }); diff --git a/src/data/encode.ts b/packages/excalidraw/data/encode.ts similarity index 100% rename from src/data/encode.ts rename to packages/excalidraw/data/encode.ts diff --git a/src/data/encryption.ts b/packages/excalidraw/data/encryption.ts similarity index 100% rename from src/data/encryption.ts rename to packages/excalidraw/data/encryption.ts diff --git a/src/data/filesystem.ts b/packages/excalidraw/data/filesystem.ts similarity index 99% rename from src/data/filesystem.ts rename to packages/excalidraw/data/filesystem.ts index fa29604f4..11f64d23e 100644 --- a/src/data/filesystem.ts +++ b/packages/excalidraw/data/filesystem.ts @@ -76,7 +76,7 @@ export const fileOpen = (opts: { }; export const fileSave = ( - blob: Blob, + blob: Blob | Promise, opts: { /** supply without the extension */ name: string; diff --git a/src/data/image.ts b/packages/excalidraw/data/image.ts similarity index 100% rename from src/data/image.ts rename to packages/excalidraw/data/image.ts diff --git a/src/data/index.ts b/packages/excalidraw/data/index.ts similarity index 86% rename from src/data/index.ts rename to packages/excalidraw/data/index.ts index 3a95d4db9..fa2ec9de6 100644 --- a/src/data/index.ts +++ b/packages/excalidraw/data/index.ts @@ -11,7 +11,6 @@ import { NonDeletedExcalidrawElement, } from "../element/types"; import { t } from "../i18n"; -import { elementsOverlappingBBox } from "../packages/withinBounds"; import { isSomeElementSelected, getSelectedElements } from "../scene"; import { exportToCanvas, exportToSvg } from "../scene/export"; import { ExportType } from "../scene/types"; @@ -20,6 +19,7 @@ import { cloneJSON } from "../utils"; import { canvasToBlob } from "./blob"; import { fileSave, FileSystemHandle } from "./filesystem"; import { serializeAsJSON } from "./json"; +import { getElementsOverlappingFrame } from "../frame"; export { loadFromBlob } from "./blob"; export { loadFromJSON, saveAsJSON } from "./json"; @@ -56,11 +56,7 @@ export const prepareElementsForExport = ( isFrameLikeElement(exportedElements[0]) ) { exportingFrame = exportedElements[0]; - exportedElements = elementsOverlappingBBox({ - elements, - bounds: exportingFrame, - type: "overlap", - }); + exportedElements = getElementsOverlappingFrame(elements, exportingFrame); } else if (exportedElements.length > 1) { exportedElements = getSelectedElements( elements, @@ -104,7 +100,7 @@ export const exportCanvas = async ( throw new Error(t("alerts.cannotExportEmptyCanvas")); } if (type === "svg" || type === "clipboard-svg") { - const tempSvg = await exportToSvg( + const svgPromise = exportToSvg( elements, { exportBackground, @@ -117,9 +113,12 @@ export const exportCanvas = async ( files, { exportingFrame }, ); + if (type === "svg") { - return await fileSave( - new Blob([tempSvg.outerHTML], { type: MIME_TYPES.svg }), + return fileSave( + svgPromise.then((svg) => { + return new Blob([svg.outerHTML], { type: MIME_TYPES.svg }); + }), { description: "Export to SVG", name, @@ -128,7 +127,9 @@ export const exportCanvas = async ( }, ); } else if (type === "clipboard-svg") { - await copyTextToSystemClipboard(tempSvg.outerHTML); + await copyTextToSystemClipboard( + await svgPromise.then((svg) => svg.outerHTML), + ); return; } } @@ -141,17 +142,20 @@ export const exportCanvas = async ( }); if (type === "png") { - let blob = await canvasToBlob(tempCanvas); + let blob = canvasToBlob(tempCanvas); + if (appState.exportEmbedScene) { - blob = await ( - await import(/* webpackChunkName: "image" */ "./image") - ).encodePngMetadata({ - blob, - metadata: serializeAsJSON(elements, appState, files, "local"), - }); + blob = blob.then((blob) => + import("./image").then(({ encodePngMetadata }) => + encodePngMetadata({ + blob, + metadata: serializeAsJSON(elements, appState, files, "local"), + }), + ), + ); } - return await fileSave(blob, { + return fileSave(blob, { description: "Export to PNG", name, // FIXME reintroduce `excalidraw.png` when most people upgrade away diff --git a/src/data/json.ts b/packages/excalidraw/data/json.ts similarity index 100% rename from src/data/json.ts rename to packages/excalidraw/data/json.ts diff --git a/src/data/library.ts b/packages/excalidraw/data/library.ts similarity index 100% rename from src/data/library.ts rename to packages/excalidraw/data/library.ts diff --git a/src/data/magic.ts b/packages/excalidraw/data/magic.ts similarity index 100% rename from src/data/magic.ts rename to packages/excalidraw/data/magic.ts diff --git a/src/data/resave.ts b/packages/excalidraw/data/resave.ts similarity index 100% rename from src/data/resave.ts rename to packages/excalidraw/data/resave.ts diff --git a/src/data/restore.ts b/packages/excalidraw/data/restore.ts similarity index 98% rename from src/data/restore.ts rename to packages/excalidraw/data/restore.ts index 4c134f938..31a9727eb 100644 --- a/src/data/restore.ts +++ b/packages/excalidraw/data/restore.ts @@ -40,6 +40,7 @@ import { arrayToMap } from "../utils"; import { MarkOptional, Mutable } from "../utility-types"; import { detectLineHeight, + getContainerElement, getDefaultLineHeight, measureTextElement, } from "../element/textElement"; @@ -183,7 +184,6 @@ const restoreElementWithProperties = < const restoreElement = ( element: Exclude, - refreshDimensions = false, ): typeof element | null => { switch (element.type) { case "text": @@ -232,10 +232,6 @@ const restoreElement = ( element = bumpVersion(element); } - if (refreshDimensions) { - element = { ...element, ...refreshTextDimensions(element) }; - } - return element; case "freedraw": { return restoreElementWithProperties(element, { @@ -295,11 +291,8 @@ const restoreElement = ( case "rectangle": case "diamond": case "iframe": - return restoreElementWithProperties(element, {}); case "embeddable": - return restoreElementWithProperties(element, { - validated: null, - }); + return restoreElementWithProperties(element, {}); case "magicframe": case "frame": return restoreElementWithProperties(element, { @@ -429,10 +422,7 @@ export const restoreElements = ( // filtering out selection, which is legacy, no longer kept in elements, // and causing issues if retained if (element.type !== "selection" && !isInvisiblySmallElement(element)) { - let migratedElement: ExcalidrawElement | null = restoreElement( - element, - opts?.refreshDimensions, - ); + let migratedElement: ExcalidrawElement | null = restoreElement(element); if (migratedElement) { const localElement = localElementsMap?.get(element.id); if (localElement && localElement.version > migratedElement.version) { @@ -465,6 +455,16 @@ export const restoreElements = ( } else if (element.boundElements) { repairContainerElement(element, restoredElementsMap); } + + if (opts.refreshDimensions && isTextElement(element)) { + Object.assign( + element, + refreshTextDimensions( + element, + getContainerElement(element, restoredElementsMap), + ), + ); + } } return restoredElements; diff --git a/src/data/transform.test.ts b/packages/excalidraw/data/transform.test.ts similarity index 97% rename from src/data/transform.test.ts rename to packages/excalidraw/data/transform.test.ts index 7c71f33f8..239cd2f4c 100644 --- a/src/data/transform.test.ts +++ b/packages/excalidraw/data/transform.test.ts @@ -822,4 +822,22 @@ describe("Test Transform", () => { "Duplicate id found for rect-1", ); }); + + it("should contains customData if provided", () => { + const rawData = [ + { + type: "rectangle", + x: 100, + y: 100, + customData: { createdBy: "user01" }, + }, + ]; + const convertedElements = convertToExcalidrawElements( + rawData as ExcalidrawElementSkeleton[], + opts, + ); + expect(convertedElements[0].customData).toStrictEqual({ + createdBy: "user01", + }); + }); }); diff --git a/src/data/transform.ts b/packages/excalidraw/data/transform.ts similarity index 99% rename from src/data/transform.ts rename to packages/excalidraw/data/transform.ts index 7b5286923..8ce842300 100644 --- a/src/data/transform.ts +++ b/packages/excalidraw/data/transform.ts @@ -24,6 +24,7 @@ import { normalizeText, } from "../element/textElement"; import { + ElementsMap, ExcalidrawArrowElement, ExcalidrawBindableElement, ExcalidrawElement, @@ -42,7 +43,7 @@ import { VerticalAlign, } from "../element/types"; import { MarkOptional } from "../utility-types"; -import { assertNever, cloneJSON, getFontString } from "../utils"; +import { arrayToMap, assertNever, cloneJSON, getFontString } from "../utils"; import { getSizeFromPoints } from "../points"; import { randomId } from "../random"; @@ -202,6 +203,7 @@ const DEFAULT_DIMENSION = 100; const bindTextToContainer = ( container: ExcalidrawElement, textProps: { text: string } & MarkOptional, + elementsMap: ElementsMap, ) => { const textElement: ExcalidrawTextElement = newTextElement({ x: 0, @@ -623,6 +625,7 @@ export const convertToExcalidrawElements = ( let [container, text] = bindTextToContainer( excalidrawElement, element?.label, + arrayToMap(elementStore.getElements()), ); elementStore.add(container); elementStore.add(text); diff --git a/src/data/types.ts b/packages/excalidraw/data/types.ts similarity index 100% rename from src/data/types.ts rename to packages/excalidraw/data/types.ts diff --git a/src/data/url.test.tsx b/packages/excalidraw/data/url.test.tsx similarity index 100% rename from src/data/url.test.tsx rename to packages/excalidraw/data/url.test.tsx diff --git a/src/data/url.ts b/packages/excalidraw/data/url.ts similarity index 100% rename from src/data/url.ts rename to packages/excalidraw/data/url.ts diff --git a/src/distribute.ts b/packages/excalidraw/distribute.ts similarity index 93% rename from src/distribute.ts rename to packages/excalidraw/distribute.ts index acad09b2d..368b2f24d 100644 --- a/src/distribute.ts +++ b/packages/excalidraw/distribute.ts @@ -1,7 +1,7 @@ -import { ExcalidrawElement } from "./element/types"; import { newElementWith } from "./element/mutateElement"; import { getMaximumGroups } from "./groups"; import { getCommonBoundingBox } from "./element/bounds"; +import type { ElementsMap, ExcalidrawElement } from "./element/types"; export interface Distribution { space: "between"; @@ -10,6 +10,7 @@ export interface Distribution { export const distributeElements = ( selectedElements: ExcalidrawElement[], + elementsMap: ElementsMap, distribution: Distribution, ): ExcalidrawElement[] => { const [start, mid, end, extent] = @@ -18,7 +19,7 @@ export const distributeElements = ( : (["minY", "midY", "maxY", "height"] as const); const bounds = getCommonBoundingBox(selectedElements); - const groups = getMaximumGroups(selectedElements) + const groups = getMaximumGroups(selectedElements, elementsMap) .map((group) => [group, getCommonBoundingBox(group)] as const) .sort((a, b) => a[1][mid] - b[1][mid]); diff --git a/src/element/ElementCanvasButtons.scss b/packages/excalidraw/element/ElementCanvasButtons.scss similarity index 100% rename from src/element/ElementCanvasButtons.scss rename to packages/excalidraw/element/ElementCanvasButtons.scss diff --git a/src/element/ElementCanvasButtons.tsx b/packages/excalidraw/element/ElementCanvasButtons.tsx similarity index 100% rename from src/element/ElementCanvasButtons.tsx rename to packages/excalidraw/element/ElementCanvasButtons.tsx diff --git a/src/element/Hyperlink.scss b/packages/excalidraw/element/Hyperlink.scss similarity index 96% rename from src/element/Hyperlink.scss rename to packages/excalidraw/element/Hyperlink.scss index eeb2f46f6..ba7e86373 100644 --- a/src/element/Hyperlink.scss +++ b/packages/excalidraw/element/Hyperlink.scss @@ -1,4 +1,4 @@ -@import "../css/variables.module"; +@import "../css/variables.module.scss"; .excalidraw-hyperlinkContainer { display: flex; diff --git a/src/element/Hyperlink.tsx b/packages/excalidraw/element/Hyperlink.tsx similarity index 96% rename from src/element/Hyperlink.tsx rename to packages/excalidraw/element/Hyperlink.tsx index caed8fe37..930b87763 100644 --- a/src/element/Hyperlink.tsx +++ b/packages/excalidraw/element/Hyperlink.tsx @@ -32,14 +32,13 @@ import { Bounds } from "./bounds"; import { getTooltipDiv, updateTooltipPosition } from "../components/Tooltip"; import { getSelectedElements } from "../scene"; import { isPointHittingElementBoundingBox } from "./collision"; -import { getElementAbsoluteCoords } from "./"; +import { getElementAbsoluteCoords } from "."; import { isLocalLink, normalizeLink } from "../data/url"; import "./Hyperlink.scss"; import { trackEvent } from "../analytics"; import { useAppProps, useExcalidrawAppState } from "../components/App"; import { isEmbeddableElement } from "./typeChecks"; -import { ShapeCache } from "../scene/ShapeCache"; const CONTAINER_WIDTH = 320; const SPACE_BOTTOM = 85; @@ -64,6 +63,7 @@ export const Hyperlink = ({ setAppState, onLinkOpen, setToast, + updateEmbedValidationStatus, }: { element: NonDeletedExcalidrawElement; setAppState: React.Component["setState"]; @@ -71,6 +71,10 @@ export const Hyperlink = ({ setToast: ( toast: { message: string; closable?: boolean; duration?: number } | null, ) => void; + updateEmbedValidationStatus: ( + element: ExcalidrawEmbeddableElement, + status: boolean, + ) => void; }) => { const appState = useExcalidrawAppState(); const appProps = useAppProps(); @@ -98,9 +102,9 @@ export const Hyperlink = ({ } if (!link) { mutateElement(element, { - validated: false, link: null, }); + updateEmbedValidationStatus(element, false); return; } @@ -110,15 +114,17 @@ export const Hyperlink = ({ } element.link && embeddableLinkCache.set(element.id, element.link); mutateElement(element, { - validated: false, link, }); - ShapeCache.delete(element); + updateEmbedValidationStatus(element, false); } else { const { width, height } = element; const embedLink = getEmbedLink(link); - if (embedLink?.warning) { - setToast({ message: embedLink.warning, closable: true }); + if (embedLink?.error instanceof URIError) { + setToast({ + message: t("toast.unrecognizedLinkFormat"), + closable: true, + }); } const ar = embedLink ? embedLink.intrinsicSize.w / embedLink.intrinsicSize.h @@ -142,10 +148,9 @@ export const Hyperlink = ({ : height, } : {}), - validated: true, link, }); - ShapeCache.delete(element); + updateEmbedValidationStatus(element, true); if (embeddableLinkCache.has(element.id)) { embeddableLinkCache.delete(element.id); } @@ -159,6 +164,7 @@ export const Hyperlink = ({ appProps.validateEmbeddable, appState.activeEmbeddable, setAppState, + updateEmbedValidationStatus, ]); useLayoutEffect(() => { diff --git a/src/element/binding.ts b/packages/excalidraw/element/binding.ts similarity index 98% rename from src/element/binding.ts rename to packages/excalidraw/element/binding.ts index 3f6cf0022..66d29f3f6 100644 --- a/src/element/binding.ts +++ b/packages/excalidraw/element/binding.ts @@ -321,9 +321,9 @@ export const updateBoundElements = ( const simultaneouslyUpdatedElementIds = getSimultaneouslyUpdatedElementIds( simultaneouslyUpdated, ); - + const scene = Scene.getScene(changedElement)!; getNonDeletedElements( - Scene.getScene(changedElement)!, + scene, boundLinearElements.map((el) => el.id), ).forEach((element) => { if (!isLinearElement(element)) { @@ -362,9 +362,12 @@ export const updateBoundElements = ( endBinding, changedElement as ExcalidrawBindableElement, ); - const boundText = getBoundTextElement(element); + const boundText = getBoundTextElement( + element, + scene.getNonDeletedElementsMap(), + ); if (boundText) { - handleBindTextResize(element, false); + handleBindTextResize(element, scene.getNonDeletedElementsMap(), false); } }); }; diff --git a/src/element/bounds.test.ts b/packages/excalidraw/element/bounds.test.ts similarity index 100% rename from src/element/bounds.test.ts rename to packages/excalidraw/element/bounds.test.ts diff --git a/src/element/bounds.ts b/packages/excalidraw/element/bounds.ts similarity index 86% rename from src/element/bounds.ts rename to packages/excalidraw/element/bounds.ts index b0d33cfc9..f892089f7 100644 --- a/src/element/bounds.ts +++ b/packages/excalidraw/element/bounds.ts @@ -5,11 +5,13 @@ import { ExcalidrawFreeDrawElement, NonDeleted, ExcalidrawTextElementWithContainer, + ElementsMapOrArray, + ElementsMap, } from "./types"; import { distance2d, rotate, rotatePoint } from "../math"; import rough from "roughjs/bin/rough"; import { Drawable, Op } from "roughjs/bin/core"; -import { Point } from "../types"; +import { AppState, Point } from "../types"; import { generateRoughOptions } from "../scene/Shape"; import { isArrowElement, @@ -35,7 +37,9 @@ export type RectangleBox = { type MaybeQuadraticSolution = [number | null, number | null] | false; -// x and y position of top left corner, x and y position of bottom right corner +/** + * x and y position of top left corner, x and y position of bottom right corner + */ export type Bounds = readonly [ minX: number, minY: number, @@ -43,6 +47,13 @@ export type Bounds = readonly [ maxY: number, ]; +export type SceneBounds = readonly [ + sceneX: number, + sceneY: number, + sceneX2: number, + sceneY2: number, +]; + export class ElementBounds { private static boundsCache = new WeakMap< ExcalidrawElement, @@ -64,13 +75,16 @@ export class ElementBounds { ) { return cachedBounds.bounds; } - - const bounds = ElementBounds.calculateBounds(element); + const scene = Scene.getScene(element); + const bounds = ElementBounds.calculateBounds( + element, + scene?.getNonDeletedElementsMap() || new Map(), + ); // hack to ensure that downstream checks could retrieve element Scene // so as to have correctly calculated bounds // FIXME remove when we get rid of all the id:Scene / element:Scene mapping - const shouldCache = Scene.getScene(element); + const shouldCache = !!scene; if (shouldCache) { ElementBounds.boundsCache.set(element, { @@ -82,7 +96,10 @@ export class ElementBounds { return bounds; } - private static calculateBounds(element: ExcalidrawElement): Bounds { + private static calculateBounds( + element: ExcalidrawElement, + elementsMap: ElementsMap, + ): Bounds { let bounds: Bounds; const [x1, y1, x2, y2, cx, cy] = getElementAbsoluteCoords(element); @@ -101,7 +118,7 @@ export class ElementBounds { maxY + element.y, ]; } else if (isLinearElement(element)) { - bounds = getLinearElementRotatedBounds(element, cx, cy); + bounds = getLinearElementRotatedBounds(element, cx, cy, elementsMap); } else if (element.type === "diamond") { const [x11, y11] = rotate(cx, y1, cx, cy, element.angle); const [x12, y12] = rotate(cx, y2, cx, cy, element.angle); @@ -144,15 +161,20 @@ export const getElementAbsoluteCoords = ( element: ExcalidrawElement, includeBoundText: boolean = false, ): [number, number, number, number, number, number] => { + const elementsMap = + Scene.getScene(element)?.getElementsMapIncludingDeleted() || new Map(); if (isFreeDrawElement(element)) { return getFreeDrawElementAbsoluteCoords(element); } else if (isLinearElement(element)) { return LinearElementEditor.getElementAbsoluteCoords( element, + elementsMap, includeBoundText, ); } else if (isTextElement(element)) { - const container = getContainerElement(element); + const container = elementsMap + ? getContainerElement(element, elementsMap) + : null; if (isArrowElement(container)) { const coords = LinearElementEditor.getBoundTextElementPosition( container, @@ -484,6 +506,31 @@ const getFreeDrawElementAbsoluteCoords = ( return [x1, y1, x2, y2, (x1 + x2) / 2, (y1 + y2) / 2]; }; +/** @returns number in pixels */ +export const getArrowheadSize = (arrowhead: Arrowhead): number => { + switch (arrowhead) { + case "arrow": + return 25; + case "diamond": + case "diamond_outline": + return 12; + default: + return 15; + } +}; + +/** @returns number in degrees */ +export const getArrowheadAngle = (arrowhead: Arrowhead): number => { + switch (arrowhead) { + case "bar": + return 90; + case "arrow": + return 20; + default: + return 25; + } +}; + export const getArrowheadPoints = ( element: ExcalidrawLinearElement, shape: Drawable[], @@ -536,53 +583,82 @@ export const getArrowheadPoints = ( const nx = (x2 - x1) / distance; const ny = (y2 - y1) / distance; - const size = { - arrow: 30, - bar: 15, - dot: 15, - triangle: 15, - }[arrowhead]; // pixels (will differ for each arrowhead) + const size = getArrowheadSize(arrowhead); let length = 0; - if (arrowhead === "arrow") { + { // Length for -> arrows is based on the length of the last section - const [cx, cy] = element.points[element.points.length - 1]; + const [cx, cy] = + position === "end" + ? element.points[element.points.length - 1] + : element.points[0]; const [px, py] = element.points.length > 1 - ? element.points[element.points.length - 2] + ? position === "end" + ? element.points[element.points.length - 2] + : element.points[1] : [0, 0]; length = Math.hypot(cx - px, cy - py); - } else { - // Length for other arrowhead types is based on the total length of the line - for (let i = 0; i < element.points.length; i++) { - const [px, py] = element.points[i - 1] || [0, 0]; - const [cx, cy] = element.points[i]; - length += Math.hypot(cx - px, cy - py); - } } // Scale down the arrowhead until we hit a certain size so that it doesn't look weird. // This value is selected by minimizing a minimum size with the last segment of the arrowhead - const minSize = Math.min(size, length / 2); + const lengthMultiplier = + arrowhead === "diamond" || arrowhead === "diamond_outline" ? 0.25 : 0.5; + const minSize = Math.min(size, length * lengthMultiplier); const xs = x2 - nx * minSize; const ys = y2 - ny * minSize; - if (arrowhead === "dot") { - const r = Math.hypot(ys - y2, xs - x2) + element.strokeWidth; - return [x2, y2, r]; + if ( + arrowhead === "dot" || + arrowhead === "circle" || + arrowhead === "circle_outline" + ) { + const diameter = Math.hypot(ys - y2, xs - x2) + element.strokeWidth - 2; + return [x2, y2, diameter]; } - const angle = { - arrow: 20, - bar: 90, - triangle: 25, - }[arrowhead]; // degrees + const angle = getArrowheadAngle(arrowhead); // Return points const [x3, y3] = rotate(xs, ys, x2, y2, (-angle * Math.PI) / 180); const [x4, y4] = rotate(xs, ys, x2, y2, (angle * Math.PI) / 180); + + if (arrowhead === "diamond" || arrowhead === "diamond_outline") { + // point opposite to the arrowhead point + let ox; + let oy; + + if (position === "start") { + const [px, py] = element.points.length > 1 ? element.points[1] : [0, 0]; + + [ox, oy] = rotate( + x2 + minSize * 2, + y2, + x2, + y2, + Math.atan2(py - y2, px - x2), + ); + } else { + const [px, py] = + element.points.length > 1 + ? element.points[element.points.length - 2] + : [0, 0]; + + [ox, oy] = rotate( + x2 - minSize * 2, + y2, + x2, + y2, + Math.atan2(y2 - py, x2 - px), + ); + } + + return [x2, y2, x3, y3, ox, oy, x4, y4]; + } + return [x2, y2, x3, y3, x4, y4]; }; @@ -609,7 +685,10 @@ const getLinearElementRotatedBounds = ( element: ExcalidrawLinearElement, cx: number, cy: number, + elementsMap: ElementsMap, ): Bounds => { + const boundTextElement = getBoundTextElement(element, elementsMap); + if (element.points.length < 2) { const [pointX, pointY] = element.points[0]; const [x, y] = rotate( @@ -621,7 +700,6 @@ const getLinearElementRotatedBounds = ( ); let coords: Bounds = [x, y, x, y]; - const boundTextElement = getBoundTextElement(element); if (boundTextElement) { const coordsWithBoundText = LinearElementEditor.getMinMaxXYWithBoundText( element, @@ -646,7 +724,6 @@ const getLinearElementRotatedBounds = ( rotate(element.x + x, element.y + y, cx, cy, element.angle); const res = getMinMaxXYFromCurvePathOps(ops, transformXY); let coords: Bounds = [res[0], res[1], res[2], res[3]]; - const boundTextElement = getBoundTextElement(element); if (boundTextElement) { const coordsWithBoundText = LinearElementEditor.getMinMaxXYWithBoundText( element, @@ -666,10 +743,8 @@ const getLinearElementRotatedBounds = ( export const getElementBounds = (element: ExcalidrawElement): Bounds => { return ElementBounds.getBounds(element); }; -export const getCommonBounds = ( - elements: readonly ExcalidrawElement[], -): Bounds => { - if (!elements.length) { +export const getCommonBounds = (elements: ElementsMapOrArray): Bounds => { + if ("size" in elements ? !elements.size : !elements.length) { return [0, 0, 0, 0]; } @@ -825,3 +900,21 @@ export const getCommonBoundingBox = ( midY: (minY + maxY) / 2, }; }; + +/** + * returns scene coords of user's editor viewport (visible canvas area) bounds + */ +export const getVisibleSceneBounds = ({ + scrollX, + scrollY, + width, + height, + zoom, +}: AppState): SceneBounds => { + return [ + -scrollX, + -scrollY, + -scrollX + width / zoom.value, + -scrollY + height / zoom.value, + ]; +}; diff --git a/src/element/collision.ts b/packages/excalidraw/element/collision.ts similarity index 99% rename from src/element/collision.ts rename to packages/excalidraw/element/collision.ts index 709781b22..b8c07e3ab 100644 --- a/src/element/collision.ts +++ b/packages/excalidraw/element/collision.ts @@ -28,6 +28,7 @@ import { StrokeRoundness, ExcalidrawFrameLikeElement, ExcalidrawIframeLikeElement, + ElementsMap, } from "./types"; import { @@ -78,6 +79,7 @@ export const hitTest = ( frameNameBoundsCache: FrameNameBoundsCache, x: number, y: number, + elementsMap: ElementsMap, ): boolean => { // How many pixels off the shape boundary we still consider a hit const threshold = 10 / appState.zoom.value; @@ -95,7 +97,7 @@ export const hitTest = ( ); } - const boundTextElement = getBoundTextElement(element); + const boundTextElement = getBoundTextElement(element, elementsMap); if (boundTextElement) { const isHittingBoundTextElement = hitTest( boundTextElement, @@ -103,6 +105,7 @@ export const hitTest = ( frameNameBoundsCache, x, y, + elementsMap, ); if (isHittingBoundTextElement) { return true; @@ -122,15 +125,16 @@ export const isHittingElementBoundingBoxWithoutHittingElement = ( frameNameBoundsCache: FrameNameBoundsCache, x: number, y: number, + elementsMap: ElementsMap, ): boolean => { const threshold = 10 / appState.zoom.value; // So that bound text element hit is considered within bounding box of container even if its outside actual bounding box of element // eg for linear elements text can be outside the element bounding box - const boundTextElement = getBoundTextElement(element); + const boundTextElement = getBoundTextElement(element, elementsMap); if ( boundTextElement && - hitTest(boundTextElement, appState, frameNameBoundsCache, x, y) + hitTest(boundTextElement, appState, frameNameBoundsCache, x, y, elementsMap) ) { return false; } diff --git a/packages/excalidraw/element/containerCache.ts b/packages/excalidraw/element/containerCache.ts new file mode 100644 index 000000000..c744f6c8e --- /dev/null +++ b/packages/excalidraw/element/containerCache.ts @@ -0,0 +1,33 @@ +import { ExcalidrawTextContainer } from "./types"; + +export const originalContainerCache: { + [id: ExcalidrawTextContainer["id"]]: + | { + height: ExcalidrawTextContainer["height"]; + } + | undefined; +} = {}; + +export const updateOriginalContainerCache = ( + id: ExcalidrawTextContainer["id"], + height: ExcalidrawTextContainer["height"], +) => { + const data = + originalContainerCache[id] || (originalContainerCache[id] = { height }); + data.height = height; + return data; +}; + +export const resetOriginalContainerCache = ( + id: ExcalidrawTextContainer["id"], +) => { + if (originalContainerCache[id]) { + delete originalContainerCache[id]; + } +}; + +export const getOriginalContainerHeightFromCache = ( + id: ExcalidrawTextContainer["id"], +) => { + return originalContainerCache[id]?.height ?? null; +}; diff --git a/src/element/dragElements.ts b/packages/excalidraw/element/dragElements.ts similarity index 82% rename from src/element/dragElements.ts rename to packages/excalidraw/element/dragElements.ts index c91ad64c6..0144f55a4 100644 --- a/src/element/dragElements.ts +++ b/packages/excalidraw/element/dragElements.ts @@ -5,14 +5,9 @@ import { getPerfectElementSize } from "./sizeHelpers"; import { NonDeletedExcalidrawElement } from "./types"; import { AppState, PointerDownState } from "../types"; import { getBoundTextElement } from "./textElement"; -import { isSelectedViaGroup } from "../groups"; import { getGridPoint } from "../math"; import Scene from "../scene/Scene"; -import { - isArrowElement, - isBoundToContainer, - isFrameLikeElement, -} from "./typeChecks"; +import { isArrowElement, isFrameLikeElement } from "./typeChecks"; export const dragSelectedElements = ( pointerDownState: PointerDownState, @@ -37,13 +32,11 @@ export const dragSelectedElements = ( .map((f) => f.id); if (frames.length > 0) { - const elementsInFrames = scene - .getNonDeletedElements() - .filter((e) => !isBoundToContainer(e)) - .filter((e) => e.frameId !== null) - .filter((e) => frames.includes(e.frameId!)); - - elementsInFrames.forEach((element) => elementsToUpdate.add(element)); + for (const element of scene.getNonDeletedElements()) { + if (element.frameId !== null && frames.includes(element.frameId)) { + elementsToUpdate.add(element); + } + } } const commonBounds = getCommonBounds( @@ -60,18 +53,14 @@ export const dragSelectedElements = ( elementsToUpdate.forEach((element) => { updateElementCoords(pointerDownState, element, adjustedOffset); - // update coords of bound text only if we're dragging the container directly - // (we don't drag the group that it's part of) if ( - // Don't update coords of arrow label since we calculate its position during render - !isArrowElement(element) && - // container isn't part of any group - // (perf optim so we don't check `isSelectedViaGroup()` in every case) - (!element.groupIds.length || - // container is part of a group, but we're dragging the container directly - (appState.editingGroupId && !isSelectedViaGroup(appState, element))) + // skip arrow labels since we calculate its position during render + !isArrowElement(element) ) { - const textElement = getBoundTextElement(element); + const textElement = getBoundTextElement( + element, + scene.getNonDeletedElementsMap(), + ); if (textElement) { updateElementCoords(pointerDownState, textElement, adjustedOffset); } diff --git a/src/element/embeddable.ts b/packages/excalidraw/element/embeddable.ts similarity index 88% rename from src/element/embeddable.ts rename to packages/excalidraw/element/embeddable.ts index c129d3927..fb51c7283 100644 --- a/src/element/embeddable.ts +++ b/packages/excalidraw/element/embeddable.ts @@ -1,21 +1,15 @@ import { register } from "../actions/register"; import { FONT_FAMILY, VERTICAL_ALIGN } from "../constants"; -import { t } from "../i18n"; import { ExcalidrawProps } from "../types"; import { getFontString, updateActiveTool } from "../utils"; import { setCursorForShape } from "../cursor"; import { newTextElement } from "./newElement"; -import { getContainerElement, wrapText } from "./textElement"; -import { - isFrameLikeElement, - isIframeElement, - isIframeLikeElement, -} from "./typeChecks"; +import { wrapText } from "./textElement"; +import { isIframeElement } from "./typeChecks"; import { ExcalidrawElement, ExcalidrawIframeLikeElement, IframeData, - NonDeletedExcalidrawElement, } from "./types"; const embeddedLinkCache = new Map(); @@ -32,9 +26,9 @@ const RE_GH_GIST_EMBED = /^ twitter embeds -const RE_TWITTER = /(?:http(?:s)?:\/\/)?(?:(?:w){3}.)?twitter.com/; +const RE_TWITTER = /(?:http(?:s)?:\/\/)?(?:(?:w){3}.)?(?:twitter|x).com/; const RE_TWITTER_EMBED = - /^ { - if (isIframeLikeElement(element)) { - return true; - } - if (element.type === "text") { - const container = getContainerElement(element); - if (container && isFrameLikeElement(container)) { - return true; - } - } - return false; -}; - export const createPlaceholderEmbeddableLabel = ( element: ExcalidrawIframeLikeElement, ): ExcalidrawElement => { @@ -321,26 +304,26 @@ const validateHostname = ( return false; }; -export const extractSrc = (htmlString: string): string => { - const twitterMatch = htmlString.match(RE_TWITTER_EMBED); +export const maybeParseEmbedSrc = (str: string): string => { + const twitterMatch = str.match(RE_TWITTER_EMBED); if (twitterMatch && twitterMatch.length === 2) { return twitterMatch[1]; } - const gistMatch = htmlString.match(RE_GH_GIST_EMBED); + const gistMatch = str.match(RE_GH_GIST_EMBED); if (gistMatch && gistMatch.length === 2) { return gistMatch[1]; } - if (RE_GIPHY.test(htmlString)) { - return `https://giphy.com/embed/${RE_GIPHY.exec(htmlString)![1]}`; + if (RE_GIPHY.test(str)) { + return `https://giphy.com/embed/${RE_GIPHY.exec(str)![1]}`; } - const match = htmlString.match(RE_GENERIC_EMBED); + const match = str.match(RE_GENERIC_EMBED); if (match && match.length === 2) { return match[1]; } - return htmlString; + return str; }; export const embeddableURLValidator = ( diff --git a/src/element/image.ts b/packages/excalidraw/element/image.ts similarity index 100% rename from src/element/image.ts rename to packages/excalidraw/element/image.ts diff --git a/src/element/index.ts b/packages/excalidraw/element/index.ts similarity index 98% rename from src/element/index.ts rename to packages/excalidraw/element/index.ts index 37d6a077b..093ef4829 100644 --- a/src/element/index.ts +++ b/packages/excalidraw/element/index.ts @@ -50,7 +50,6 @@ export { dragNewElement, } from "./dragElements"; export { isTextElement, isExcalidrawElement } from "./typeChecks"; -export { textWysiwyg } from "./textWysiwyg"; export { redrawTextBoundingBox } from "./textElement"; export { getPerfectElementSize, diff --git a/src/element/linearElementEditor.ts b/packages/excalidraw/element/linearElementEditor.ts similarity index 98% rename from src/element/linearElementEditor.ts rename to packages/excalidraw/element/linearElementEditor.ts index 9ee490b39..5c3c6acaa 100644 --- a/src/element/linearElementEditor.ts +++ b/packages/excalidraw/element/linearElementEditor.ts @@ -5,6 +5,7 @@ import { PointBinding, ExcalidrawBindableElement, ExcalidrawTextElementWithContainer, + ElementsMap, } from "./types"; import { distance2d, @@ -193,6 +194,7 @@ export class LinearElementEditor { pointSceneCoords: { x: number; y: number }[], ) => void, linearElementEditor: LinearElementEditor, + elementsMap: ElementsMap, ): boolean { if (!linearElementEditor) { return false; @@ -272,9 +274,9 @@ export class LinearElementEditor { ); } - const boundTextElement = getBoundTextElement(element); + const boundTextElement = getBoundTextElement(element, elementsMap); if (boundTextElement) { - handleBindTextResize(element, false); + handleBindTextResize(element, elementsMap, false); } // suggest bindings for first and last point if selected @@ -404,9 +406,10 @@ export class LinearElementEditor { static getEditorMidPoints = ( element: NonDeleted, + elementsMap: ElementsMap, appState: InteractiveCanvasAppState, ): typeof editorMidPointsCache["points"] => { - const boundText = getBoundTextElement(element); + const boundText = getBoundTextElement(element, elementsMap); // Since its not needed outside editor unless 2 pointer lines or bound text if ( @@ -465,6 +468,7 @@ export class LinearElementEditor { linearElementEditor: LinearElementEditor, scenePointer: { x: number; y: number }, appState: AppState, + elementsMap: ElementsMap, ) => { const { elementId } = linearElementEditor; const element = LinearElementEditor.getElement(elementId); @@ -503,7 +507,7 @@ export class LinearElementEditor { } let index = 0; const midPoints: typeof editorMidPointsCache["points"] = - LinearElementEditor.getEditorMidPoints(element, appState); + LinearElementEditor.getEditorMidPoints(element, elementsMap, appState); while (index < midPoints.length) { if (midPoints[index] !== null) { const distance = distance2d( @@ -581,6 +585,7 @@ export class LinearElementEditor { linearElementEditor: LinearElementEditor, appState: AppState, midPoint: Point, + elementsMap: ElementsMap, ) { const element = LinearElementEditor.getElement( linearElementEditor.elementId, @@ -588,7 +593,11 @@ export class LinearElementEditor { if (!element) { return -1; } - const midPoints = LinearElementEditor.getEditorMidPoints(element, appState); + const midPoints = LinearElementEditor.getEditorMidPoints( + element, + elementsMap, + appState, + ); let index = 0; while (index < midPoints.length) { if (LinearElementEditor.arePointsEqual(midPoint, midPoints[index])) { @@ -605,6 +614,7 @@ export class LinearElementEditor { history: History, scenePointer: { x: number; y: number }, linearElementEditor: LinearElementEditor, + elementsMap: ElementsMap, ): { didAddPoint: boolean; hitElement: NonDeleted | null; @@ -630,6 +640,7 @@ export class LinearElementEditor { linearElementEditor, scenePointer, appState, + elementsMap, ); let segmentMidpointIndex = null; if (segmentMidpoint) { @@ -637,6 +648,7 @@ export class LinearElementEditor { linearElementEditor, appState, segmentMidpoint, + elementsMap, ); } if (event.altKey && appState.editingLinearElement) { @@ -1418,6 +1430,7 @@ export class LinearElementEditor { static getElementAbsoluteCoords = ( element: ExcalidrawLinearElement, + elementsMap: ElementsMap, includeBoundText: boolean = false, ): [number, number, number, number, number, number] => { let coords: [number, number, number, number, number, number]; @@ -1444,7 +1457,7 @@ export class LinearElementEditor { x2 = maxX + element.x; y2 = maxY + element.y; } else { - const shape = ShapeCache.generateElementShape(element); + const shape = ShapeCache.generateElementShape(element, null); // first element is always the curve const ops = getCurvePathOps(shape[0]); @@ -1462,7 +1475,7 @@ export class LinearElementEditor { if (!includeBoundText) { return coords; } - const boundTextElement = getBoundTextElement(element); + const boundTextElement = getBoundTextElement(element, elementsMap); if (boundTextElement) { coords = LinearElementEditor.getMinMaxXYWithBoundText( element, diff --git a/src/element/mutateElement.ts b/packages/excalidraw/element/mutateElement.ts similarity index 100% rename from src/element/mutateElement.ts rename to packages/excalidraw/element/mutateElement.ts diff --git a/src/element/newElement.test.ts b/packages/excalidraw/element/newElement.test.ts similarity index 100% rename from src/element/newElement.test.ts rename to packages/excalidraw/element/newElement.test.ts diff --git a/src/element/newElement.ts b/packages/excalidraw/element/newElement.ts similarity index 97% rename from src/element/newElement.ts rename to packages/excalidraw/element/newElement.ts index e7611a406..83e876660 100644 --- a/src/element/newElement.ts +++ b/packages/excalidraw/element/newElement.ts @@ -16,7 +16,7 @@ import { ExcalidrawEmbeddableElement, ExcalidrawMagicFrameElement, ExcalidrawIframeElement, -} from "../element/types"; +} from "./types"; import { arrayToMap, getUpdatedTimestamp, isTestEnv } from "../utils"; import { randomInteger, randomId } from "../random"; import { bumpVersion, newElementWith } from "./mutateElement"; @@ -26,7 +26,6 @@ import { getElementAbsoluteCoords } from "."; import { adjustXYWithRotation } from "../math"; import { getResizedElementAbsoluteCoords } from "./bounds"; import { - getContainerElement, measureTextElement, normalizeText, wrapTextElement, @@ -90,6 +89,7 @@ export type ElementConstructorOpts = MarkOptional< | "roundness" | "locked" | "opacity" + | "customData" >; const _newElementBase = ( @@ -145,6 +145,7 @@ const _newElementBase = ( updated: getUpdatedTimestamp(), link, locked, + customData: rest.customData, }; return element; }; @@ -162,13 +163,9 @@ export const newElement = ( export const newEmbeddableElement = ( opts: { type: "embeddable"; - validated: ExcalidrawEmbeddableElement["validated"]; } & ElementConstructorOpts, ): NonDeleted => { - return { - ..._newElementBase("embeddable", opts), - validated: opts.validated, - }; + return _newElementBase("embeddable", opts); }; export const newIframeElement = ( @@ -363,16 +360,20 @@ const getAdjustedDimensions = ( export const refreshTextDimensions = ( textElement: ExcalidrawTextElement, + container: ExcalidrawTextContainer | null, text = textElement.text, ) => { if (textElement.isDeleted) { return; } - const container = getContainerElement(textElement); if (container) { - text = wrapTextElement(textElement, getBoundTextMaxWidth(container), { - text, - }); + text = wrapTextElement( + textElement, + getBoundTextMaxWidth(container, textElement), + { + text, + }, + ); } const dimensions = getAdjustedDimensions(textElement, text); return { text, ...dimensions }; @@ -380,6 +381,7 @@ export const refreshTextDimensions = ( export const updateTextElement = ( textElement: ExcalidrawTextElement, + container: ExcalidrawTextContainer | null, { text, isDeleted, @@ -393,7 +395,7 @@ export const updateTextElement = ( return newElementWith(textElement, { originalText, isDeleted: isDeleted ?? textElement.isDeleted, - ...refreshTextDimensions(textElement, originalText), + ...refreshTextDimensions(textElement, container, originalText), }); }; diff --git a/src/element/resizeElements.ts b/packages/excalidraw/element/resizeElements.ts similarity index 94% rename from src/element/resizeElements.ts rename to packages/excalidraw/element/resizeElements.ts index 38182f969..3e173e82f 100644 --- a/src/element/resizeElements.ts +++ b/packages/excalidraw/element/resizeElements.ts @@ -15,6 +15,7 @@ import { ExcalidrawElement, ExcalidrawTextElementWithContainer, ExcalidrawImageElement, + ElementsMap, } from "./types"; import type { Mutable } from "../utility-types"; import { @@ -41,7 +42,7 @@ import { MaybeTransformHandleType, TransformHandleDirection, } from "./transformHandles"; -import { AppState, Point, PointerDownState } from "../types"; +import { Point, PointerDownState } from "../types"; import Scene from "../scene/Scene"; import { getApproxMinLineWidth, @@ -68,10 +69,10 @@ export const normalizeAngle = (angle: number): number => { // Returns true when transform (resizing/rotation) happened export const transformElements = ( - pointerDownState: PointerDownState, + originalElements: PointerDownState["originalElements"], transformHandleType: MaybeTransformHandleType, selectedElements: readonly NonDeletedExcalidrawElement[], - resizeArrowDirection: "origin" | "end", + elementsMap: ElementsMap, shouldRotateWithDiscreteAngle: boolean, shouldResizeFromCenter: boolean, shouldMaintainAspectRatio: boolean, @@ -79,7 +80,6 @@ export const transformElements = ( pointerY: number, centerX: number, centerY: number, - appState: AppState, ) => { if (selectedElements.length === 1) { const [element] = selectedElements; @@ -89,7 +89,6 @@ export const transformElements = ( pointerX, pointerY, shouldRotateWithDiscreteAngle, - pointerDownState.originalElements, ); updateBoundElements(element); } else if ( @@ -101,6 +100,7 @@ export const transformElements = ( ) { resizeSingleTextElement( element, + elementsMap, transformHandleType, shouldResizeFromCenter, pointerX, @@ -109,9 +109,10 @@ export const transformElements = ( updateBoundElements(element); } else if (transformHandleType) { resizeSingleElement( - pointerDownState.originalElements, + originalElements, shouldMaintainAspectRatio, element, + elementsMap, transformHandleType, shouldResizeFromCenter, pointerX, @@ -123,8 +124,9 @@ export const transformElements = ( } else if (selectedElements.length > 1) { if (transformHandleType === "rotation") { rotateMultipleElements( - pointerDownState, + originalElements, selectedElements, + elementsMap, pointerX, pointerY, shouldRotateWithDiscreteAngle, @@ -139,8 +141,9 @@ export const transformElements = ( transformHandleType === "se" ) { resizeMultipleElements( - pointerDownState, + originalElements, selectedElements, + elementsMap, transformHandleType, shouldResizeFromCenter, pointerX, @@ -157,7 +160,6 @@ const rotateSingleElement = ( pointerX: number, pointerY: number, shouldRotateWithDiscreteAngle: boolean, - originalElements: Map>, ) => { const [x1, y1, x2, y2] = getElementAbsoluteCoords(element); const cx = (x1 + x2) / 2; @@ -207,6 +209,7 @@ const rescalePointsInElement = ( const measureFontSizeFromWidth = ( element: NonDeleted, + elementsMap: ElementsMap, nextWidth: number, nextHeight: number, ): { size: number; baseline: number } | null => { @@ -215,9 +218,9 @@ const measureFontSizeFromWidth = ( const hasContainer = isBoundToContainer(element); if (hasContainer) { - const container = getContainerElement(element); + const container = getContainerElement(element, elementsMap); if (container) { - width = getBoundTextMaxWidth(container); + width = getBoundTextMaxWidth(container, element); } } const nextFontSize = element.fontSize * (nextWidth / width); @@ -253,6 +256,7 @@ const getSidesForTransformHandle = ( const resizeSingleTextElement = ( element: NonDeleted, + elementsMap: ElementsMap, transformHandleType: "nw" | "ne" | "sw" | "se", shouldResizeFromCenter: boolean, pointerX: number, @@ -299,7 +303,12 @@ const resizeSingleTextElement = ( if (scale > 0) { const nextWidth = element.width * scale; const nextHeight = element.height * scale; - const metrics = measureFontSizeFromWidth(element, nextWidth, nextHeight); + const metrics = measureFontSizeFromWidth( + element, + elementsMap, + nextWidth, + nextHeight, + ); if (metrics === null) { return; } @@ -338,6 +347,7 @@ export const resizeSingleElement = ( originalElements: PointerDownState["originalElements"], shouldMaintainAspectRatio: boolean, element: NonDeletedExcalidrawElement, + elementsMap: ElementsMap, transformHandleDirection: TransformHandleDirection, shouldResizeFromCenter: boolean, pointerX: number, @@ -381,7 +391,7 @@ export const resizeSingleElement = ( let scaleY = atStartBoundsHeight / boundsCurrentHeight; let boundTextFont: { fontSize?: number; baseline?: number } = {}; - const boundTextElement = getBoundTextElement(element); + const boundTextElement = getBoundTextElement(element, elementsMap); if (transformHandleDirection.includes("e")) { scaleX = (rotatedPointer[0] - startTopLeft[0]) / boundsCurrentWidth; @@ -444,7 +454,8 @@ export const resizeSingleElement = ( const nextFont = measureFontSizeFromWidth( boundTextElement, - getBoundTextMaxWidth(updatedElement), + elementsMap, + getBoundTextMaxWidth(updatedElement, boundTextElement), getBoundTextMaxHeight(updatedElement, boundTextElement), ); if (nextFont === null) { @@ -626,6 +637,7 @@ export const resizeSingleElement = ( } handleBindTextResize( element, + elementsMap, transformHandleDirection, shouldMaintainAspectRatio, ); @@ -633,8 +645,9 @@ export const resizeSingleElement = ( }; export const resizeMultipleElements = ( - pointerDownState: PointerDownState, + originalElements: PointerDownState["originalElements"], selectedElements: readonly NonDeletedExcalidrawElement[], + elementsMap: ElementsMap, transformHandleType: "nw" | "ne" | "sw" | "se", shouldResizeFromCenter: boolean, pointerX: number, @@ -654,7 +667,7 @@ export const resizeMultipleElements = ( }[], element, ) => { - const origElement = pointerDownState.originalElements.get(element.id); + const origElement = originalElements.get(element.id); if (origElement) { acc.push({ orig: origElement, latest: element }); } @@ -675,7 +688,7 @@ export const resizeMultipleElements = ( if (!textId) { return acc; } - const text = pointerDownState.originalElements.get(textId) ?? null; + const text = originalElements.get(textId) ?? null; if (!isBoundToContainer(text)) { return acc; } @@ -821,7 +834,12 @@ export const resizeMultipleElements = ( } if (isTextElement(orig)) { - const metrics = measureFontSizeFromWidth(orig, width, height); + const metrics = measureFontSizeFromWidth( + orig, + elementsMap, + width, + height, + ); if (!metrics) { return; } @@ -829,7 +847,7 @@ export const resizeMultipleElements = ( update.baseline = metrics.baseline; } - const boundTextElement = pointerDownState.originalElements.get( + const boundTextElement = originalElements.get( getBoundTextElementId(orig) ?? "", ) as ExcalidrawTextElementWithContainer | undefined; @@ -862,7 +880,7 @@ export const resizeMultipleElements = ( newSize: { width, height }, }); - const boundTextElement = getBoundTextElement(element); + const boundTextElement = getBoundTextElement(element, elementsMap); if (boundTextElement && boundTextFontSize) { mutateElement( boundTextElement, @@ -872,7 +890,7 @@ export const resizeMultipleElements = ( }, false, ); - handleBindTextResize(element, transformHandleType, true); + handleBindTextResize(element, elementsMap, transformHandleType, true); } } @@ -880,8 +898,9 @@ export const resizeMultipleElements = ( }; const rotateMultipleElements = ( - pointerDownState: PointerDownState, + originalElements: PointerDownState["originalElements"], elements: readonly NonDeletedExcalidrawElement[], + elementsMap: ElementsMap, pointerX: number, pointerY: number, shouldRotateWithDiscreteAngle: boolean, @@ -902,8 +921,7 @@ const rotateMultipleElements = ( const cx = (x1 + x2) / 2; const cy = (y1 + y2) / 2; const origAngle = - pointerDownState.originalElements.get(element.id)?.angle ?? - element.angle; + originalElements.get(element.id)?.angle ?? element.angle; const [rotatedCX, rotatedCY] = rotate( cx, cy, @@ -922,7 +940,7 @@ const rotateMultipleElements = ( ); updateBoundElements(element, { simultaneouslyUpdated: elements }); - const boundText = getBoundTextElement(element); + const boundText = getBoundTextElement(element, elementsMap); if (boundText && !isArrowElement(element)) { mutateElement( boundText, diff --git a/src/element/resizeTest.ts b/packages/excalidraw/element/resizeTest.ts similarity index 100% rename from src/element/resizeTest.ts rename to packages/excalidraw/element/resizeTest.ts diff --git a/src/element/showSelectedShapeActions.ts b/packages/excalidraw/element/showSelectedShapeActions.ts similarity index 100% rename from src/element/showSelectedShapeActions.ts rename to packages/excalidraw/element/showSelectedShapeActions.ts diff --git a/src/element/sizeHelpers.test.ts b/packages/excalidraw/element/sizeHelpers.test.ts similarity index 100% rename from src/element/sizeHelpers.test.ts rename to packages/excalidraw/element/sizeHelpers.test.ts diff --git a/src/element/sizeHelpers.ts b/packages/excalidraw/element/sizeHelpers.ts similarity index 100% rename from src/element/sizeHelpers.ts rename to packages/excalidraw/element/sizeHelpers.ts diff --git a/src/element/sortElements.test.ts b/packages/excalidraw/element/sortElements.test.ts similarity index 100% rename from src/element/sortElements.test.ts rename to packages/excalidraw/element/sortElements.test.ts diff --git a/src/element/sortElements.ts b/packages/excalidraw/element/sortElements.ts similarity index 100% rename from src/element/sortElements.ts rename to packages/excalidraw/element/sortElements.ts diff --git a/src/element/subtypes/index.ts b/packages/excalidraw/element/subtypes/index.ts similarity index 91% rename from src/element/subtypes/index.ts rename to packages/excalidraw/element/subtypes/index.ts index e8f4a0130..534ead329 100644 --- a/src/element/subtypes/index.ts +++ b/packages/excalidraw/element/subtypes/index.ts @@ -1,5 +1,10 @@ import { useEffect } from "react"; -import { ExcalidrawElement, ExcalidrawTextElement, NonDeleted } from "../types"; +import { + ElementsMap, + ExcalidrawElement, + ExcalidrawTextElement, + NonDeleted, +} from "../types"; import { getNonDeletedElements } from "../"; import { getSelectedElements } from "../../scene"; import { AppState, ExcalidrawImperativeAPI, ToolType } from "../../types"; @@ -22,6 +27,7 @@ import { } from "../textElement"; import { ShapeCache } from "../../scene/ShapeCache"; import Scene from "../../scene/Scene"; +import { RenderableElementsMap } from "../../scene/types"; // Use "let" instead of "const" so we can dynamically add subtypes let subtypeNames: readonly Subtype[] = []; @@ -113,6 +119,7 @@ export const subtypeActionPredicate: ActionPredicateFn = function ( action, elements, appState, + app, ) { // We always enable subtype actions. Also let through standard actions // which no subtypes might have disabled. @@ -135,7 +142,9 @@ export const subtypeActionPredicate: ActionPredicateFn = function ( // its subtype? return ( chosen.some((el) => { - const e = hasBoundTextElement(el) ? getBoundTextElement(el)! : el; + const e = hasBoundTextElement(el) + ? getBoundTextElement(el, app.scene.getElementsMapIncludingDeleted())! + : el; return isForSubtype(e.subtype, action.name, true); }) || // Or has any active subtype enabled this actionName? @@ -156,7 +165,9 @@ export const subtypeActionPredicate: ActionPredicateFn = function ( return ( // Has every ExcalidrawElement not disabled this actionName? (chosen.every((el) => { - const e = hasBoundTextElement(el) ? getBoundTextElement(el)! : el; + const e = hasBoundTextElement(el) + ? getBoundTextElement(el, app.scene.getElementsMapIncludingDeleted())! + : el; return !isForSubtype(e.subtype, action.name, false); }) && // And has every active subtype not disabled this actionName? @@ -170,7 +181,9 @@ export const subtypeActionPredicate: ActionPredicateFn = function ( // Or can we find an ExcalidrawElement without a valid subtype // which would disable this action if it had a valid subtype? chosen.some((el) => { - const e = hasBoundTextElement(el) ? getBoundTextElement(el)! : el; + const e = hasBoundTextElement(el) + ? getBoundTextElement(el, app.scene.getElementsMapIncludingDeleted())! + : el; return parentTypeMap.some( (value) => value.parentType === e.type && @@ -179,14 +192,21 @@ export const subtypeActionPredicate: ActionPredicateFn = function ( ); }) || chosen.some((el) => { - const e = hasBoundTextElement(el) ? getBoundTextElement(el)! : el; + const e = hasBoundTextElement(el) + ? getBoundTextElement(el, app.scene.getElementsMapIncludingDeleted())! + : el; return ( // Would the subtype of e by inself disable this action? isForSubtype(e.subtype, action.name, false) && // Can we find an ExcalidrawElement which could have the same subtype // as e but whose subtype does not disable this action? chosen.some((el) => { - const e2 = hasBoundTextElement(el) ? getBoundTextElement(el)! : el; + const e2 = hasBoundTextElement(el) + ? getBoundTextElement( + el, + app.scene.getElementsMapIncludingDeleted(), + )! + : el; return ( // Does e have a valid subtype whose parent types include the // type of e2, and does the subtype of e2 not disable this action? @@ -246,12 +266,14 @@ export type SubtypeMethods = { ) => { width: number; height: number; baseline: number }; render: ( element: NonDeleted, + elementsMap: RenderableElementsMap, context: CanvasRenderingContext2D, ) => void; renderSvg: ( svgRoot: SVGElement, addToRoot: (node: SVGElement, element: ExcalidrawElement) => void, element: NonDeleted, + elementsMap: RenderableElementsMap, opt?: { offsetX?: number; offsetY?: number }, ) => void; wrapText: ( @@ -346,7 +368,7 @@ export const prepareSubtype = ( record: SubtypeRecord, subtypePrepFn: SubtypePrepFn, onSubtypeLoaded?: SubtypeLoadedCb, -): { actions: Action[] | null; methods: Partial } => { +): { actions: readonly Action[] | null; methods: Partial } => { const map = getSubtypeMethods(record.subtype); if (map) { return { actions: null, methods: map }; @@ -465,6 +487,12 @@ export const checkRefreshOnSubtypeLoad = ( hasSubtype: SubtypeCheckFn, elements: readonly ExcalidrawElement[], ) => { + const elementsMap = new Map() as ElementsMap; + for (const element of elements) { + if (!element.isDeleted) { + elementsMap.set(element.id, element); + } + } let refreshNeeded = false; const scenes: Scene[] = []; getNonDeletedElements(elements).forEach((element) => { @@ -474,7 +502,10 @@ export const checkRefreshOnSubtypeLoad = ( if (hasSubtype(element)) { ShapeCache.delete(element); if (isTextElement(element)) { - redrawTextBoundingBox(element, getContainerElement(element)); + redrawTextBoundingBox( + element, + getContainerElement(element, elementsMap), + ); } refreshNeeded = true; const scene = Scene.getScene(element); diff --git a/src/element/subtypes/mathjax/icon.tsx b/packages/excalidraw/element/subtypes/mathjax/icon.tsx similarity index 100% rename from src/element/subtypes/mathjax/icon.tsx rename to packages/excalidraw/element/subtypes/mathjax/icon.tsx diff --git a/src/element/subtypes/mathjax/implementation.tsx b/packages/excalidraw/element/subtypes/mathjax/implementation.tsx similarity index 95% rename from src/element/subtypes/mathjax/implementation.tsx rename to packages/excalidraw/element/subtypes/mathjax/implementation.tsx index 24d7f8a6a..eea82b4e8 100644 --- a/src/element/subtypes/mathjax/implementation.tsx +++ b/packages/excalidraw/element/subtypes/mathjax/implementation.tsx @@ -27,7 +27,7 @@ import Scene from "../../../scene/Scene"; // Imports for actions import { LangLdr, registerCustomLangData, t } from "../../../i18n"; import { Action, makeCustomActionName } from "../../../actions/types"; -import { AppState } from "../../../types"; +import { AppClassProperties, AppState } from "../../../types"; import { changeProperty, getFormValue, @@ -854,6 +854,7 @@ const getImageMetrics = ( const getSelectedMathElements = ( elements: readonly ExcalidrawElement[], appState: Readonly, + app: AppClassProperties, ): NonDeleted[] => { const selectedElements = getSelectedElements( getNonDeletedElements(elements), @@ -866,7 +867,12 @@ const getSelectedMathElements = ( (element, index, eligibleElements) => isMathElement(element) || (hasBoundTextElement(element) && - isMathElement(getBoundTextElement(element))), + isMathElement( + getBoundTextElement( + element, + app.scene.getElementsMapIncludingDeleted(), + ), + )), ) as NonDeleted[]; return eligibleElements; }; @@ -935,7 +941,7 @@ const measureMathElement = function (element, next) { return metrics; } as SubtypeMethods["measureText"]; -const renderMathElement = function (element, context) { +const renderMathElement = function (element, elementMap, context) { ensureMathElement(element); const isMathJaxLoaded = mathJaxLoaded; const _element = element as NonDeleted; @@ -1042,8 +1048,10 @@ const renderMathElement = function (element, context) { context.restore(); } }; - const container = getContainerElement(_element); - const parentWidth = container ? getBoundTextMaxWidth(container) : undefined; + const container = getContainerElement(_element, elementMap); + const parentWidth = container + ? getBoundTextMaxWidth(container, _element) + : undefined; const offsetX = (_element.width - (container ? parentWidth! : _element.width)) * @@ -1065,7 +1073,13 @@ const renderMathElement = function (element, context) { context.restore(); } as SubtypeMethods["render"]; -const renderSvgMathElement = function (svgRoot, addToRoot, element, opt) { +const renderSvgMathElement = function ( + svgRoot, + addToRoot, + element, + elementsMap, + opt, +) { ensureMathElement(element); const isMathJaxLoaded = mathJaxLoaded; @@ -1119,8 +1133,10 @@ const renderSvgMathElement = function (svgRoot, addToRoot, element, opt) { } tempSvg.appendChild(groupNode); - const container = getContainerElement(_element); - const parentWidth = container ? getBoundTextMaxWidth(container) : undefined; + const container = getContainerElement(_element, elementsMap); + const parentWidth = container + ? getBoundTextMaxWidth(container, _element) + : undefined; const offsetX = (_element.width - (container ? parentWidth! : _element.width)) * @@ -1368,15 +1384,21 @@ const ensureMathJaxLoaded = async function (callback) { const enableActionChangeMathProps = ( elements: readonly ExcalidrawElement[], appState: AppState, + app: AppClassProperties, ) => { - const eligibleElements = getSelectedMathElements(elements, appState); + const eligibleElements = getSelectedMathElements(elements, appState, app); let enabled = false; eligibleElements.forEach((element) => { if ( isMathElement(element) || (hasBoundTextElement(element) && - isMathElement(getBoundTextElement(element))) + isMathElement( + getBoundTextElement( + element, + app.scene.getElementsMapIncludingDeleted(), + ), + )) ) { enabled = true; } @@ -1434,7 +1456,7 @@ const createMathActions = () => { }; const actionResetUseTex: Action = { name: makeCustomActionName("resetUseTex"), - perform: (elements, appState) => { + perform: (elements, appState, _, app) => { const useTex = getMathProps.getUseTex(appState); const modElements = changeProperty( elements, @@ -1454,7 +1476,13 @@ const createMathActions = () => { }), }, ); - redrawTextBoundingBox(newElement, getContainerElement(oldElement)); + redrawTextBoundingBox( + newElement, + getContainerElement( + oldElement, + app.scene.getElementsMapIncludingDeleted(), + ), + ); return newElement; } @@ -1470,11 +1498,16 @@ const createMathActions = () => { }, keyTest: (event) => event.shiftKey && event.code === "KeyR", contextItemLabel: "labels.resetUseTex", - predicate: (elements, appState) => { + predicate: (elements, appState, _, app) => { const useTex = getMathProps.getUseTex(appState); - const mathElements = getSelectedMathElements(elements, appState); + const mathElements = getSelectedMathElements(elements, appState, app); return mathElements.some((el) => { - const e = isMathElement(el) ? el : getBoundTextElement(el)!; + const e = isMathElement(el) + ? el + : getBoundTextElement( + el, + app.scene.getElementsMapIncludingDeleted(), + )!; return e.customData === undefined || e.customData.useTex !== useTex; }); }, @@ -1482,14 +1515,17 @@ const createMathActions = () => { }; const actionChangeMathOnly: Action = { name: makeCustomActionName("changeMathOnly"), - perform: (elements, appState, mathOnly: boolean | null) => { + perform: (elements, appState, mathOnly: boolean | null, app) => { if (mathOnly === null) { mathOnly = getFormValue( elements, appState, (element) => { const el = hasBoundTextElement(element) - ? getBoundTextElement(element) + ? getBoundTextElement( + element, + app.scene.getElementsMapIncludingDeleted(), + ) : element; return isMathElement(el) && el.customData?.mathOnly; }, @@ -1513,7 +1549,13 @@ const createMathActions = () => { oldElement, { customData }, ); - redrawTextBoundingBox(newElement, getContainerElement(oldElement)); + redrawTextBoundingBox( + newElement, + getContainerElement( + oldElement, + app.scene.getElementsMapIncludingDeleted(), + ), + ); return newElement; } @@ -1531,7 +1573,7 @@ const createMathActions = () => { commitToHistory: true, }; }, - PanelComponent: ({ elements, appState, updateData }) => { + PanelComponent: ({ elements, appState, updateData, app }) => { const textIcon = (text: string, selected: boolean) => { const color = selected ? "var(--button-color, var(--color-primary-darker))" @@ -1549,7 +1591,10 @@ const createMathActions = () => { appState, (element) => { const el = hasBoundTextElement(element) - ? getBoundTextElement(element) + ? getBoundTextElement( + element, + app.scene.getElementsMapIncludingDeleted(), + ) : element; return isMathElement(el) ? getMathProps.ensureMathProps(el.customData).mathOnly @@ -1582,7 +1627,8 @@ const createMathActions = () => { ); }, predicate: (...rest) => - rest[4] === undefined && enableActionChangeMathProps(rest[0], rest[1]), + rest[4] === undefined && + enableActionChangeMathProps(rest[0], rest[1], rest[3]), trackEvent: false, }; const actionMath = SubtypeButton(mathSubtype, "text", mathSubtypeIcon, "M"); diff --git a/src/element/subtypes/mathjax/index.ts b/packages/excalidraw/element/subtypes/mathjax/index.ts similarity index 100% rename from src/element/subtypes/mathjax/index.ts rename to packages/excalidraw/element/subtypes/mathjax/index.ts diff --git a/src/element/subtypes/mathjax/locales/en.json b/packages/excalidraw/element/subtypes/mathjax/locales/en.json similarity index 100% rename from src/element/subtypes/mathjax/locales/en.json rename to packages/excalidraw/element/subtypes/mathjax/locales/en.json diff --git a/src/element/subtypes/mathjax/tests/implementation.test.tsx b/packages/excalidraw/element/subtypes/mathjax/tests/implementation.test.tsx similarity index 97% rename from src/element/subtypes/mathjax/tests/implementation.test.tsx rename to packages/excalidraw/element/subtypes/mathjax/tests/implementation.test.tsx index 2cc1e53f0..e7399f4e6 100644 --- a/src/element/subtypes/mathjax/tests/implementation.test.tsx +++ b/packages/excalidraw/element/subtypes/mathjax/tests/implementation.test.tsx @@ -1,7 +1,7 @@ import { vi } from "vitest"; import { render } from "../../../../tests/test-utils"; import { API } from "../../../../tests/helpers/api"; -import { Excalidraw } from "../../../../packages/excalidraw/index"; +import { Excalidraw } from "../../../../index"; import { measureTextElement } from "../../../textElement"; import { ensureSubtypesLoaded } from "../../"; diff --git a/src/element/subtypes/mathjax/types.ts b/packages/excalidraw/element/subtypes/mathjax/types.ts similarity index 100% rename from src/element/subtypes/mathjax/types.ts rename to packages/excalidraw/element/subtypes/mathjax/types.ts diff --git a/src/element/textElement.test.ts b/packages/excalidraw/element/textElement.test.ts similarity index 98% rename from src/element/textElement.test.ts rename to packages/excalidraw/element/textElement.test.ts index b6221336d..2f3a2dcc7 100644 --- a/src/element/textElement.test.ts +++ b/packages/excalidraw/element/textElement.test.ts @@ -319,17 +319,17 @@ describe("Test measureText", () => { it("should return max width when container is rectangle", () => { const container = API.createElement({ type: "rectangle", ...params }); - expect(getBoundTextMaxWidth(container)).toBe(168); + expect(getBoundTextMaxWidth(container, null)).toBe(168); }); it("should return max width when container is ellipse", () => { const container = API.createElement({ type: "ellipse", ...params }); - expect(getBoundTextMaxWidth(container)).toBe(116); + expect(getBoundTextMaxWidth(container, null)).toBe(116); }); it("should return max width when container is diamond", () => { const container = API.createElement({ type: "diamond", ...params }); - expect(getBoundTextMaxWidth(container)).toBe(79); + expect(getBoundTextMaxWidth(container, null)).toBe(79); }); }); diff --git a/src/element/textElement.ts b/packages/excalidraw/element/textElement.ts similarity index 92% rename from src/element/textElement.ts rename to packages/excalidraw/element/textElement.ts index 980ad84cd..84cb5f1e0 100644 --- a/src/element/textElement.ts +++ b/packages/excalidraw/element/textElement.ts @@ -1,6 +1,7 @@ import { getSubtypeMethods, SubtypeMethods } from "./subtypes"; -import { getFontString, arrayToMap, isTestEnv } from "../utils"; +import { getFontString, arrayToMap, isTestEnv, normalizeEOL } from "../utils"; import { + ElementsMap, ExcalidrawElement, ExcalidrawElementType, ExcalidrawTextContainer, @@ -23,20 +24,20 @@ import { VERTICAL_ALIGN, } from "../constants"; import { MaybeTransformHandleType } from "./transformHandles"; -import Scene from "../scene/Scene"; import { isTextElement } from "."; import { isBoundToContainer, isArrowElement } from "./typeChecks"; import { LinearElementEditor } from "./linearElementEditor"; import { AppState } from "../types"; import { isTextBindableContainer } from "./typeChecks"; -import { getElementAbsoluteCoords } from "../element"; +import { getElementAbsoluteCoords } from "."; import { getSelectedElements } from "../scene"; import { isHittingElementNotConsideringBoundingBox } from "./collision"; + +import { ExtractSetType } from "../utility-types"; import { resetOriginalContainerCache, updateOriginalContainerCache, -} from "./textWysiwyg"; -import { ExtractSetType } from "../utility-types"; +} from "./containerCache"; export const measureTextElement = function (element, next) { const map = getSubtypeMethods(element.subtype); @@ -64,15 +65,13 @@ export const wrapTextElement = function (element, containerWidth, next) { export const normalizeText = (text: string) => { return ( - text + normalizeEOL(text) // replace tabs with spaces so they render and measure correctly .replace(/\t/g, " ") - // normalize newlines - .replace(/\r?\n|\r/g, "\n") ); }; -export const splitIntoLines = (text: string) => { +const splitIntoLines = (text: string) => { return normalizeText(text).split("\n"); }; @@ -117,7 +116,7 @@ export const redrawTextBoundingBox = ( container, textElement as ExcalidrawTextElementWithContainer, ); - const maxContainerWidth = getBoundTextMaxWidth(container); + const maxContainerWidth = getBoundTextMaxWidth(container, textElement); if (!isArrowElement(container) && metrics.height > maxContainerHeight) { const nextHeight = computeContainerDimensionForBoundText( @@ -190,6 +189,7 @@ export const bindTextToShapeAfterDuplication = ( export const handleBindTextResize = ( container: NonDeletedExcalidrawElement, + elementsMap: ElementsMap, transformHandleType: MaybeTransformHandleType, shouldMaintainAspectRatio = false, ) => { @@ -198,25 +198,17 @@ export const handleBindTextResize = ( return; } resetOriginalContainerCache(container.id); - let textElement = Scene.getScene(container)!.getElement( - boundTextElementId, - ) as ExcalidrawTextElement; + const textElement = getBoundTextElement(container, elementsMap); if (textElement && textElement.text) { if (!container) { return; } - textElement = Scene.getScene(container)!.getElement( - boundTextElementId, - ) as ExcalidrawTextElement; let text = textElement.text; let nextHeight = textElement.height; let nextWidth = textElement.width; - const maxWidth = getBoundTextMaxWidth(container); - const maxHeight = getBoundTextMaxHeight( - container, - textElement as ExcalidrawTextElementWithContainer, - ); + const maxWidth = getBoundTextMaxWidth(container, textElement); + const maxHeight = getBoundTextMaxHeight(container, textElement); let containerHeight = container.height; let nextBaseLine = textElement.baseline; if ( @@ -263,10 +255,7 @@ export const handleBindTextResize = ( if (!isArrowElement(container)) { mutateElement( textElement, - computeBoundTextPosition( - container, - textElement as ExcalidrawTextElementWithContainer, - ), + computeBoundTextPosition(container, textElement), ); } } @@ -284,7 +273,7 @@ export const computeBoundTextPosition = ( } const containerCoords = getContainerCoords(container); const maxContainerHeight = getBoundTextMaxHeight(container, boundTextElement); - const maxContainerWidth = getBoundTextMaxWidth(container); + const maxContainerWidth = getBoundTextMaxWidth(container, boundTextElement); let x; let y; @@ -687,33 +676,32 @@ export const getBoundTextElementId = (container: ExcalidrawElement | null) => { : null; }; -export const getBoundTextElement = (element: ExcalidrawElement | null) => { +export const getBoundTextElement = ( + element: ExcalidrawElement | null, + elementsMap: ElementsMap, +) => { if (!element) { return null; } const boundTextElementId = getBoundTextElementId(element); + if (boundTextElementId) { - return ( - (Scene.getScene(element)?.getElement( - boundTextElementId, - ) as ExcalidrawTextElementWithContainer) || null - ); + return (elementsMap.get(boundTextElementId) || + null) as ExcalidrawTextElementWithContainer | null; } return null; }; export const getContainerElement = ( - element: - | (ExcalidrawElement & { - containerId: ExcalidrawElement["id"] | null; - }) - | null, -) => { + element: ExcalidrawTextElement | null, + elementsMap: ElementsMap, +): ExcalidrawTextContainer | null => { if (!element) { return null; } if (element.containerId) { - return Scene.getScene(element)?.getElement(element.containerId) || null; + return (elementsMap.get(element.containerId) || + null) as ExcalidrawTextContainer | null; } return null; }; @@ -721,6 +709,7 @@ export const getContainerElement = ( export const getContainerCenter = ( container: ExcalidrawElement, appState: AppState, + elementsMap: ElementsMap, ) => { if (!isArrowElement(container)) { return { @@ -740,6 +729,7 @@ export const getContainerCenter = ( const index = container.points.length / 2 - 1; let midSegmentMidpoint = LinearElementEditor.getEditorMidPoints( container, + elementsMap, appState, )[index]; if (!midSegmentMidpoint) { @@ -773,28 +763,16 @@ export const getContainerCoords = (container: NonDeletedExcalidrawElement) => { }; }; -export const getTextElementAngle = (textElement: ExcalidrawTextElement) => { - const container = getContainerElement(textElement); +export const getTextElementAngle = ( + textElement: ExcalidrawTextElement, + container: ExcalidrawTextContainer | null, +) => { if (!container || isArrowElement(container)) { return textElement.angle; } return container.angle; }; -export const getBoundTextElementOffset = ( - boundTextElement: ExcalidrawTextElement | null, -) => { - const container = getContainerElement(boundTextElement); - if (!container || !boundTextElement) { - return 0; - } - if (isArrowElement(container)) { - return BOUND_TEXT_PADDING * 8; - } - - return BOUND_TEXT_PADDING; -}; - export const getBoundTextElementPosition = ( container: ExcalidrawElement, boundTextElement: ExcalidrawTextElementWithContainer, @@ -809,12 +787,12 @@ export const getBoundTextElementPosition = ( export const shouldAllowVerticalAlign = ( selectedElements: NonDeletedExcalidrawElement[], + elementsMap: ElementsMap, ) => { return selectedElements.some((element) => { - const hasBoundContainer = isBoundToContainer(element); - if (hasBoundContainer) { - const container = getContainerElement(element); - if (isTextElement(element) && isArrowElement(container)) { + if (isBoundToContainer(element)) { + const container = getContainerElement(element, elementsMap); + if (isArrowElement(container)) { return false; } return true; @@ -825,12 +803,12 @@ export const shouldAllowVerticalAlign = ( export const suppportsHorizontalAlign = ( selectedElements: NonDeletedExcalidrawElement[], + elementsMap: ElementsMap, ) => { return selectedElements.some((element) => { - const hasBoundContainer = isBoundToContainer(element); - if (hasBoundContainer) { - const container = getContainerElement(element); - if (isTextElement(element) && isArrowElement(container)) { + if (isBoundToContainer(element)) { + const container = getContainerElement(element, elementsMap); + if (isArrowElement(container)) { return false; } return true; @@ -911,9 +889,7 @@ export const computeContainerDimensionForBoundText = ( export const getBoundTextMaxWidth = ( container: ExcalidrawElement, - boundTextElement: ExcalidrawTextElement | null = getBoundTextElement( - container, - ), + boundTextElement: ExcalidrawTextElement | null, ) => { const { width } = container; if (isArrowElement(container)) { diff --git a/src/element/textWysiwyg.test.tsx b/packages/excalidraw/element/textWysiwyg.test.tsx similarity index 99% rename from src/element/textWysiwyg.test.tsx rename to packages/excalidraw/element/textWysiwyg.test.tsx index 2ad4db167..478fe5c1a 100644 --- a/src/element/textWysiwyg.test.tsx +++ b/packages/excalidraw/element/textWysiwyg.test.tsx @@ -1,5 +1,5 @@ import ReactDOM from "react-dom"; -import { Excalidraw } from "../packages/excalidraw/index"; +import { Excalidraw } from "../index"; import { GlobalTestState, render, screen } from "../tests/test-utils"; import { Keyboard, Pointer, UI } from "../tests/helpers/ui"; import { CODES, KEYS } from "../keys"; @@ -17,7 +17,7 @@ import { } from "./types"; import { API } from "../tests/helpers/api"; import { mutateElement } from "./mutateElement"; -import { getOriginalContainerHeightFromCache } from "./textWysiwyg"; +import { getOriginalContainerHeightFromCache } from "./containerCache"; import { getTextEditor, updateTextEditor } from "../tests/queries/dom"; // Unmount ReactDOM from root diff --git a/src/element/textWysiwyg.tsx b/packages/excalidraw/element/textWysiwyg.tsx similarity index 95% rename from src/element/textWysiwyg.tsx rename to packages/excalidraw/element/textWysiwyg.tsx index 6a682ec36..8262c28ba 100644 --- a/src/element/textWysiwyg.tsx +++ b/packages/excalidraw/element/textWysiwyg.tsx @@ -17,7 +17,6 @@ import { ExcalidrawLinearElement, ExcalidrawTextElementWithContainer, ExcalidrawTextElement, - ExcalidrawTextContainer, } from "./types"; import { AppState } from "../types"; import { bumpVersion, mutateElement } from "./mutateElement"; @@ -35,6 +34,7 @@ import { computeContainerDimensionForBoundText, detectLineHeight, computeBoundTextPosition, + getBoundTextElement, } from "./textElement"; import { actionDecreaseFontSize, @@ -45,6 +45,10 @@ import App from "../components/App"; import { LinearElementEditor } from "./linearElementEditor"; import { parseClipboard } from "../clipboard"; import { SubtypeMethods, getSubtypeMethods } from "./subtypes"; +import { + originalContainerCache, + updateOriginalContainerCache, +} from "./containerCache"; const getTransform = ( offsetX: number, @@ -69,38 +73,6 @@ const getTransform = ( return `translate(${translateX}px, ${translateY}px) scale(${zoom.value}) rotate(${degree}deg)${offset}`; }; -const originalContainerCache: { - [id: ExcalidrawTextContainer["id"]]: - | { - height: ExcalidrawTextContainer["height"]; - } - | undefined; -} = {}; - -export const updateOriginalContainerCache = ( - id: ExcalidrawTextContainer["id"], - height: ExcalidrawTextContainer["height"], -) => { - const data = - originalContainerCache[id] || (originalContainerCache[id] = { height }); - data.height = height; - return data; -}; - -export const resetOriginalContainerCache = ( - id: ExcalidrawTextContainer["id"], -) => { - if (originalContainerCache[id]) { - delete originalContainerCache[id]; - } -}; - -export const getOriginalContainerHeightFromCache = ( - id: ExcalidrawTextContainer["id"], -) => { - return originalContainerCache[id]?.height ?? null; -}; - const getEditorStyle = function (element) { const map = getSubtypeMethods(element.subtype); if (map?.getEditorStyle) { @@ -165,7 +137,10 @@ export const textWysiwyg = ({ if (updatedTextElement && isTextElement(updatedTextElement)) { let coordX = updatedTextElement.x; let coordY = updatedTextElement.y; - const container = getContainerElement(updatedTextElement); + const container = getContainerElement( + updatedTextElement, + app.scene.getElementsMapIncludingDeleted(), + ); let maxWidth = updatedTextElement.width; // Editing metrics @@ -174,7 +149,7 @@ export const textWysiwyg = ({ ? wrapText( updatedTextElement.originalText, getFontString(updatedTextElement), - getBoundTextMaxWidth(container), + getBoundTextMaxWidth(container, updatedTextElement), ) : updatedTextElement.originalText, getFontString(updatedTextElement), @@ -218,7 +193,8 @@ export const textWysiwyg = ({ } } - maxWidth = getBoundTextMaxWidth(container); + maxWidth = getBoundTextMaxWidth(container, updatedTextElement); + maxHeight = getBoundTextMaxHeight( container, updatedTextElement as ExcalidrawTextElementWithContainer, @@ -326,7 +302,7 @@ export const textWysiwyg = ({ offsetX, transformWidth, updatedTextElement.height, - getTextElementAngle(updatedTextElement), + getTextElementAngle(updatedTextElement, container), appState, maxWidth, editorMaxHeight, @@ -398,17 +374,24 @@ export const textWysiwyg = ({ if (!data) { return; } - const container = getContainerElement(element); + const container = getContainerElement( + element, + app.scene.getElementsMapIncludingDeleted(), + ); const font = getFontString({ fontSize: app.state.currentItemFontSize, fontFamily: app.state.currentItemFontFamily, }); if (container) { + const boundTextElement = getBoundTextElement( + container, + app.scene.getNonDeletedElementsMap(), + ); const wrappedText = wrapText( `${editable.value}${data}`, font, - getBoundTextMaxWidth(container), + getBoundTextMaxWidth(container, boundTextElement), ); const width = getTextWidth(wrappedText, font); editable.style.width = `${width}px`; @@ -578,7 +561,10 @@ export const textWysiwyg = ({ return; } let text = editable.value; - const container = getContainerElement(updateElement); + const container = getContainerElement( + updateElement, + app.scene.getElementsMapIncludingDeleted(), + ); if (container) { text = updateElement.text; diff --git a/src/element/transformHandles.ts b/packages/excalidraw/element/transformHandles.ts similarity index 96% rename from src/element/transformHandles.ts rename to packages/excalidraw/element/transformHandles.ts index 00ebfacfd..19c60a93f 100644 --- a/src/element/transformHandles.ts +++ b/packages/excalidraw/element/transformHandles.ts @@ -9,7 +9,7 @@ import { rotate } from "../math"; import { InteractiveCanvasAppState, Zoom } from "../types"; import { isTextElement } from "."; import { isFrameLikeElement, isLinearElement } from "./typeChecks"; -import { DEFAULT_SPACING } from "../renderer/renderScene"; +import { DEFAULT_TRANSFORM_HANDLE_SPACING } from "../constants"; export type TransformHandleDirection = | "n" @@ -106,7 +106,8 @@ export const getTransformHandlesFromCoords = ( const width = x2 - x1; const height = y2 - y1; const dashedLineMargin = margin / zoom.value; - const centeringOffset = (size - DEFAULT_SPACING * 2) / (2 * zoom.value); + const centeringOffset = + (size - DEFAULT_TRANSFORM_HANDLE_SPACING * 2) / (2 * zoom.value); const transformHandles: TransformHandles = { nw: omitSides.nw @@ -263,8 +264,8 @@ export const getTransformHandles = ( }; } const dashedLineMargin = isLinearElement(element) - ? DEFAULT_SPACING + 8 - : DEFAULT_SPACING; + ? DEFAULT_TRANSFORM_HANDLE_SPACING + 8 + : DEFAULT_TRANSFORM_HANDLE_SPACING; return getTransformHandlesFromCoords( getElementAbsoluteCoords(element, true), element.angle, diff --git a/src/element/typeChecks.test.ts b/packages/excalidraw/element/typeChecks.test.ts similarity index 100% rename from src/element/typeChecks.test.ts rename to packages/excalidraw/element/typeChecks.test.ts diff --git a/src/element/typeChecks.ts b/packages/excalidraw/element/typeChecks.ts similarity index 98% rename from src/element/typeChecks.ts rename to packages/excalidraw/element/typeChecks.ts index ef1bcd3db..7193e251b 100644 --- a/src/element/typeChecks.ts +++ b/packages/excalidraw/element/typeChecks.ts @@ -214,7 +214,10 @@ export const isBoundToContainer = ( }; export const isUsingAdaptiveRadius = (type: string) => - type === "rectangle" || type === "embeddable" || type === "iframe"; + type === "rectangle" || + type === "embeddable" || + type === "iframe" || + type === "image"; export const isUsingProportionalRadius = (type: string) => type === "line" || type === "arrow" || type === "diamond"; diff --git a/src/element/types.ts b/packages/excalidraw/element/types.ts similarity index 83% rename from src/element/types.ts rename to packages/excalidraw/element/types.ts index 92584e7e6..51334ca03 100644 --- a/src/element/types.ts +++ b/packages/excalidraw/element/types.ts @@ -6,7 +6,7 @@ import { THEME, VERTICAL_ALIGN, } from "../constants"; -import { MarkNonNullable, ValueOf } from "../utility-types"; +import { MakeBrand, MarkNonNullable, ValueOf } from "../utility-types"; import { MagicCacheData } from "../data/magic"; export type ChartType = "bar" | "line"; @@ -89,14 +89,6 @@ export type ExcalidrawEllipseElement = _ExcalidrawElementBase & { export type ExcalidrawEmbeddableElement = _ExcalidrawElementBase & Readonly<{ type: "embeddable"; - /** - * indicates whether the embeddable src (url) has been validated for rendering. - * null value indicates that the validation is pending. We reset the - * value on each restore (or url change) so that we can guarantee - * the validation came from a trusted source (the editor). Also because we - * may not have access to host-app supplied url validator during restore. - */ - validated: boolean | null; }>; export type ExcalidrawIframeElement = _ExcalidrawElementBase & @@ -113,7 +105,7 @@ export type ExcalidrawIframeLikeElement = export type IframeData = | { intrinsicSize: { w: number; h: number }; - warning?: string; + error?: Error; } & ( | { type: "video" | "generic"; link: string } | { type: "document"; srcdoc: (theme: Theme) => string } @@ -224,7 +216,16 @@ export type PointBinding = { gap: number; }; -export type Arrowhead = "arrow" | "bar" | "dot" | "triangle"; +export type Arrowhead = + | "arrow" + | "bar" + | "dot" // legacy. Do not use for new elements. + | "circle" + | "circle_outline" + | "triangle" + | "triangle_outline" + | "diamond" + | "diamond_outline"; export type ExcalidrawLinearElement = _ExcalidrawElementBase & Readonly<{ @@ -254,3 +255,41 @@ export type ExcalidrawFreeDrawElement = _ExcalidrawElementBase & export type FileId = string & { _brand: "FileId" }; export type ExcalidrawElementType = ExcalidrawElement["type"]; + +/** + * Map of excalidraw elements. + * Unspecified whether deleted or non-deleted. + * Can be a subset of Scene elements. + */ +export type ElementsMap = Map; + +/** + * Map of non-deleted elements. + * Can be a subset of Scene elements. + */ +export type NonDeletedElementsMap = Map< + ExcalidrawElement["id"], + NonDeletedExcalidrawElement +> & + MakeBrand<"NonDeletedElementsMap">; + +/** + * Map of all excalidraw Scene elements, including deleted. + * Not a subset. Use this type when you need access to current Scene elements. + */ +export type SceneElementsMap = Map & + MakeBrand<"SceneElementsMap">; + +/** + * Map of all non-deleted Scene elements. + * Not a subset. Use this type when you need access to current Scene elements. + */ +export type NonDeletedSceneElementsMap = Map< + ExcalidrawElement["id"], + NonDeletedExcalidrawElement +> & + MakeBrand<"NonDeletedSceneElementsMap">; + +export type ElementsMapOrArray = + | readonly ExcalidrawElement[] + | Readonly; diff --git a/src/emitter.ts b/packages/excalidraw/emitter.ts similarity index 54% rename from src/emitter.ts rename to packages/excalidraw/emitter.ts index 5b1cdd0a7..98e97ad46 100644 --- a/src/emitter.ts +++ b/packages/excalidraw/emitter.ts @@ -1,21 +1,16 @@ +import { UnsubscribeCallback } from "./types"; + type Subscriber = (...payload: T) => void; export class Emitter { public subscribers: Subscriber[] = []; - public value: T | undefined; - private updateOnChangeOnly: boolean; - - constructor(opts?: { initialState?: T; updateOnChangeOnly?: boolean }) { - this.updateOnChangeOnly = opts?.updateOnChangeOnly ?? false; - this.value = opts?.initialState; - } /** * Attaches subscriber * * @returns unsubscribe function */ - on(...handlers: Subscriber[] | Subscriber[][]) { + on(...handlers: Subscriber[] | Subscriber[][]): UnsubscribeCallback { const _handlers = handlers .flat() .filter((item) => typeof item === "function"); @@ -25,6 +20,17 @@ export class Emitter { return () => this.off(_handlers); } + once(...handlers: Subscriber[] | Subscriber[][]): UnsubscribeCallback { + const _handlers = handlers + .flat() + .filter((item) => typeof item === "function"); + + _handlers.push(() => detach()); + + const detach = this.on(..._handlers); + return detach; + } + off(...handlers: Subscriber[] | Subscriber[][]) { const _handlers = handlers.flat(); this.subscribers = this.subscribers.filter( @@ -32,16 +38,14 @@ export class Emitter { ); } - trigger(...payload: T): any[] { - if (this.updateOnChangeOnly && this.value === payload) { - return []; + trigger(...payload: T) { + for (const handler of this.subscribers) { + handler(...payload); } - this.value = payload; - return this.subscribers.map((handler) => handler(...payload)); + return this; } - destroy() { + clear() { this.subscribers = []; - this.value = undefined; } } diff --git a/src/packages/excalidraw/env.js b/packages/excalidraw/env.cjs similarity index 100% rename from src/packages/excalidraw/env.js rename to packages/excalidraw/env.cjs diff --git a/src/errors.ts b/packages/excalidraw/errors.ts similarity index 100% rename from src/errors.ts rename to packages/excalidraw/errors.ts diff --git a/src/frame.test.tsx b/packages/excalidraw/frame.test.tsx similarity index 99% rename from src/frame.test.tsx rename to packages/excalidraw/frame.test.tsx index 1e882d8d1..e37b28b21 100644 --- a/src/frame.test.tsx +++ b/packages/excalidraw/frame.test.tsx @@ -1,8 +1,5 @@ import { ExcalidrawElement } from "./element/types"; -import { - convertToExcalidrawElements, - Excalidraw, -} from "./packages/excalidraw/index"; +import { convertToExcalidrawElements, Excalidraw } from "./index"; import { API } from "./tests/helpers/api"; import { Keyboard, Pointer } from "./tests/helpers/ui"; import { render } from "./tests/test-utils"; diff --git a/src/frame.ts b/packages/excalidraw/frame.ts similarity index 76% rename from src/frame.ts rename to packages/excalidraw/frame.ts index ed14ae821..c4a5a259d 100644 --- a/src/frame.ts +++ b/packages/excalidraw/frame.ts @@ -4,6 +4,8 @@ import { isTextElement, } from "./element"; import { + ElementsMap, + ElementsMapOrArray, ExcalidrawElement, ExcalidrawFrameLikeElement, NonDeleted, @@ -21,8 +23,12 @@ import { getElementsWithinSelection, getSelectedElements } from "./scene"; import { getElementsInGroup, selectGroupsFromGivenElements } from "./groups"; import Scene, { ExcalidrawElementsIncludingDeleted } from "./scene/Scene"; import { getElementLineSegments } from "./element/bounds"; -import { doLineSegmentsIntersect } from "./packages/utils"; +import { + doLineSegmentsIntersect, + elementsOverlappingBBox, +} from "../utils/export"; import { isFrameElement, isFrameLikeElement } from "./element/typeChecks"; +import { ReadonlySetLike } from "./utility-types"; // --------------------------- Frame State ------------------------------------ export const bindElementsToFramesAfterDuplication = ( @@ -104,17 +110,16 @@ export const elementsAreInFrameBounds = ( elements: readonly ExcalidrawElement[], frame: ExcalidrawFrameLikeElement, ) => { - const [selectionX1, selectionY1, selectionX2, selectionY2] = - getElementAbsoluteCoords(frame); + const [frameX1, frameY1, frameX2, frameY2] = getElementAbsoluteCoords(frame); const [elementX1, elementY1, elementX2, elementY2] = getCommonBounds(elements); return ( - selectionX1 <= elementX1 && - selectionY1 <= elementY1 && - selectionX2 >= elementX2 && - selectionY2 >= elementY2 + frameX1 <= elementX1 && + frameY1 <= elementY1 && + frameX2 >= elementX2 && + frameY2 >= elementY2 ); }; @@ -209,9 +214,17 @@ export const groupByFrameLikes = (elements: readonly ExcalidrawElement[]) => { }; export const getFrameChildren = ( - allElements: ExcalidrawElementsIncludingDeleted, + allElements: ElementsMapOrArray, frameId: string, -) => allElements.filter((element) => element.frameId === frameId); +) => { + const frameChildren: ExcalidrawElement[] = []; + for (const element of allElements.values()) { + if (element.frameId === frameId) { + frameChildren.push(element); + } + } + return frameChildren; +}; export const getFrameLikeElements = ( allElements: ExcalidrawElementsIncludingDeleted, @@ -369,43 +382,107 @@ export const getContainingFrame = ( // --------------------------- Frame Operations ------------------------------- +/** */ +export const filterElementsEligibleAsFrameChildren = ( + elements: readonly ExcalidrawElement[], + frame: ExcalidrawFrameLikeElement, +) => { + const otherFrames = new Set(); + + elements = omitGroupsContainingFrameLikes(elements); + + for (const element of elements) { + if (isFrameLikeElement(element) && element.id !== frame.id) { + otherFrames.add(element.id); + } + } + + const processedGroups = new Set(); + + const eligibleElements: ExcalidrawElement[] = []; + + for (const element of elements) { + // don't add frames or their children + if ( + isFrameLikeElement(element) || + (element.frameId && otherFrames.has(element.frameId)) + ) { + continue; + } + + if (element.groupIds.length) { + const shallowestGroupId = element.groupIds.at(-1)!; + if (!processedGroups.has(shallowestGroupId)) { + processedGroups.add(shallowestGroupId); + const groupElements = getElementsInGroup(elements, shallowestGroupId); + if (groupElements.some((el) => elementOverlapsWithFrame(el, frame))) { + for (const child of groupElements) { + eligibleElements.push(child); + } + } + } + } else { + const overlaps = elementOverlapsWithFrame(element, frame); + if (overlaps) { + eligibleElements.push(element); + } + } + } + + return eligibleElements; +}; + /** * Retains (or repairs for target frame) the ordering invriant where children * elements come right before the parent frame: * [el, el, child, child, frame, el] + * + * @returns mutated allElements (same data structure) */ -export const addElementsToFrame = ( - allElements: ExcalidrawElementsIncludingDeleted, +export const addElementsToFrame = ( + allElements: T, elementsToAdd: NonDeletedExcalidrawElement[], frame: ExcalidrawFrameLikeElement, -) => { - const { currTargetFrameChildrenMap } = allElements.reduce( - (acc, element, index) => { - if (element.frameId === frame.id) { - acc.currTargetFrameChildrenMap.set(element.id, true); - } - return acc; - }, - { - currTargetFrameChildrenMap: new Map(), - }, - ); +): T => { + const elementsMap = arrayToMap(allElements); + const currTargetFrameChildrenMap = new Map(); + for (const element of allElements.values()) { + if (element.frameId === frame.id) { + currTargetFrameChildrenMap.set(element.id, true); + } + } const suppliedElementsToAddSet = new Set(elementsToAdd.map((el) => el.id)); const finalElementsToAdd: ExcalidrawElement[] = []; + const otherFrames = new Set(); + + for (const element of elementsToAdd) { + if (isFrameLikeElement(element) && element.id !== frame.id) { + otherFrames.add(element.id); + } + } + // - add bound text elements if not already in the array // - filter out elements that are already in the frame for (const element of omitGroupsContainingFrameLikes( allElements, elementsToAdd, )) { + // don't add frames or their children + if ( + isFrameLikeElement(element) || + (element.frameId && otherFrames.has(element.frameId)) + ) { + continue; + } + if (!currTargetFrameChildrenMap.has(element.id)) { finalElementsToAdd.push(element); } - const boundTextElement = getBoundTextElement(element); + const boundTextElement = getBoundTextElement(element, elementsMap); if ( boundTextElement && !suppliedElementsToAddSet.has(boundTextElement.id) && @@ -424,13 +501,13 @@ export const addElementsToFrame = ( false, ); } - return allElements.slice(); + + return allElements; }; export const removeElementsFromFrame = ( - allElements: ExcalidrawElementsIncludingDeleted, - elementsToRemove: NonDeletedExcalidrawElement[], - appState: AppState, + elementsToRemove: ReadonlySetLike, + elementsMap: ElementsMap, ) => { const _elementsToRemove = new Map< ExcalidrawElement["id"], @@ -449,7 +526,7 @@ export const removeElementsFromFrame = ( const arr = toRemoveElementsByFrame.get(element.frameId) || []; arr.push(element); - const boundTextElement = getBoundTextElement(element); + const boundTextElement = getBoundTextElement(element, elementsMap); if (boundTextElement) { _elementsToRemove.set(boundTextElement.id, boundTextElement); arr.push(boundTextElement); @@ -468,35 +545,35 @@ export const removeElementsFromFrame = ( false, ); } - - return allElements.slice(); }; -export const removeAllElementsFromFrame = ( - allElements: ExcalidrawElementsIncludingDeleted, +export const removeAllElementsFromFrame = ( + allElements: readonly T[], frame: ExcalidrawFrameLikeElement, - appState: AppState, ) => { const elementsInFrame = getFrameChildren(allElements, frame.id); - return removeElementsFromFrame(allElements, elementsInFrame, appState); + removeElementsFromFrame(elementsInFrame, arrayToMap(allElements)); + return allElements; }; -export const replaceAllElementsInFrame = ( - allElements: ExcalidrawElementsIncludingDeleted, +export const replaceAllElementsInFrame = ( + allElements: readonly T[], nextElementsInFrame: ExcalidrawElement[], frame: ExcalidrawFrameLikeElement, - appState: AppState, -) => { + app: AppClassProperties, +): T[] => { return addElementsToFrame( - removeAllElementsFromFrame(allElements, frame, appState), + removeAllElementsFromFrame(allElements, frame), nextElementsInFrame, frame, - ); + ).slice(); }; /** does not mutate elements, but returns new ones */ -export const updateFrameMembershipOfSelectedElements = ( - allElements: ExcalidrawElementsIncludingDeleted, +export const updateFrameMembershipOfSelectedElements = < + T extends ElementsMapOrArray, +>( + allElements: T, appState: AppState, app: AppClassProperties, ) => { @@ -521,19 +598,22 @@ export const updateFrameMembershipOfSelectedElements = ( const elementsToRemove = new Set(); + const elementsMap = arrayToMap(allElements); + elementsToFilter.forEach((element) => { if ( element.frameId && !isFrameLikeElement(element) && - !isElementInFrame(element, allElements, appState) + !isElementInFrame(element, elementsMap, appState) ) { elementsToRemove.add(element); } }); - return elementsToRemove.size > 0 - ? removeElementsFromFrame(allElements, [...elementsToRemove], appState) - : allElements; + if (elementsToRemove.size > 0) { + removeElementsFromFrame(elementsToRemove, elementsMap); + } + return allElements; }; /** @@ -541,14 +621,16 @@ export const updateFrameMembershipOfSelectedElements = ( * anywhere in the group tree */ export const omitGroupsContainingFrameLikes = ( - allElements: ExcalidrawElementsIncludingDeleted, + allElements: ElementsMapOrArray, /** subset of elements you want to filter. Optional perf optimization so we * don't have to filter all elements unnecessarily */ selectedElements?: readonly ExcalidrawElement[], ) => { const uniqueGroupIds = new Set(); - for (const el of selectedElements || allElements) { + const elements = selectedElements || allElements; + + for (const el of elements.values()) { const topMostGroupId = el.groupIds[el.groupIds.length - 1]; if (topMostGroupId) { uniqueGroupIds.add(topMostGroupId); @@ -566,9 +648,15 @@ export const omitGroupsContainingFrameLikes = ( } } - return (selectedElements || allElements).filter( - (el) => !rejectedGroupIds.has(el.groupIds[el.groupIds.length - 1]), - ); + const ret: ExcalidrawElement[] = []; + + for (const element of elements.values()) { + if (!rejectedGroupIds.has(element.groupIds[element.groupIds.length - 1])) { + ret.push(element); + } + } + + return ret; }; /** @@ -577,10 +665,11 @@ export const omitGroupsContainingFrameLikes = ( */ export const getTargetFrame = ( element: ExcalidrawElement, + elementsMap: ElementsMap, appState: StaticCanvasAppState, ) => { const _element = isTextElement(element) - ? getContainerElement(element) || element + ? getContainerElement(element, elementsMap) || element : element; return appState.selectedElementIds[_element.id] && @@ -593,12 +682,12 @@ export const getTargetFrame = ( // given an element, return if the element is in some frame export const isElementInFrame = ( element: ExcalidrawElement, - allElements: ExcalidrawElementsIncludingDeleted, + allElements: ElementsMap, appState: StaticCanvasAppState, ) => { - const frame = getTargetFrame(element, appState); + const frame = getTargetFrame(element, allElements, appState); const _element = isTextElement(element) - ? getContainerElement(element) || element + ? getContainerElement(element, allElements) || element : element; if (frame) { @@ -657,10 +746,26 @@ export const getFrameLikeTitle = ( element: ExcalidrawFrameLikeElement, frameIdx: number, ) => { - const existingName = element.name?.trim(); - if (existingName) { - return existingName; - } - // TODO name frames AI only is specific to AI frames - return isFrameElement(element) ? `Frame ${frameIdx}` : `AI Frame ${frameIdx}`; + // TODO name frames "AI" only if specific to AI frames + return element.name === null + ? isFrameElement(element) + ? `Frame ${frameIdx}` + : `AI Frame $${frameIdx}` + : element.name; +}; + +export const getElementsOverlappingFrame = ( + elements: readonly ExcalidrawElement[], + frame: ExcalidrawFrameLikeElement, +) => { + return ( + elementsOverlappingBBox({ + elements, + bounds: frame, + type: "overlap", + }) + // removes elements who are overlapping, but are in a different frame, + // and thus invisible in target frame + .filter((el) => !el.frameId || el.frameId === frame.id) + ); }; diff --git a/src/ga.ts b/packages/excalidraw/ga.ts similarity index 100% rename from src/ga.ts rename to packages/excalidraw/ga.ts diff --git a/src/gadirections.ts b/packages/excalidraw/gadirections.ts similarity index 100% rename from src/gadirections.ts rename to packages/excalidraw/gadirections.ts diff --git a/src/galines.ts b/packages/excalidraw/galines.ts similarity index 100% rename from src/galines.ts rename to packages/excalidraw/galines.ts diff --git a/src/gapoints.ts b/packages/excalidraw/gapoints.ts similarity index 100% rename from src/gapoints.ts rename to packages/excalidraw/gapoints.ts diff --git a/src/gatransforms.ts b/packages/excalidraw/gatransforms.ts similarity index 100% rename from src/gatransforms.ts rename to packages/excalidraw/gatransforms.ts diff --git a/src/gesture.ts b/packages/excalidraw/gesture.ts similarity index 100% rename from src/gesture.ts rename to packages/excalidraw/gesture.ts diff --git a/src/global.d.ts b/packages/excalidraw/global.d.ts similarity index 88% rename from src/global.d.ts rename to packages/excalidraw/global.d.ts index af79408ce..cd816b0cd 100644 --- a/src/global.d.ts +++ b/packages/excalidraw/global.d.ts @@ -1,16 +1,3 @@ -// eslint-disable-next-line @typescript-eslint/no-unused-vars -interface Document { - fonts?: { - ready?: Promise; - check?: (font: string, text?: string) => boolean; - load?: (font: string, text?: string) => Promise; - addEventListener?( - type: "loading" | "loadingdone" | "loadingerror", - listener: (this: Document, ev: Event) => any, - ): void; - }; -} - interface Window { ClipboardItem: any; __EXCALIDRAW_SHA__: string | undefined; diff --git a/src/groups.ts b/packages/excalidraw/groups.ts similarity index 96% rename from src/groups.ts rename to packages/excalidraw/groups.ts index dd5512ba1..f8c0eddb9 100644 --- a/src/groups.ts +++ b/packages/excalidraw/groups.ts @@ -3,6 +3,8 @@ import { ExcalidrawElement, NonDeleted, NonDeletedExcalidrawElement, + ElementsMapOrArray, + ElementsMap, } from "./element/types"; import { AppClassProperties, @@ -270,9 +272,17 @@ export const isElementInGroup = (element: ExcalidrawElement, groupId: string) => element.groupIds.includes(groupId); export const getElementsInGroup = ( - elements: readonly ExcalidrawElement[], + elements: ElementsMapOrArray, groupId: string, -) => elements.filter((element) => isElementInGroup(element, groupId)); +) => { + const elementsInGroup: ExcalidrawElement[] = []; + for (const element of elements.values()) { + if (isElementInGroup(element, groupId)) { + elementsInGroup.push(element); + } + } + return elementsInGroup; +}; export const getSelectedGroupIdForElement = ( element: ExcalidrawElement, @@ -320,12 +330,12 @@ export const removeFromSelectedGroups = ( export const getMaximumGroups = ( elements: ExcalidrawElement[], + elementsMap: ElementsMap, ): ExcalidrawElement[][] => { const groups: Map = new Map< String, ExcalidrawElement[] >(); - elements.forEach((element: ExcalidrawElement) => { const groupId = element.groupIds.length === 0 @@ -335,7 +345,7 @@ export const getMaximumGroups = ( const currentGroupMembers = groups.get(groupId) || []; // Include bound text if present when grouping - const boundTextElement = getBoundTextElement(element); + const boundTextElement = getBoundTextElement(element, elementsMap); if (boundTextElement) { currentGroupMembers.push(boundTextElement); } diff --git a/src/history.ts b/packages/excalidraw/history.ts similarity index 100% rename from src/history.ts rename to packages/excalidraw/history.ts diff --git a/src/hooks/useCallbackRefState.ts b/packages/excalidraw/hooks/useCallbackRefState.ts similarity index 100% rename from src/hooks/useCallbackRefState.ts rename to packages/excalidraw/hooks/useCallbackRefState.ts diff --git a/src/hooks/useCreatePortalContainer.ts b/packages/excalidraw/hooks/useCreatePortalContainer.ts similarity index 100% rename from src/hooks/useCreatePortalContainer.ts rename to packages/excalidraw/hooks/useCreatePortalContainer.ts diff --git a/src/hooks/useLibraryItemSvg.ts b/packages/excalidraw/hooks/useLibraryItemSvg.ts similarity index 97% rename from src/hooks/useLibraryItemSvg.ts rename to packages/excalidraw/hooks/useLibraryItemSvg.ts index d6de4a0c4..ac40140b4 100644 --- a/src/hooks/useLibraryItemSvg.ts +++ b/packages/excalidraw/hooks/useLibraryItemSvg.ts @@ -2,7 +2,7 @@ import { atom, useAtom } from "jotai"; import { useEffect, useState } from "react"; import { COLOR_PALETTE } from "../colors"; import { jotaiScope } from "../jotai"; -import { exportToSvg } from "../packages/utils"; +import { exportToSvg } from "../../utils/export"; import { LibraryItem } from "../types"; export type SvgCache = Map; diff --git a/src/hooks/useOutsideClick.ts b/packages/excalidraw/hooks/useOutsideClick.ts similarity index 100% rename from src/hooks/useOutsideClick.ts rename to packages/excalidraw/hooks/useOutsideClick.ts diff --git a/src/hooks/useScrollPosition.ts b/packages/excalidraw/hooks/useScrollPosition.ts similarity index 100% rename from src/hooks/useScrollPosition.ts rename to packages/excalidraw/hooks/useScrollPosition.ts diff --git a/src/hooks/useStable.ts b/packages/excalidraw/hooks/useStable.ts similarity index 100% rename from src/hooks/useStable.ts rename to packages/excalidraw/hooks/useStable.ts diff --git a/src/hooks/useTransition.ts b/packages/excalidraw/hooks/useTransition.ts similarity index 100% rename from src/hooks/useTransition.ts rename to packages/excalidraw/hooks/useTransition.ts diff --git a/src/i18n.ts b/packages/excalidraw/i18n.ts similarity index 97% rename from src/i18n.ts rename to packages/excalidraw/i18n.ts index 82922b1ef..6b9102436 100644 --- a/src/i18n.ts +++ b/packages/excalidraw/i18n.ts @@ -107,9 +107,7 @@ export const setLanguage = async (lang: Language) => { currentLangData = {}; } else { try { - currentLangData = await import( - /* webpackChunkName: "locales/[request]" */ `./locales/${currentLang.code}.json` - ); + currentLangData = await import(`./locales/${currentLang.code}.json`); } catch (error: any) { console.error(`Failed to load language ${lang.code}:`, error.message); currentLangData = fallbackLangData; diff --git a/src/index-node.ts b/packages/excalidraw/index-node.ts similarity index 100% rename from src/index-node.ts rename to packages/excalidraw/index-node.ts diff --git a/src/packages/excalidraw/index.tsx b/packages/excalidraw/index.tsx similarity index 74% rename from src/packages/excalidraw/index.tsx rename to packages/excalidraw/index.tsx index 689efe614..f7be8affc 100644 --- a/src/packages/excalidraw/index.tsx +++ b/packages/excalidraw/index.tsx @@ -1,20 +1,24 @@ import React, { useEffect } from "react"; -import { InitializeApp } from "../../components/InitializeApp"; -import App from "../../components/App"; -import { isShallowEqual } from "../../utils"; +import { InitializeApp } from "./components/InitializeApp"; +import App from "./components/App"; +import { isShallowEqual } from "./utils"; -import "../../css/app.scss"; -import "../../css/styles.scss"; +import "./css/app.scss"; +import "./css/styles.scss"; +import "../../public/fonts/fonts.css"; +import polyfill from "./polyfill"; -import { AppProps, ExcalidrawProps } from "../../types"; -import { defaultLang } from "../../i18n"; -import { DEFAULT_UI_OPTIONS } from "../../constants"; +import { AppProps, ExcalidrawProps } from "./types"; +import { defaultLang } from "./i18n"; +import { DEFAULT_UI_OPTIONS } from "./constants"; import { Provider } from "jotai"; -import { jotaiScope, jotaiStore } from "../../jotai"; -import Footer from "../../components/footer/FooterCenter"; -import MainMenu from "../../components/main-menu/MainMenu"; -import WelcomeScreen from "../../components/welcome-screen/WelcomeScreen"; -import LiveCollaborationTrigger from "../../components/live-collaboration/LiveCollaborationTrigger"; +import { jotaiScope, jotaiStore } from "./jotai"; +import Footer from "./components/footer/FooterCenter"; +import MainMenu from "./components/main-menu/MainMenu"; +import WelcomeScreen from "./components/welcome-screen/WelcomeScreen"; +import LiveCollaborationTrigger from "./components/live-collaboration/LiveCollaborationTrigger"; + +polyfill(); const ExcalidrawBase = (props: ExcalidrawProps) => { const { @@ -40,6 +44,7 @@ const ExcalidrawBase = (props: ExcalidrawProps) => { generateIdForFile, onLinkOpen, onPointerDown, + onPointerUp, onScrollChange, children, validateEmbeddable, @@ -76,6 +81,13 @@ const ExcalidrawBase = (props: ExcalidrawProps) => { } useEffect(() => { + const importPolyfill = async () => { + //@ts-ignore + await import("canvas-roundrect-polyfill"); + }; + + importPolyfill(); + // Block pinch-zooming on iOS outside of the content area const handleTouchMove = (event: TouchEvent) => { // @ts-ignore @@ -120,6 +132,7 @@ const ExcalidrawBase = (props: ExcalidrawProps) => { generateIdForFile={generateIdForFile} onLinkOpen={onLinkOpen} onPointerDown={onPointerDown} + onPointerUp={onPointerUp} onScrollChange={onScrollChange} validateEmbeddable={validateEmbeddable} renderEmbeddable={renderEmbeddable} @@ -196,14 +209,14 @@ export { getSceneVersion, isInvisiblySmallElement, getNonDeletedElements, -} from "../../element"; -export { defaultLang, useI18n, languages } from "../../i18n"; +} from "./element"; +export { defaultLang, useI18n, languages } from "./i18n"; export { restore, restoreAppState, restoreElements, restoreLibraryItems, -} from "../../data/restore"; +} from "./data/restore"; export { exportToCanvas, exportToBlob, @@ -216,45 +229,43 @@ export { getFreeDrawSvgPath, exportToClipboard, mergeLibraryItems, -} from "../../packages/utils"; -export { isLinearElement } from "../../element/typeChecks"; +} from "../utils/export"; +export { isLinearElement } from "./element/typeChecks"; -export { FONT_FAMILY, THEME, MIME_TYPES } from "../../constants"; +export { FONT_FAMILY, THEME, MIME_TYPES, ROUNDNESS } from "./constants"; export { mutateElement, newElementWith, bumpVersion, -} from "../../element/mutateElement"; +} from "./element/mutateElement"; -export { - parseLibraryTokensFromUrl, - useHandleLibrary, -} from "../../data/library"; +export { parseLibraryTokensFromUrl, useHandleLibrary } from "./data/library"; export { sceneCoordsToViewportCoords, viewportCoordsToSceneCoords, -} from "../../utils"; +} from "./utils"; -export { Sidebar } from "../../components/Sidebar/Sidebar"; -export { Button } from "../../components/Button"; +export { Sidebar } from "./components/Sidebar/Sidebar"; +export { Button } from "./components/Button"; export { Footer }; export { MainMenu }; -export { useDevice } from "../../components/App"; +export { useDevice } from "./components/App"; export { WelcomeScreen }; export { LiveCollaborationTrigger }; -export { DefaultSidebar } from "../../components/DefaultSidebar"; -export { TTDDialog } from "../../components/TTDDialog/TTDDialog"; -export { TTDDialogTrigger } from "../../components/TTDDialog/TTDDialogTrigger"; +export { DefaultSidebar } from "./components/DefaultSidebar"; +export { TTDDialog } from "./components/TTDDialog/TTDDialog"; +export { TTDDialogTrigger } from "./components/TTDDialog/TTDDialogTrigger"; -export { normalizeLink } from "../../data/url"; -export { convertToExcalidrawElements } from "../../data/transform"; -export { getCommonBounds } from "../../element/bounds"; +export { normalizeLink } from "./data/url"; +export { zoomToFitBounds } from "./actions/actionCanvas"; +export { convertToExcalidrawElements } from "./data/transform"; +export { getCommonBounds, getVisibleSceneBounds } from "./element/bounds"; export { elementsOverlappingBBox, isElementInsideBBox, elementPartiallyOverlapsWithOrContainsBBox, -} from "../withinBounds"; +} from "../utils/export"; diff --git a/src/jotai.ts b/packages/excalidraw/jotai.ts similarity index 100% rename from src/jotai.ts rename to packages/excalidraw/jotai.ts diff --git a/src/keys.ts b/packages/excalidraw/keys.ts similarity index 100% rename from src/keys.ts rename to packages/excalidraw/keys.ts diff --git a/packages/excalidraw/laser-trails.ts b/packages/excalidraw/laser-trails.ts new file mode 100644 index 000000000..49a0de5be --- /dev/null +++ b/packages/excalidraw/laser-trails.ts @@ -0,0 +1,124 @@ +import { LaserPointerOptions } from "@excalidraw/laser-pointer"; +import { AnimatedTrail, Trail } from "./animated-trail"; +import { AnimationFrameHandler } from "./animation-frame-handler"; +import type App from "./components/App"; +import { SocketId } from "./types"; +import { easeOut } from "./utils"; +import { getClientColor } from "./clients"; + +export class LaserTrails implements Trail { + public localTrail: AnimatedTrail; + private collabTrails = new Map(); + + private container?: SVGSVGElement; + + constructor( + private animationFrameHandler: AnimationFrameHandler, + private app: App, + ) { + this.animationFrameHandler.register(this, this.onFrame.bind(this)); + + this.localTrail = new AnimatedTrail(animationFrameHandler, app, { + ...this.getTrailOptions(), + fill: () => "red", + }); + } + + private getTrailOptions() { + return { + simplify: 0, + streamline: 0.4, + sizeMapping: (c) => { + const DECAY_TIME = 1000; + const DECAY_LENGTH = 50; + const t = Math.max( + 0, + 1 - (performance.now() - c.pressure) / DECAY_TIME, + ); + const l = + (DECAY_LENGTH - + Math.min(DECAY_LENGTH, c.totalLength - c.currentIndex)) / + DECAY_LENGTH; + + return Math.min(easeOut(l), easeOut(t)); + }, + } as Partial; + } + + startPath(x: number, y: number): void { + this.localTrail.startPath(x, y); + } + + addPointToPath(x: number, y: number): void { + this.localTrail.addPointToPath(x, y); + } + + endPath(): void { + this.localTrail.endPath(); + } + + start(container: SVGSVGElement) { + this.container = container; + + this.animationFrameHandler.start(this); + this.localTrail.start(container); + } + + stop() { + this.animationFrameHandler.stop(this); + this.localTrail.stop(); + } + + onFrame() { + this.updateCollabTrails(); + } + + private updateCollabTrails() { + if (!this.container || this.app.state.collaborators.size === 0) { + return; + } + + for (const [key, collabolator] of this.app.state.collaborators.entries()) { + let trail!: AnimatedTrail; + + if (!this.collabTrails.has(key)) { + trail = new AnimatedTrail(this.animationFrameHandler, this.app, { + ...this.getTrailOptions(), + fill: () => getClientColor(key), + }); + trail.start(this.container); + + this.collabTrails.set(key, trail); + } else { + trail = this.collabTrails.get(key)!; + } + + if (collabolator.pointer && collabolator.pointer.tool === "laser") { + if (collabolator.button === "down" && !trail.hasCurrentTrail) { + trail.startPath(collabolator.pointer.x, collabolator.pointer.y); + } + + if ( + collabolator.button === "down" && + trail.hasCurrentTrail && + !trail.hasLastPoint(collabolator.pointer.x, collabolator.pointer.y) + ) { + trail.addPointToPath(collabolator.pointer.x, collabolator.pointer.y); + } + + if (collabolator.button === "up" && trail.hasCurrentTrail) { + trail.addPointToPath(collabolator.pointer.x, collabolator.pointer.y); + trail.endPath(); + } + } + } + + for (const key of this.collabTrails.keys()) { + if (!this.app.state.collaborators.has(key)) { + const trail = this.collabTrails.get(key)!; + trail.stop(); + this.collabTrails.delete(key); + } + } + } +} diff --git a/src/locales/README.md b/packages/excalidraw/locales/README.md similarity index 100% rename from src/locales/README.md rename to packages/excalidraw/locales/README.md diff --git a/src/locales/ar-SA.json b/packages/excalidraw/locales/ar-SA.json similarity index 97% rename from src/locales/ar-SA.json rename to packages/excalidraw/locales/ar-SA.json index 18742875f..5b0db36b2 100644 --- a/src/locales/ar-SA.json +++ b/packages/excalidraw/locales/ar-SA.json @@ -11,6 +11,8 @@ "copyAsPng": "نسخ إلى الحافظة بصيغة PNG", "copyAsSvg": "نسخ إلى الحافظة بصيغة SVG", "copyText": "نسخ إلى الحافظة كنص", + "copySource": "", + "convertToCode": "", "bringForward": "جلب للأمام", "sendToBack": "أرسل للخلف", "bringToFront": "أحضر للأمام", @@ -36,8 +38,12 @@ "arrowhead_none": "لا شيء", "arrowhead_arrow": "سهم", "arrowhead_bar": "شريط", - "arrowhead_dot": "نقطة", + "arrowhead_circle": "", + "arrowhead_circle_outline": "", "arrowhead_triangle": "مثلث", + "arrowhead_triangle_outline": "", + "arrowhead_diamond": "", + "arrowhead_diamond_outline": "", "fontSize": "حجم الخط", "fontFamily": "نوع الخط", "addWatermark": "إضافة \"مصنوعة بواسطة Excalidraw\"", @@ -130,7 +136,9 @@ "sidebarLock": "إبقاء الشريط الجانبي مفتوح", "selectAllElementsInFrame": "تحديد جميع العناصر في الإطار", "removeAllElementsFromFrame": "إزالة جميع العناصر من الإطار", - "eyeDropper": "اختيار اللون من القماش" + "eyeDropper": "اختيار اللون من القماش", + "textToDiagram": "", + "prompt": "" }, "library": { "noItems": "لا توجد عناصر أضيفت بعد...", @@ -209,6 +217,7 @@ "importLibraryError": "تعذر تحميل المكتبة", "collabSaveFailed": "تعذر الحفظ في قاعدة البيانات. إذا استمرت المشاكل، يفضل أن تحفظ ملفك محليا كي لا تفقد عملك.", "collabSaveFailed_sizeExceeded": "تعذر الحفظ في قاعدة البيانات، يبدو أن القماش كبير للغاية، يفضّل حفظ الملف محليا كي لا تفقد عملك.", + "imageToolNotSupported": "", "brave_measure_text_error": { "line1": "يبدو أنك تستخدم متصفح Brave مع إعداد حظر صارم لتتبع البصمة.", "line2": "قد يؤدي هذا إلى كسر عناصر النص في الرسومات الخاصة بك.", @@ -217,8 +226,12 @@ }, "libraryElementTypeError": { "embeddable": "لا يمكن إضافة العناصر القابلة للتضمين في المكتبة.", + "iframe": "", "image": "سوف يتم دعم إضافة صور إلى المكتبة قريباً!" - } + }, + "asyncPasteFailedOnRead": "", + "asyncPasteFailedOnParse": "", + "copyToSystemClipboardFailed": "" }, "toolBar": { "selection": "تحديد", @@ -236,10 +249,13 @@ "link": "إضافة/تحديث الرابط للشكل المحدد", "eraser": "ممحاة", "frame": "أداة الإطار", + "magicframe": "", "embeddable": "تضمين ويب", "laser": "مؤشر ليزر", "hand": "يد (أداة الإزاحة)", - "extraTools": "المزيد من أﻷدوات" + "extraTools": "المزيد من أﻷدوات", + "mermaidToExcalidraw": "", + "magicSettings": "" }, "headings": { "canvasActions": "إجراءات اللوحة", @@ -498,5 +514,12 @@ "description": "سيتسبب تحميل رسمة خارجية باستبدال محتواك الموجود حالياً.

بإمكانك إجراء النسخ الاحتياطي لرسمتك الحالية باستخدام أحد الخيارات أدناه." } } + }, + "mermaid": { + "title": "", + "button": "", + "description": "", + "syntax": "", + "preview": "" } } diff --git a/src/locales/az-AZ.json b/packages/excalidraw/locales/az-AZ.json similarity index 94% rename from src/locales/az-AZ.json rename to packages/excalidraw/locales/az-AZ.json index 1ece3cc98..145fc0ac5 100644 --- a/src/locales/az-AZ.json +++ b/packages/excalidraw/locales/az-AZ.json @@ -11,6 +11,8 @@ "copyAsPng": "PNG olaraq panoya kopyala", "copyAsSvg": "SVG olaraq panoya kopyala", "copyText": "Mətn olaraq panoya kopyala", + "copySource": "", + "convertToCode": "", "bringForward": "Önə daşı", "sendToBack": "Geriyə göndərin", "bringToFront": "Önə gətirin", @@ -36,8 +38,12 @@ "arrowhead_none": "Heç biri", "arrowhead_arrow": "Ox", "arrowhead_bar": "Çubuq", - "arrowhead_dot": "Nöqtə", + "arrowhead_circle": "", + "arrowhead_circle_outline": "", "arrowhead_triangle": "Üçbucaq", + "arrowhead_triangle_outline": "", + "arrowhead_diamond": "", + "arrowhead_diamond_outline": "", "fontSize": "Şrift ölçüsü", "fontFamily": "Şrift qrupu", "addWatermark": "\"Made with Excalidraw\" əlavə et", @@ -130,7 +136,9 @@ "sidebarLock": "", "selectAllElementsInFrame": "", "removeAllElementsFromFrame": "", - "eyeDropper": "" + "eyeDropper": "", + "textToDiagram": "", + "prompt": "" }, "library": { "noItems": "", @@ -209,6 +217,7 @@ "importLibraryError": "", "collabSaveFailed": "", "collabSaveFailed_sizeExceeded": "", + "imageToolNotSupported": "", "brave_measure_text_error": { "line1": "", "line2": "", @@ -217,8 +226,12 @@ }, "libraryElementTypeError": { "embeddable": "", + "iframe": "", "image": "" - } + }, + "asyncPasteFailedOnRead": "", + "asyncPasteFailedOnParse": "", + "copyToSystemClipboardFailed": "" }, "toolBar": { "selection": "", @@ -236,10 +249,13 @@ "link": "", "eraser": "", "frame": "", + "magicframe": "", "embeddable": "", "laser": "", "hand": "", - "extraTools": "" + "extraTools": "", + "mermaidToExcalidraw": "", + "magicSettings": "" }, "headings": { "canvasActions": "", @@ -498,5 +514,12 @@ "description": "" } } + }, + "mermaid": { + "title": "", + "button": "", + "description": "", + "syntax": "", + "preview": "" } } diff --git a/src/locales/bg-BG.json b/packages/excalidraw/locales/bg-BG.json similarity index 96% rename from src/locales/bg-BG.json rename to packages/excalidraw/locales/bg-BG.json index 716c73d54..d48f47982 100644 --- a/src/locales/bg-BG.json +++ b/packages/excalidraw/locales/bg-BG.json @@ -11,6 +11,8 @@ "copyAsPng": "Копиране в клипборда", "copyAsSvg": "Копирано в клипборда като SVG", "copyText": "", + "copySource": "", + "convertToCode": "", "bringForward": "Преместване напред", "sendToBack": "Изнасяне назад", "bringToFront": "Изнасяне отпред", @@ -36,8 +38,12 @@ "arrowhead_none": "Без", "arrowhead_arrow": "Стрелка", "arrowhead_bar": "Връх на стрелката", - "arrowhead_dot": "Точка", + "arrowhead_circle": "", + "arrowhead_circle_outline": "", "arrowhead_triangle": "Триъгълник", + "arrowhead_triangle_outline": "", + "arrowhead_diamond": "", + "arrowhead_diamond_outline": "", "fontSize": "Размер на шрифта", "fontFamily": "Семейство шрифтове", "addWatermark": "Добави \"Направено с Excalidraw\"", @@ -130,7 +136,9 @@ "sidebarLock": "", "selectAllElementsInFrame": "", "removeAllElementsFromFrame": "", - "eyeDropper": "Избери цвят от платното" + "eyeDropper": "Избери цвят от платното", + "textToDiagram": "", + "prompt": "" }, "library": { "noItems": "Няма добавени неща все още...", @@ -209,6 +217,7 @@ "importLibraryError": "Не можем да заредим библиотеката", "collabSaveFailed": "", "collabSaveFailed_sizeExceeded": "", + "imageToolNotSupported": "", "brave_measure_text_error": { "line1": "", "line2": "", @@ -217,8 +226,12 @@ }, "libraryElementTypeError": { "embeddable": "", + "iframe": "", "image": "" - } + }, + "asyncPasteFailedOnRead": "", + "asyncPasteFailedOnParse": "", + "copyToSystemClipboardFailed": "" }, "toolBar": { "selection": "Селекция", @@ -236,10 +249,13 @@ "link": "", "eraser": "Гума", "frame": "", + "magicframe": "", "embeddable": "", "laser": "", "hand": "", - "extraTools": "Още инструменти" + "extraTools": "Още инструменти", + "mermaidToExcalidraw": "", + "magicSettings": "" }, "headings": { "canvasActions": "Действия по платното", @@ -498,5 +514,12 @@ "description": "" } } + }, + "mermaid": { + "title": "", + "button": "", + "description": "", + "syntax": "", + "preview": "" } } diff --git a/src/locales/bn-BD.json b/packages/excalidraw/locales/bn-BD.json similarity index 97% rename from src/locales/bn-BD.json rename to packages/excalidraw/locales/bn-BD.json index a2b926890..9bb910e80 100644 --- a/src/locales/bn-BD.json +++ b/packages/excalidraw/locales/bn-BD.json @@ -11,6 +11,8 @@ "copyAsPng": "পীএনজী ছবির মতন কপি করুন", "copyAsSvg": "এসভীজী ছবির মতন কপি করুন", "copyText": "লিখিত তথ্যের মতন কপি করুন", + "copySource": "", + "convertToCode": "", "bringForward": "অধিকতর সামনে আনুন", "sendToBack": "অধিকতর পিছনে নিয়ে যান", "bringToFront": "সবার সামনে আনুন", @@ -36,8 +38,12 @@ "arrowhead_none": "কিছু না", "arrowhead_arrow": "তীর", "arrowhead_bar": "রেখাংশ", - "arrowhead_dot": "বিন্দু", + "arrowhead_circle": "", + "arrowhead_circle_outline": "", "arrowhead_triangle": "ত্রিভূজ", + "arrowhead_triangle_outline": "", + "arrowhead_diamond": "", + "arrowhead_diamond_outline": "", "fontSize": "লেখনীর মাত্রা", "fontFamily": "লেখনীর হরফ", "addWatermark": "এক্সক্যালিড্র দ্বারা প্রস্তুত", @@ -130,7 +136,9 @@ "sidebarLock": "লক", "selectAllElementsInFrame": "", "removeAllElementsFromFrame": "", - "eyeDropper": "" + "eyeDropper": "", + "textToDiagram": "", + "prompt": "" }, "library": { "noItems": "সংগ্রহে কিছু যোগ করা হয়নি", @@ -209,6 +217,7 @@ "importLibraryError": "সংগ্রহ লোড করা যায়নি", "collabSaveFailed": "", "collabSaveFailed_sizeExceeded": "", + "imageToolNotSupported": "", "brave_measure_text_error": { "line1": "", "line2": "", @@ -217,8 +226,12 @@ }, "libraryElementTypeError": { "embeddable": "", + "iframe": "", "image": "" - } + }, + "asyncPasteFailedOnRead": "", + "asyncPasteFailedOnParse": "", + "copyToSystemClipboardFailed": "" }, "toolBar": { "selection": "বাছাই", @@ -236,10 +249,13 @@ "link": "একটি নির্বাচিত আকৃতির জন্য লিঙ্ক যোগ বা আপডেট করুন", "eraser": "ঝাড়ন", "frame": "", + "magicframe": "", "embeddable": "", "laser": "", "hand": "", - "extraTools": "" + "extraTools": "", + "mermaidToExcalidraw": "", + "magicSettings": "" }, "headings": { "canvasActions": "ক্যানভাস কার্যকলাপ", @@ -498,5 +514,12 @@ "description": "" } } + }, + "mermaid": { + "title": "", + "button": "", + "description": "", + "syntax": "", + "preview": "" } } diff --git a/src/locales/ca-ES.json b/packages/excalidraw/locales/ca-ES.json similarity index 92% rename from src/locales/ca-ES.json rename to packages/excalidraw/locales/ca-ES.json index 1205838c4..4f601d93a 100644 --- a/src/locales/ca-ES.json +++ b/packages/excalidraw/locales/ca-ES.json @@ -11,6 +11,8 @@ "copyAsPng": "Copia al porta-retalls com a PNG", "copyAsSvg": "Copia al porta-retalls com a SVG", "copyText": "Copia al porta-retalls com a text", + "copySource": "Copia l'origen al porta-retalls", + "convertToCode": "", "bringForward": "Porta endavant", "sendToBack": "Envia enrere", "bringToFront": "Porta al davant", @@ -36,8 +38,12 @@ "arrowhead_none": "Cap", "arrowhead_arrow": "Fletxa", "arrowhead_bar": "Barra", - "arrowhead_dot": "Punt", + "arrowhead_circle": "", + "arrowhead_circle_outline": "", "arrowhead_triangle": "Triangle", + "arrowhead_triangle_outline": "", + "arrowhead_diamond": "", + "arrowhead_diamond_outline": "", "fontSize": "Mida de lletra", "fontFamily": "Tipus de lletra", "addWatermark": "Afegeix-hi «Fet amb Excalidraw»", @@ -109,12 +115,12 @@ "createContainerFromText": "", "link": { "edit": "Edita l'enllaç", - "editEmbed": "", + "editEmbed": "Edita l'enllaç i incrusta-ho", "create": "Crea un enllaç", "createEmbed": "", "label": "Enllaç", "labelEmbed": "", - "empty": "" + "empty": "No s'ha definit cap enllaç" }, "lineEditor": { "edit": "Editar línia", @@ -128,9 +134,11 @@ }, "statusPublished": "Publicat", "sidebarLock": "Manté la barra lateral oberta", - "selectAllElementsInFrame": "", - "removeAllElementsFromFrame": "", - "eyeDropper": "" + "selectAllElementsInFrame": "Selecciona tots els elements del marc", + "removeAllElementsFromFrame": "Eliminat tots els elements del marc", + "eyeDropper": "", + "textToDiagram": "", + "prompt": "" }, "library": { "noItems": "Encara no s'hi han afegit elements...", @@ -173,7 +181,7 @@ "publishLibrary": "Publica", "submit": "Envia", "confirm": "Confirma", - "embeddableInteractionButton": "" + "embeddableInteractionButton": "Feu clic per interactuar" }, "alerts": { "clearReset": "S'esborrarà tot el llenç. N'esteu segur?", @@ -209,6 +217,7 @@ "importLibraryError": "No s'ha pogut carregar la biblioteca", "collabSaveFailed": "No s'ha pogut desar a la base de dades de fons. Si els problemes persisteixen, hauríeu de desar el fitxer localment per assegurar-vos que no perdeu el vostre treball.", "collabSaveFailed_sizeExceeded": "No s'ha pogut desar a la base de dades de fons, sembla que el llenç és massa gran. Hauríeu de desar el fitxer localment per assegurar-vos que no perdeu el vostre treball.", + "imageToolNotSupported": "", "brave_measure_text_error": { "line1": "", "line2": "", @@ -217,8 +226,12 @@ }, "libraryElementTypeError": { "embeddable": "", + "iframe": "", "image": "" - } + }, + "asyncPasteFailedOnRead": "", + "asyncPasteFailedOnParse": "No s'ha pogut enganxar.", + "copyToSystemClipboardFailed": "" }, "toolBar": { "selection": "Selecció", @@ -236,10 +249,13 @@ "link": "Afegeix / actualitza l'enllaç per a la forma seleccionada", "eraser": "Esborrador", "frame": "", + "magicframe": "", "embeddable": "", "laser": "", "hand": "Mà (eina de desplaçament)", - "extraTools": "" + "extraTools": "", + "mermaidToExcalidraw": "De Mermaid a Excalidraw", + "magicSettings": "Preferències d'IA" }, "headings": { "canvasActions": "Accions del llenç", @@ -379,8 +395,8 @@ "header": "", "label": { "withBackground": "", - "onlySelected": "", - "darkMode": "", + "onlySelected": "Només els seleccionats", + "darkMode": "Mode fosc", "embedScene": "", "scale": "", "padding": "" @@ -389,12 +405,12 @@ "embedScene": "" }, "title": { - "exportToPng": "", - "exportToSvg": "", - "copyPngToClipboard": "" + "exportToPng": "Exporta a PNG", + "exportToSvg": "Exporta a SVG", + "copyPngToClipboard": "Copia el PNG al porta-retalls" }, "button": { - "exportToPng": "", + "exportToPng": "PNG", "exportToSvg": "", "copyPngToClipboard": "" } @@ -443,10 +459,10 @@ "blue": "", "cyan": "", "teal": "", - "green": "", - "yellow": "", - "orange": "", - "bronze": "" + "green": "Verd", + "yellow": "Groc", + "orange": "Taronja", + "bronze": "Bronze" }, "welcomeScreen": { "app": { @@ -471,13 +487,13 @@ "overwriteConfirm": { "action": { "exportToImage": { - "title": "", - "button": "", + "title": "Exporta com a imatge", + "button": "Exporta com a imatge", "description": "" }, "saveToDisk": { - "title": "", - "button": "", + "title": "Desa al disc", + "button": "Desa al disc", "description": "" }, "excalidrawPlus": { @@ -488,15 +504,22 @@ }, "modal": { "loadFromFile": { - "title": "", - "button": "", + "title": "Carrega des d'un fitxer", + "button": "Carrega des d'un fitxer", "description": "" }, "shareableLink": { - "title": "", + "title": "Carrega des d'un enllaç", "button": "", "description": "" } } + }, + "mermaid": { + "title": "De Mermaid a Excalidraw", + "button": "Insereix", + "description": "", + "syntax": "Sintaxi de Mermaid", + "preview": "Previsualització" } } diff --git a/src/locales/cs-CZ.json b/packages/excalidraw/locales/cs-CZ.json similarity index 97% rename from src/locales/cs-CZ.json rename to packages/excalidraw/locales/cs-CZ.json index 8d758e0da..150fecd3d 100644 --- a/src/locales/cs-CZ.json +++ b/packages/excalidraw/locales/cs-CZ.json @@ -11,6 +11,8 @@ "copyAsPng": "Zkopírovat do schránky jako PNG", "copyAsSvg": "Zkopírovat do schránky jako SVG", "copyText": "Zkopírovat do schránky jako text", + "copySource": "", + "convertToCode": "", "bringForward": "Přenést blíž", "sendToBack": "Přenést do pozadí", "bringToFront": "Přenést do popředí", @@ -36,8 +38,12 @@ "arrowhead_none": "Žádný", "arrowhead_arrow": "Šipka", "arrowhead_bar": "Kóta", - "arrowhead_dot": "Tečka", + "arrowhead_circle": "", + "arrowhead_circle_outline": "", "arrowhead_triangle": "Trojúhelník", + "arrowhead_triangle_outline": "", + "arrowhead_diamond": "", + "arrowhead_diamond_outline": "", "fontSize": "Velikost písma", "fontFamily": "Písmo", "addWatermark": "Přidat \"Vyrobeno s Excalidraw\"", @@ -130,7 +136,9 @@ "sidebarLock": "Ponechat postranní panel otevřený", "selectAllElementsInFrame": "", "removeAllElementsFromFrame": "", - "eyeDropper": "Vyberte barvu z plátna" + "eyeDropper": "Vyberte barvu z plátna", + "textToDiagram": "", + "prompt": "" }, "library": { "noItems": "Dosud neexistují žádné položky...", @@ -209,6 +217,7 @@ "importLibraryError": "Nelze načíst knihovnu", "collabSaveFailed": "Nelze uložit do databáze na serveru. Pokud problémy přetrvávají, měli byste uložit soubor lokálně, abyste se ujistili, že neztratíte svou práci.", "collabSaveFailed_sizeExceeded": "Nelze uložit do databáze na serveru, plátno se zdá být příliš velké. Měli byste uložit soubor lokálně, abyste se ujistili, že neztratíte svou práci.", + "imageToolNotSupported": "", "brave_measure_text_error": { "line1": "Vypadá to, že používáte Brave prohlížeč s povoleným nastavením Aggressively Block Fingerprinting.", "line2": "To by mohlo vést k narušení Textových elementů ve vašich výkresech.", @@ -217,8 +226,12 @@ }, "libraryElementTypeError": { "embeddable": "", + "iframe": "", "image": "" - } + }, + "asyncPasteFailedOnRead": "", + "asyncPasteFailedOnParse": "", + "copyToSystemClipboardFailed": "" }, "toolBar": { "selection": "Výběr", @@ -236,10 +249,13 @@ "link": "Přidat/aktualizovat odkaz pro vybraný tvar", "eraser": "Guma", "frame": "", + "magicframe": "", "embeddable": "", "laser": "", "hand": "Ruka (nástroj pro posouvání)", - "extraTools": "" + "extraTools": "", + "mermaidToExcalidraw": "", + "magicSettings": "" }, "headings": { "canvasActions": "Akce plátna", @@ -498,5 +514,12 @@ "description": "" } } + }, + "mermaid": { + "title": "", + "button": "", + "description": "", + "syntax": "", + "preview": "" } } diff --git a/src/locales/da-DK.json b/packages/excalidraw/locales/da-DK.json similarity index 53% rename from src/locales/da-DK.json rename to packages/excalidraw/locales/da-DK.json index 0cd9c8ce0..ebefa12ad 100644 --- a/src/locales/da-DK.json +++ b/packages/excalidraw/locales/da-DK.json @@ -11,6 +11,8 @@ "copyAsPng": "Kopier til klippebord som PNG", "copyAsSvg": "Kopier til klippebord som SVG", "copyText": "Kopiér til udklipsholder som tekst", + "copySource": "Kopiér kilde til udklipsholder", + "convertToCode": "Konvertér til kode", "bringForward": "Flyt fremad", "sendToBack": "Placer bagest", "bringToFront": "Placer forrest", @@ -36,8 +38,12 @@ "arrowhead_none": "Ingen", "arrowhead_arrow": "Pil", "arrowhead_bar": "Bjælke", - "arrowhead_dot": "Prik", + "arrowhead_circle": "", + "arrowhead_circle_outline": "", "arrowhead_triangle": "Trekant", + "arrowhead_triangle_outline": "", + "arrowhead_diamond": "", + "arrowhead_diamond_outline": "", "fontSize": "Skriftstørrelse", "fontFamily": "Skrifttypefamilie", "addWatermark": "Tilføj \"Lavet med Excalidraw\"", @@ -75,77 +81,79 @@ "name": "Navn", "yourName": "Dit navn", "madeWithExcalidraw": "Fremstillet med Excalidraw", - "group": "", - "ungroup": "", - "collaborators": "", - "showGrid": "", - "addToLibrary": "", - "removeFromLibrary": "", - "libraryLoadingMessage": "", - "libraries": "", - "loadingScene": "", - "align": "", - "alignTop": "", - "alignBottom": "", - "alignLeft": "", - "alignRight": "", - "centerVertically": "", - "centerHorizontally": "", - "distributeHorizontally": "", - "distributeVertically": "", - "flipHorizontal": "", - "flipVertical": "", - "viewMode": "", + "group": "Grupper valgte", + "ungroup": "Opløs gruppe", + "collaborators": "Deltagere", + "showGrid": "Vis gitter", + "addToLibrary": "Føj til Bibliotek", + "removeFromLibrary": "Fjern fra biblioteket", + "libraryLoadingMessage": "Indlæser bibliotek…", + "libraries": "Gennemse biblioteker", + "loadingScene": "Indlæser scene…", + "align": "Justér", + "alignTop": "Juster til top", + "alignBottom": "Juster til bund", + "alignLeft": "Venstrejusteret", + "alignRight": "Juster højre", + "centerVertically": "Center vertikalt", + "centerHorizontally": "Vandret centreret", + "distributeHorizontally": "Distribuer vandret", + "distributeVertically": "Distribuer lodret", + "flipHorizontal": "Spejlvend horisontalt", + "flipVertical": "Vend lodret", + "viewMode": "Visningstilstand", "share": "Del", - "showStroke": "", - "showBackground": "", - "toggleTheme": "", - "personalLib": "", - "excalidrawLib": "", - "decreaseFontSize": "", - "increaseFontSize": "", - "unbindText": "", - "bindText": "", - "createContainerFromText": "", + "showStroke": "Vis stregfarve-vælger", + "showBackground": "Vis baggrundsfarve-vælger", + "toggleTheme": "Skift tema", + "personalLib": "Personligt bibliotek", + "excalidrawLib": "Excalidraw Bibliotek", + "decreaseFontSize": "Gør skriften mindre", + "increaseFontSize": "Gør skriften større", + "unbindText": "Frigør tekst", + "bindText": "Bind tekst til beholderen", + "createContainerFromText": "Ombryd tekst i en beholder", "link": { - "edit": "", - "editEmbed": "", - "create": "", - "createEmbed": "", - "label": "", - "labelEmbed": "", - "empty": "" + "edit": "Redigér link", + "editEmbed": "Redigér link & indlejret", + "create": "Link oprettet", + "createEmbed": "Opret link & indlejret", + "label": "Links", + "labelEmbed": "Link & indlejret", + "empty": "Intet link angivet" }, "lineEditor": { - "edit": "", - "exit": "" + "edit": "Rediger Linje", + "exit": "Afslut linjeeditor" }, "elementLock": { - "lock": "", - "unlock": "", - "lockAll": "", - "unlockAll": "" + "lock": "Lås", + "unlock": "Lås op", + "lockAll": "Lås alle", + "unlockAll": "Lås alle op" }, - "statusPublished": "", - "sidebarLock": "", - "selectAllElementsInFrame": "", - "removeAllElementsFromFrame": "", - "eyeDropper": "" + "statusPublished": "Udgiver", + "sidebarLock": "Hold sidepanel åben", + "selectAllElementsInFrame": "Vælg alle elementer i rammen", + "removeAllElementsFromFrame": "Fjern alle elementer fra ramme", + "eyeDropper": "Vælg farve fra lærred", + "textToDiagram": "Tekst til diagram", + "prompt": "Prompt" }, "library": { - "noItems": "", - "hint_emptyLibrary": "", - "hint_emptyPrivateLibrary": "" + "noItems": "Ingen varer tilføjet endnu...", + "hint_emptyLibrary": "Vælg et element på lærred for at tilføje det her, eller installer et bibliotek fra det offentlige arkiv, nedenfor.", + "hint_emptyPrivateLibrary": "Vælg et element på lærred for at tilføje det her." }, "buttons": { - "clearReset": "", - "exportJSON": "", - "exportImage": "", - "export": "", + "clearReset": "Nulstil lærredet", + "exportJSON": "Eksportér til fil", + "exportImage": "Eksporter billede...", + "export": "Gem til...", "copyToClipboard": "Kopier til klippebord", - "save": "", + "save": "Gem til nuværende fil", "saveAs": "Gem som", - "load": "", + "load": "Åbn", "getShareableLink": "Lav et delbart link", "close": "Luk", "selectLanguage": "Vælg sprog", @@ -164,16 +172,16 @@ "darkMode": "Mørk tilstand", "lightMode": "Lys baggrund", "zenMode": "Zentilstand", - "objectsSnapMode": "", + "objectsSnapMode": "Fastgør til objekter", "exitZenMode": "Stop zentilstand", "cancel": "Annuller", "clear": "Ryd", "remove": "Fjern", - "embed": "", + "embed": "Slå indlejring til/fra", "publishLibrary": "Publicér", "submit": "Gem", "confirm": "Bekræft", - "embeddableInteractionButton": "" + "embeddableInteractionButton": "Klik for at interagere" }, "alerts": { "clearReset": "Dette vil rydde hele lærredet. Er du sikker?", @@ -181,77 +189,85 @@ "couldNotCreateShareableLinkTooBig": "Kunne ikke oprette delbart link: scenen er for stor", "couldNotLoadInvalidFile": "Kunne ikke indlæse ugyldig fil", "importBackendFailed": "Import fra backend mislykkedes.", - "cannotExportEmptyCanvas": "", - "couldNotCopyToClipboard": "", - "decryptFailed": "", - "uploadedSecurly": "", - "loadSceneOverridePrompt": "", - "collabStopOverridePrompt": "", - "errorAddingToLibrary": "", - "errorRemovingFromLibrary": "", - "confirmAddLibrary": "", - "imageDoesNotContainScene": "", - "cannotRestoreFromImage": "", - "invalidSceneUrl": "", - "resetLibrary": "", - "removeItemsFromsLibrary": "", - "invalidEncryptionKey": "", - "collabOfflineWarning": "" + "cannotExportEmptyCanvas": "Kan ikke eksportere tomt lærred.", + "couldNotCopyToClipboard": "Kunne ikke kopiere til udklipsholderen.", + "decryptFailed": "Kunne ikke dekryptere data.", + "uploadedSecurly": "Upload er blevet sikret med ende-til-ende kryptering, hvilket betyder, at Excalidraw server og tredjeparter ikke kan læse indholdet.", + "loadSceneOverridePrompt": "Indlæsning af ekstern tegning erstatter dit eksisterende indhold. Ønsker du at fortsætte?", + "collabStopOverridePrompt": "Stopper sessionen vil overskrive din tidligere, lokalt gemte tegning. Er du sikker?\n\n(Hvis du ønsker at beholde din lokale tegning, skal du blot lukke browserfanen i stedet.)", + "errorAddingToLibrary": "Kunne ikke tilføje element til biblioteket", + "errorRemovingFromLibrary": "Kunne ikke fjerne element fra biblioteket", + "confirmAddLibrary": "Dette vil tilføje {{numShapes}} form(er) til dit bibliotek. Er du sikker?", + "imageDoesNotContainScene": "Dette billede synes ikke at indeholde scene data. Har du aktiveret scene indlejring under eksport?", + "cannotRestoreFromImage": "Scene kunne ikke gendannes fra denne billedfil", + "invalidSceneUrl": "Kunne ikke importere scene fra den angivne URL. Det er enten misdannet eller indeholder ikke gyldige Excalidraw JSON data.", + "resetLibrary": "Dette vil rydde hele lærredet. Er du sikker?", + "removeItemsFromsLibrary": "Slet {{count}} vare(r) fra biblioteket?", + "invalidEncryptionKey": "Krypteringsnøglen skal være på 22 tegn. Live-samarbejde er deaktiveret.", + "collabOfflineWarning": "Ingen internetforbindelse tilgængelig.\nDine ændringer vil ikke blive gemt!" }, "errors": { - "unsupportedFileType": "", - "imageInsertError": "", - "fileTooBig": "", - "svgImageInsertError": "", - "failedToFetchImage": "", - "invalidSVGString": "", - "cannotResolveCollabServer": "", - "importLibraryError": "", - "collabSaveFailed": "", - "collabSaveFailed_sizeExceeded": "", + "unsupportedFileType": "Filtypen er ikke understøttet.", + "imageInsertError": "Billedet kunne ikke indsættes. Prøv igen senere...", + "fileTooBig": "Filen er for stor. Maksimal tilladt størrelse er {{maxSize}}.", + "svgImageInsertError": "Kunne ikke indsætte SVG-billede. SVG-markup'en ser ugyldig ud.", + "failedToFetchImage": "Dataene blev ikke hentet.", + "invalidSVGString": "Ugyldig SVG.", + "cannotResolveCollabServer": "Kunne ikke oprette forbindelse til samarbejdsserveren. Genindlæs siden og prøv igen.", + "importLibraryError": "Biblioteket kunne ikke indlæses", + "collabSaveFailed": "Kunne ikke gemme i databasen. Hvis problemerne fortsætter, bør du gemme din fil lokalt for at sikre, at du ikke mister dit arbejde.", + "collabSaveFailed_sizeExceeded": "Kunne ikke gemme i databasen, lærredet lader til at være for stort. Du bør gemme filen lokalt for at sikre, at du ikke mister dit arbejde.", + "imageToolNotSupported": "Billeder er deaktiveret.", "brave_measure_text_error": { - "line1": "", - "line2": "", - "line3": "", - "line4": "" + "line1": "Det ser ud til, at du bruger Brave browser med indstillingen Aggressively Block Fingerprinting aktiveret.", + "line2": "Dette kan resultere i brud på tekstelementerne i dine tegninger.", + "line3": "Vi anbefaler kraftigt at deaktivere denne indstilling. Du kan følge disse trin om, hvordan du gør det.", + "line4": "Hvis deaktivering af denne indstilling ikke løser visning af tekstelementer, åbn venligst et issue på vores GitHub, eller skriv os på Discord" }, "libraryElementTypeError": { - "embeddable": "", - "image": "" - } + "embeddable": "Indlejringselementer kan ikke tilføjes til biblioteket.", + "iframe": "IFrame elementer kan ikke tilføjes til biblioteket.", + "image": "Understøttelse af at tilføje billeder til biblioteket kommer snart!" + }, + "asyncPasteFailedOnRead": "Kunne ikke indsætte (kan ikke læse fra systemets udklipsholder).", + "asyncPasteFailedOnParse": "Kunne ikke indsætte.", + "copyToSystemClipboardFailed": "Kunne ikke kopiere til udklipsholderen." }, "toolBar": { - "selection": "", - "image": "", - "rectangle": "", - "diamond": "", - "ellipse": "", - "arrow": "", - "line": "", - "freedraw": "", - "text": "", - "library": "", - "lock": "", - "penMode": "", - "link": "", - "eraser": "", - "frame": "", - "embeddable": "", - "laser": "", - "hand": "", - "extraTools": "" + "selection": "&Udvalg", + "image": "Indsæt billeder", + "rectangle": "Rektangler", + "diamond": "Diamanter", + "ellipse": "Ellipser", + "arrow": "Pile", + "line": "Linje", + "freedraw": "Tegn", + "text": "Tekster", + "library": "~Bibliotek", + "lock": "Behold valgte værktøj aktiv efter tegning", + "penMode": "Pen-tilstand - forhindrer berøring", + "link": "Tilføj/ Opdater link for en valgt form", + "eraser": "Slet", + "frame": "Rammeværktøj", + "magicframe": "Wireframe til kode", + "embeddable": "Web-indlejring", + "laser": "Lasermarkør", + "hand": "Hånd (panorering værktøj)", + "extraTools": "Flere værktøjer", + "mermaidToExcalidraw": "Mermaid til Excalidraw", + "magicSettings": "AI indstillinger" }, "headings": { - "canvasActions": "", - "selectedShapeActions": "", - "shapes": "" + "canvasActions": "Lærred handlinger", + "selectedShapeActions": "Valgte figurhandlinger", + "shapes": "Former" }, "hints": { - "canvasPanning": "", - "linearElement": "", + "canvasPanning": "For at flytte lærred, hold musehjulet eller mellemrumstasten mens du trækker, eller brug håndværktøjet", + "linearElement": "Klik for at starte flere punkter, træk for enkelt linje", "freeDraw": "Klik og træk, slip når du er færdig", - "text": "", - "embeddable": "", + "text": "Tip: du kan også tilføje tekst ved at dobbeltklikke hvor som helst med det valgte værktøj", + "embeddable": "Klik på træk for at oprette en hjemmeside indlejret", "text_selected": "", "text_editing": "", "linearElementMulti": "", @@ -271,12 +287,12 @@ "disableSnapping": "" }, "canvasError": { - "cannotShowPreview": "", - "canvasTooBig": "", - "canvasTooBigTip": "" + "cannotShowPreview": "Kan ikke vise forhåndsvisning", + "canvasTooBig": "Lærredet kan være for stort.", + "canvasTooBigTip": "Tip: Prøv at flytte de fjerneste elementer lidt tættere sammen." }, "errorSplash": { - "headingMain": "", + "headingMain": "Der opstod en fejl. Prøv .", "clearCanvasMessage": "", "clearCanvasCaveat": "", "trackedToSentry": "", @@ -498,5 +514,12 @@ "description": "" } } + }, + "mermaid": { + "title": "", + "button": "", + "description": "", + "syntax": "", + "preview": "" } } diff --git a/src/locales/de-DE.json b/packages/excalidraw/locales/de-DE.json similarity index 94% rename from src/locales/de-DE.json rename to packages/excalidraw/locales/de-DE.json index c41186ef1..5ce97646e 100644 --- a/src/locales/de-DE.json +++ b/packages/excalidraw/locales/de-DE.json @@ -11,6 +11,8 @@ "copyAsPng": "In Zwischenablage kopieren (PNG)", "copyAsSvg": "In Zwischenablage kopieren (SVG)", "copyText": "In die Zwischenablage als Text kopieren", + "copySource": "Quelle in Zwischenablage kopieren", + "convertToCode": "In Code konvertieren", "bringForward": "Nach vorne", "sendToBack": "In den Hintergrund", "bringToFront": "In den Vordergrund", @@ -36,8 +38,12 @@ "arrowhead_none": "Keine", "arrowhead_arrow": "Pfeil", "arrowhead_bar": "Balken", - "arrowhead_dot": "Punkt", + "arrowhead_circle": "Kreis", + "arrowhead_circle_outline": "Kreis (Umrandung)", "arrowhead_triangle": "Dreieck", + "arrowhead_triangle_outline": "Dreieck (Umrandung)", + "arrowhead_diamond": "Raute", + "arrowhead_diamond_outline": "Raute (Umrandung)", "fontSize": "Schriftgröße", "fontFamily": "Schriftfamilie", "addWatermark": "\"Made with Excalidraw\" hinzufügen", @@ -130,7 +136,9 @@ "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", + "textToDiagram": "Text zu Diagramm", + "prompt": "Eingabe" }, "library": { "noItems": "Noch keine Elemente hinzugefügt...", @@ -209,6 +217,7 @@ "importLibraryError": "Bibliothek konnte nicht geladen werden", "collabSaveFailed": "Keine Speicherung in der Backend-Datenbank möglich. Wenn die Probleme weiterhin bestehen, solltest Du Deine Datei lokal speichern, um sicherzustellen, dass Du Deine Arbeit nicht verlierst.", "collabSaveFailed_sizeExceeded": "Keine Speicherung in der Backend-Datenbank möglich, die Zeichenfläche scheint zu groß zu sein. Du solltest Deine Datei lokal speichern, um sicherzustellen, dass Du Deine Arbeit nicht verlierst.", + "imageToolNotSupported": "Bilder sind deaktiviert.", "brave_measure_text_error": { "line1": "Sieht so aus, als ob Du den Brave-Browser verwendest und die aggressive Blockierung von Fingerabdrücken aktiviert hast.", "line2": "Dies könnte dazu führen, dass die Textelemente in Ihren Zeichnungen zerstört werden.", @@ -217,8 +226,12 @@ }, "libraryElementTypeError": { "embeddable": "Einbettbare Elemente können der Bibliothek nicht hinzugefügt werden.", + "iframe": "IFrame-Elemente können nicht zur Bibliothek hinzugefügt werden.", "image": "Unterstützung für das Hinzufügen von Bildern in die Bibliothek kommt bald!" - } + }, + "asyncPasteFailedOnRead": "Einfügen fehlgeschlagen (konnte aus der Zwischenablage des Systems nicht gelesen werden).", + "asyncPasteFailedOnParse": "Einfügen fehlgeschlagen.", + "copyToSystemClipboardFailed": "Kopieren in die Zwischenablage fehlgeschlagen." }, "toolBar": { "selection": "Auswahl", @@ -236,10 +249,13 @@ "link": "Link für ausgewählte Form hinzufügen / aktualisieren", "eraser": "Radierer", "frame": "Rahmenwerkzeug", + "magicframe": "Wireframe zu Code", "embeddable": "Web-Einbettung", "laser": "Laserpointer", "hand": "Hand (Schwenkwerkzeug)", - "extraTools": "Weitere Werkzeuge" + "extraTools": "Weitere Werkzeuge", + "mermaidToExcalidraw": "Mermaid zu Excalidraw", + "magicSettings": "KI-Einstellungen" }, "headings": { "canvasActions": "Aktionen für Zeichenfläche", @@ -498,5 +514,12 @@ "description": "Das Laden einer externen Zeichnung wird Deinen vorhandenen Inhalt ersetzen.

Du kannst Deine Zeichnung zuerst mit einer der folgenden Optionen sichern." } } + }, + "mermaid": { + "title": "Mermaid zu Excalidraw", + "button": "Einfügen", + "description": "Derzeit werden nur Flussdiagramme, Sequenzdiagramme und Klassendiagramme unterstützt. Die anderen Typen werden als Bild in Excalidraw dargestellt.", + "syntax": "Mermaid-Syntax", + "preview": "Vorschau" } } diff --git a/src/locales/el-GR.json b/packages/excalidraw/locales/el-GR.json similarity index 98% rename from src/locales/el-GR.json rename to packages/excalidraw/locales/el-GR.json index 5a60fbb8b..f6fa2f0f9 100644 --- a/src/locales/el-GR.json +++ b/packages/excalidraw/locales/el-GR.json @@ -11,6 +11,8 @@ "copyAsPng": "Αντιγραφή στο πρόχειρο ως PNG", "copyAsSvg": "Αντιγραφή στο πρόχειρο ως SVG", "copyText": "Αντιγραφή στο πρόχειρο ως κείμενο", + "copySource": "", + "convertToCode": "", "bringForward": "Στο προσκήνιο", "sendToBack": "Ένα επίπεδο πίσω", "bringToFront": "Ένα επίπεδο μπροστά", @@ -36,8 +38,12 @@ "arrowhead_none": "Κανένα", "arrowhead_arrow": "Βέλος", "arrowhead_bar": "Μπάρα", - "arrowhead_dot": "Τελεία", + "arrowhead_circle": "", + "arrowhead_circle_outline": "", "arrowhead_triangle": "Τρίγωνο", + "arrowhead_triangle_outline": "", + "arrowhead_diamond": "", + "arrowhead_diamond_outline": "", "fontSize": "Μέγεθος γραμματοσειράς", "fontFamily": "Γραμματοσειρά", "addWatermark": "Προσθήκη \"Φτιαγμένο με Excalidraw\"", @@ -130,7 +136,9 @@ "sidebarLock": "Κρατήστε την πλαϊνή μπάρα ανοιχτή", "selectAllElementsInFrame": "", "removeAllElementsFromFrame": "", - "eyeDropper": "" + "eyeDropper": "", + "textToDiagram": "", + "prompt": "" }, "library": { "noItems": "Δεν έχουν προστεθεί αντικείμενα ακόμη...", @@ -209,6 +217,7 @@ "importLibraryError": "Αδυναμία φόρτωσης βιβλιοθήκης", "collabSaveFailed": "Η αποθήκευση στη βάση δεδομένων δεν ήταν δυνατή. Αν το προβλήματα παραμείνει, θα πρέπει να αποθηκεύσετε το αρχείο σας τοπικά για να βεβαιωθείτε ότι δεν χάνετε την εργασία σας.", "collabSaveFailed_sizeExceeded": "Η αποθήκευση στη βάση δεδομένων δεν ήταν δυνατή, ο καμβάς φαίνεται να είναι πολύ μεγάλος. Θα πρέπει να αποθηκεύσετε το αρχείο τοπικά για να βεβαιωθείτε ότι δεν θα χάσετε την εργασία σας.", + "imageToolNotSupported": "", "brave_measure_text_error": { "line1": "", "line2": "", @@ -217,8 +226,12 @@ }, "libraryElementTypeError": { "embeddable": "", + "iframe": "", "image": "" - } + }, + "asyncPasteFailedOnRead": "", + "asyncPasteFailedOnParse": "", + "copyToSystemClipboardFailed": "" }, "toolBar": { "selection": "Επιλογή", @@ -236,10 +249,13 @@ "link": "Προσθήκη/ Ενημέρωση συνδέσμου για ένα επιλεγμένο σχήμα", "eraser": "Γόμα", "frame": "", + "magicframe": "", "embeddable": "", "laser": "", "hand": "", - "extraTools": "" + "extraTools": "", + "mermaidToExcalidraw": "", + "magicSettings": "" }, "headings": { "canvasActions": "Ενέργειες καμβά", @@ -498,5 +514,12 @@ "description": "" } } + }, + "mermaid": { + "title": "", + "button": "", + "description": "", + "syntax": "", + "preview": "" } } diff --git a/src/locales/en.json b/packages/excalidraw/locales/en.json similarity index 95% rename from src/locales/en.json rename to packages/excalidraw/locales/en.json index 8b4a1df21..b983586bb 100644 --- a/src/locales/en.json +++ b/packages/excalidraw/locales/en.json @@ -38,8 +38,12 @@ "arrowhead_none": "None", "arrowhead_arrow": "Arrow", "arrowhead_bar": "Bar", - "arrowhead_dot": "Dot", + "arrowhead_circle": "Circle", + "arrowhead_circle_outline": "Circle (outline)", "arrowhead_triangle": "Triangle", + "arrowhead_triangle_outline": "Triangle (outline)", + "arrowhead_diamond": "Diamond", + "arrowhead_diamond_outline": "Diamond (outline)", "fontSize": "Font size", "fontFamily": "Font family", "addWatermark": "Add \"Made with Excalidraw\"", @@ -134,7 +138,9 @@ "removeAllElementsFromFrame": "Remove all elements from frame", "eyeDropper": "Pick color from canvas", "textToDiagram": "Text to diagram", - "prompt": "Prompt" + "prompt": "Prompt", + "followUs": "Follow us", + "discordChat": "Discord chat" }, "library": { "noItems": "No items added yet...", @@ -295,9 +301,12 @@ "openIssueMessage": "We were very cautious not to include your scene information on the error. If your scene is not private, please consider following up on our . Please include information below by copying and pasting into the GitHub issue.", "sceneContent": "Scene content:" }, + "shareDialog": { + "or": "Or" + }, "roomDialog": { - "desc_intro": "You can invite people to your current scene to collaborate with you.", - "desc_privacy": "Don't worry, the session uses end-to-end encryption, so whatever you draw will stay private. Not even our server will be able to see what you come up with.", + "desc_intro": "Invite people to collaborate on your drawing.", + "desc_privacy": "Don't worry, the session is end-to-end encrypted, and fully private. Not even our server can see what you draw.", "button_startSession": "Start session", "button_stopSession": "Stop session", "desc_inProgressIntro": "Live-collaboration session is now in progress.", @@ -514,8 +523,18 @@ "mermaid": { "title": "Mermaid to Excalidraw", "button": "Insert", - "description": "Currently only Flowcharts and Sequence Diagrams are supported. The other types will be rendered as image in Excalidraw.", + "description": "Currently only Flowchart, Sequence, and Class Diagrams are supported. The other types will be rendered as image in Excalidraw.", "syntax": "Mermaid Syntax", "preview": "Preview" + }, + "userList": { + "search": { + "placeholder": "Quick search", + "empty": "No users found" + }, + "hint": { + "text": "Click on user to follow", + "followStatus": "You're currently following this user" + } } } diff --git a/src/locales/es-ES.json b/packages/excalidraw/locales/es-ES.json similarity index 94% rename from src/locales/es-ES.json rename to packages/excalidraw/locales/es-ES.json index d925ab788..ab7152829 100644 --- a/src/locales/es-ES.json +++ b/packages/excalidraw/locales/es-ES.json @@ -11,6 +11,8 @@ "copyAsPng": "Copiar al portapapeles como PNG", "copyAsSvg": "Copiar al portapapeles como SVG", "copyText": "Copiar al portapapeles como texto", + "copySource": "Copiar fuente al portapapeles", + "convertToCode": "Convertir a código", "bringForward": "Traer hacia delante", "sendToBack": "Enviar al fondo", "bringToFront": "Traer al frente", @@ -36,8 +38,12 @@ "arrowhead_none": "Ninguna", "arrowhead_arrow": "Flecha", "arrowhead_bar": "Barra", - "arrowhead_dot": "Punto", + "arrowhead_circle": "Círculo", + "arrowhead_circle_outline": "Círculo (contorno)", "arrowhead_triangle": "Triángulo", + "arrowhead_triangle_outline": "Triángulo (contorno)", + "arrowhead_diamond": "Diamante", + "arrowhead_diamond_outline": "Diamante (contorno)", "fontSize": "Tamaño de la fuente", "fontFamily": "Tipo de fuente", "addWatermark": "Agregar \"Hecho con Excalidraw\"", @@ -130,7 +136,9 @@ "sidebarLock": "Mantener barra lateral abierta", "selectAllElementsInFrame": "Seleccionar todos los elementos en el marco", "removeAllElementsFromFrame": "Eliminar todos los elementos del marco", - "eyeDropper": "Seleccionar un color del lienzo" + "eyeDropper": "Seleccionar un color del lienzo", + "textToDiagram": "Texto a diagrama", + "prompt": "Sugerencia" }, "library": { "noItems": "No hay elementos añadidos todavía...", @@ -203,12 +211,13 @@ "imageInsertError": "No se pudo insertar la imagen. Inténtelo de nuevo más tarde...", "fileTooBig": "Archivo demasiado grande. El tamaño máximo permitido es {{maxSize}}.", "svgImageInsertError": "No se pudo insertar la imagen SVG. El código SVG parece inválido.", - "failedToFetchImage": "", + "failedToFetchImage": "Error al obtener la imagen.", "invalidSVGString": "SVG no válido.", "cannotResolveCollabServer": "No se pudo conectar al servidor colaborador. Por favor, vuelva a cargar la página y vuelva a intentarlo.", "importLibraryError": "No se pudo cargar la librería", "collabSaveFailed": "No se pudo guardar en la base de datos del backend. Si los problemas persisten, debería guardar su archivo localmente para asegurarse de que no pierde su trabajo.", "collabSaveFailed_sizeExceeded": "No se pudo guardar en la base de datos del backend, el lienzo parece ser demasiado grande. Debería guardar el archivo localmente para asegurarse de que no pierde su trabajo.", + "imageToolNotSupported": "", "brave_measure_text_error": { "line1": "Parece que estás usando el navegador Brave con el ajuste Forzar el bloqueo de huellas digitales habilitado.", "line2": "Esto podría resultar en errores en los Elementos de Texto en tus dibujos.", @@ -217,8 +226,12 @@ }, "libraryElementTypeError": { "embeddable": "", + "iframe": "Los elementos IFrame no se pueden agregar a la biblioteca.", "image": "" - } + }, + "asyncPasteFailedOnRead": "No se pudo pegar (no se pudo leer desde el portapapeles del sistema).", + "asyncPasteFailedOnParse": "No se pudo pegar.", + "copyToSystemClipboardFailed": "No se pudo copiar al portapapeles." }, "toolBar": { "selection": "Selección", @@ -236,10 +249,13 @@ "link": "Añadir/Actualizar enlace para una forma seleccionada", "eraser": "Borrar", "frame": "", + "magicframe": "Esquema a código", "embeddable": "Incrustar Web", "laser": "Puntero láser", "hand": "Mano (herramienta de panoramización)", - "extraTools": "Más herramientas" + "extraTools": "Más herramientas", + "mermaidToExcalidraw": "Mermaid a Excalidraw", + "magicSettings": "Ajustes AI" }, "headings": { "canvasActions": "Acciones del lienzo", @@ -498,5 +514,12 @@ "description": "Cargar un dibujo externo reemplazará tu contenido existente.

Puedes primero hacer una copia de seguridad de tu dibujo usando una de las opciones de abajo." } } + }, + "mermaid": { + "title": "Mermaid a Excalidraw", + "button": "Insertar", + "description": "Actualmente sólo Flowchart, Secuencia, y Class Diagramas son soportados. Los otros tipos se renderizarán como imagen en Excalidraw.", + "syntax": "Sintaxis Mermaid", + "preview": "Vista previa" } } diff --git a/src/locales/eu-ES.json b/packages/excalidraw/locales/eu-ES.json similarity index 95% rename from src/locales/eu-ES.json rename to packages/excalidraw/locales/eu-ES.json index 5a257f05d..19c9adb5f 100644 --- a/src/locales/eu-ES.json +++ b/packages/excalidraw/locales/eu-ES.json @@ -11,6 +11,8 @@ "copyAsPng": "Kopiatu arbelera PNG gisa", "copyAsSvg": "Kopiatu arbelera SVG gisa", "copyText": "Kopiatu arbelera testu gisa", + "copySource": "Kopiatu iturria arbelean", + "convertToCode": "Bihurtu kodea", "bringForward": "Ekarri aurrerago", "sendToBack": "Eraman atzera", "bringToFront": "Ekarri aurrera", @@ -36,8 +38,12 @@ "arrowhead_none": "Bat ere ez", "arrowhead_arrow": "Gezia", "arrowhead_bar": "Barra", - "arrowhead_dot": "Puntua", + "arrowhead_circle": "", + "arrowhead_circle_outline": "", "arrowhead_triangle": "Hirukia", + "arrowhead_triangle_outline": "", + "arrowhead_diamond": "", + "arrowhead_diamond_outline": "", "fontSize": "Letra-tamaina", "fontFamily": "Letra-tipoa", "addWatermark": "Gehitu \"Excalidraw bidez egina\"", @@ -130,7 +136,9 @@ "sidebarLock": "Mantendu alboko barra irekita", "selectAllElementsInFrame": "Hautatu markoko elementu guztiak", "removeAllElementsFromFrame": "Kendu markoko elementu guztiak", - "eyeDropper": "Aukeratu kolorea oihaletik" + "eyeDropper": "Aukeratu kolorea oihaletik", + "textToDiagram": "Testutik diagramara", + "prompt": "" }, "library": { "noItems": "Oraindik ez da elementurik gehitu...", @@ -203,12 +211,13 @@ "imageInsertError": "Ezin izan da irudia txertatu. Saiatu berriro geroago...", "fileTooBig": "Fitxategia handiegia da. Onartutako gehienezko tamaina {{maxSize}} da.", "svgImageInsertError": "Ezin izan da SVG irudia txertatu. SVG markak baliogabea dirudi.", - "failedToFetchImage": "", + "failedToFetchImage": "Ezin izan da irudia eskuratu.", "invalidSVGString": "SVG baliogabea.", "cannotResolveCollabServer": "Ezin izan da elkarlaneko zerbitzarira konektatu. Mesedez, berriro kargatu orria eta saiatu berriro.", "importLibraryError": "Ezin izan da liburutegia kargatu", "collabSaveFailed": "Ezin izan da backend datu-basean gorde. Arazoak jarraitzen badu, zure fitxategia lokalean gorde beharko zenuke zure lana ez duzula galtzen ziurtatzeko.", "collabSaveFailed_sizeExceeded": "Ezin izan da backend datu-basean gorde, ohiala handiegia dela dirudi. Fitxategia lokalean gorde beharko zenuke zure lana galtzen ez duzula ziurtatzeko.", + "imageToolNotSupported": "Irudiak desgaituta daude.", "brave_measure_text_error": { "line1": "Brave arakatzailea erabiltzen ari zarela dirudi Blokeatu hatz-markak erasokorki ezarpena gaituta.", "line2": "Honek zure marrazkietako Testu-elementuak hautsi ditzake.", @@ -217,8 +226,12 @@ }, "libraryElementTypeError": { "embeddable": "Kapsulatutako elementuak ezin dira liburutegira gehitu.", + "iframe": "IFrame elementuak ezin dira liburutegira gehitu.", "image": "Laster egongo da irudiak liburutegian gehitzeko laguntza!" - } + }, + "asyncPasteFailedOnRead": "Ezin izan da itsatsi (ezin izan da sistemaren arbeletik irakurri).", + "asyncPasteFailedOnParse": "Ezin izan da itsatsi.", + "copyToSystemClipboardFailed": "Ezin izan da arbelean kopiatu." }, "toolBar": { "selection": "Hautapena", @@ -236,10 +249,13 @@ "link": "Gehitu / Eguneratu esteka hautatutako forma baterako", "eraser": "Borragoma", "frame": "Marko tresna", + "magicframe": "Wireframe kodetzeko", "embeddable": "Web kapsulatzea", "laser": "Laser punteroa", "hand": "Eskua (panoratze tresna)", - "extraTools": "Tresna gehiago" + "extraTools": "Tresna gehiago", + "mermaidToExcalidraw": "", + "magicSettings": "AI ezarpenak" }, "headings": { "canvasActions": "Canvas ekintzak", @@ -498,5 +514,12 @@ "description": "Kanpoko irudi bat kargatzeak lehendik duzun edukia ordezkatuko du.

. Zure marrazkiaren babeskopia egin dezakezu lehenik beheko aukeretako bat erabiliz." } } + }, + "mermaid": { + "title": "", + "button": "Txertatu", + "description": "", + "syntax": "", + "preview": "Aurrebista" } } diff --git a/src/locales/fa-IR.json b/packages/excalidraw/locales/fa-IR.json similarity index 96% rename from src/locales/fa-IR.json rename to packages/excalidraw/locales/fa-IR.json index 101803258..838973e52 100644 --- a/src/locales/fa-IR.json +++ b/packages/excalidraw/locales/fa-IR.json @@ -11,6 +11,8 @@ "copyAsPng": "کپی در حافطه موقت به صورت PNG", "copyAsSvg": "کپی در حافطه موقت به صورت SVG", "copyText": "کپی در حافطه موقت به صورت متن", + "copySource": "", + "convertToCode": "", "bringForward": "جلو آوردن", "sendToBack": "پس فرستادن", "bringToFront": "جلو آوردن", @@ -36,8 +38,12 @@ "arrowhead_none": "هیچ کدام", "arrowhead_arrow": "پیکان", "arrowhead_bar": "میله ای", - "arrowhead_dot": "نقطه", + "arrowhead_circle": "", + "arrowhead_circle_outline": "", "arrowhead_triangle": "مثلث", + "arrowhead_triangle_outline": "", + "arrowhead_diamond": "", + "arrowhead_diamond_outline": "", "fontSize": "اندازه قلم", "fontFamily": "نوع قلم", "addWatermark": "\"ساخته شده با Excalidraw\" را اضافه کن", @@ -130,7 +136,9 @@ "sidebarLock": "باز نگه داشتن سایدبار", "selectAllElementsInFrame": "", "removeAllElementsFromFrame": "", - "eyeDropper": "انتخاب رنگ از کرباس" + "eyeDropper": "انتخاب رنگ از کرباس", + "textToDiagram": "", + "prompt": "" }, "library": { "noItems": "آیتمی به اینجا اضافه نشده...", @@ -209,6 +217,7 @@ "importLibraryError": "داده‌ها بارگذاری نشدند", "collabSaveFailed": "در پایگاه داده باطن ذخیره نشد. اگر مشکلات همچنان ادامه داشت، باید فایل خود را به صورت محلی ذخیره کنید تا مطمئن شوید کار خود را از دست نمی دهید.", "collabSaveFailed_sizeExceeded": "در پایگاه داده بکند ذخیره نشد. اگر مشکلات همچنان ادامه داشت، باید فایل خود را به صورت محلی ذخیره کنید تا مطمئن شوید کار خود را از دست نمی دهید.", + "imageToolNotSupported": "", "brave_measure_text_error": { "line1": "به نظر می‌رسد از مرورگر Brave با تنظیم مسدود کردن شدید اثرانگشت استفاده می‌کنید.", "line2": "این می تواند منجر به شکستن عناصر متن در نقاشی های شما شود.", @@ -217,8 +226,12 @@ }, "libraryElementTypeError": { "embeddable": "", + "iframe": "", "image": "" - } + }, + "asyncPasteFailedOnRead": "", + "asyncPasteFailedOnParse": "", + "copyToSystemClipboardFailed": "" }, "toolBar": { "selection": "گزینش", @@ -236,10 +249,13 @@ "link": "افزودن/به‌روزرسانی پیوند برای شکل انتخابی", "eraser": "پاک کن", "frame": "", + "magicframe": "", "embeddable": "", "laser": "", "hand": "دست (ابزار پانینگ)", - "extraTools": "ابزارهای بیشتر" + "extraTools": "ابزارهای بیشتر", + "mermaidToExcalidraw": "", + "magicSettings": "" }, "headings": { "canvasActions": "عملیات روی بوم", @@ -380,7 +396,7 @@ "label": { "withBackground": "پس زمینه", "onlySelected": "", - "darkMode": "", + "darkMode": "حالت تیره", "embedScene": "", "scale": "", "padding": "" @@ -394,9 +410,9 @@ "copyPngToClipboard": "" }, "button": { - "exportToPng": "", - "exportToSvg": "", - "copyPngToClipboard": "" + "exportToPng": "PNG", + "exportToSvg": "SVG", + "copyPngToClipboard": "کپی در کلیپ‌بورد" } }, "encrypted": { @@ -498,5 +514,12 @@ "description": "" } } + }, + "mermaid": { + "title": "", + "button": "", + "description": "", + "syntax": "", + "preview": "پیش‌نمایش" } } diff --git a/src/locales/fi-FI.json b/packages/excalidraw/locales/fi-FI.json similarity index 96% rename from src/locales/fi-FI.json rename to packages/excalidraw/locales/fi-FI.json index 0a8963f9a..4193c93ad 100644 --- a/src/locales/fi-FI.json +++ b/packages/excalidraw/locales/fi-FI.json @@ -11,6 +11,8 @@ "copyAsPng": "Kopioi leikepöydälle PNG-tiedostona", "copyAsSvg": "Kopioi leikepöydälle SVG-tiedostona", "copyText": "Kopioi tekstinä", + "copySource": "", + "convertToCode": "", "bringForward": "Tuo eteenpäin", "sendToBack": "Vie taakse", "bringToFront": "Tuo eteen", @@ -36,8 +38,12 @@ "arrowhead_none": "Ei mitään", "arrowhead_arrow": "Nuoli", "arrowhead_bar": "Tasapää", - "arrowhead_dot": "Piste", + "arrowhead_circle": "", + "arrowhead_circle_outline": "", "arrowhead_triangle": "Kolmio", + "arrowhead_triangle_outline": "", + "arrowhead_diamond": "", + "arrowhead_diamond_outline": "", "fontSize": "Kirjasinkoko", "fontFamily": "Kirjasintyyppi", "addWatermark": "Lisää \"Tehty Excalidrawilla\"", @@ -130,7 +136,9 @@ "sidebarLock": "Pidä sivupalkki avoinna", "selectAllElementsInFrame": "", "removeAllElementsFromFrame": "", - "eyeDropper": "" + "eyeDropper": "", + "textToDiagram": "", + "prompt": "" }, "library": { "noItems": "Kirjastossa ei ole vielä yhtään kohdetta...", @@ -209,6 +217,7 @@ "importLibraryError": "Kokoelman lataaminen epäonnistui", "collabSaveFailed": "Ei voitu tallentaan palvelimen tietokantaan. Jos ongelmia esiintyy, sinun kannatta tallentaa tallentaa tiedosto paikallisesti varmistaaksesi, että et menetä työtäsi.", "collabSaveFailed_sizeExceeded": "Ei voitu tallentaan palvelimen tietokantaan. Jos ongelmia esiintyy, sinun kannatta tallentaa tallentaa tiedosto paikallisesti varmistaaksesi, että et menetä työtäsi.", + "imageToolNotSupported": "", "brave_measure_text_error": { "line1": "", "line2": "", @@ -217,8 +226,12 @@ }, "libraryElementTypeError": { "embeddable": "", + "iframe": "", "image": "" - } + }, + "asyncPasteFailedOnRead": "", + "asyncPasteFailedOnParse": "", + "copyToSystemClipboardFailed": "" }, "toolBar": { "selection": "Valinta", @@ -236,10 +249,13 @@ "link": "Lisää/päivitä linkki valitulle muodolle", "eraser": "Poistotyökalu", "frame": "", + "magicframe": "", "embeddable": "", "laser": "", "hand": "Käsi (panning-työkalu)", - "extraTools": "" + "extraTools": "", + "mermaidToExcalidraw": "", + "magicSettings": "" }, "headings": { "canvasActions": "Piirtoalueen toiminnot", @@ -498,5 +514,12 @@ "description": "" } } + }, + "mermaid": { + "title": "", + "button": "", + "description": "", + "syntax": "", + "preview": "" } } diff --git a/src/locales/fr-FR.json b/packages/excalidraw/locales/fr-FR.json similarity index 95% rename from src/locales/fr-FR.json rename to packages/excalidraw/locales/fr-FR.json index 8dfcc4c05..674204d12 100644 --- a/src/locales/fr-FR.json +++ b/packages/excalidraw/locales/fr-FR.json @@ -11,6 +11,8 @@ "copyAsPng": "Copier dans le presse-papier en PNG", "copyAsSvg": "Copier dans le presse-papier en SVG", "copyText": "Copier dans le presse-papier en tant que texte", + "copySource": "Copier la source dans le presse-papiers", + "convertToCode": "Convertir en code", "bringForward": "Envoyer vers l'avant", "sendToBack": "Déplacer à l'arrière-plan", "bringToFront": "Mettre au premier plan", @@ -36,8 +38,12 @@ "arrowhead_none": "Sans", "arrowhead_arrow": "Flèche", "arrowhead_bar": "Barre", - "arrowhead_dot": "Point", + "arrowhead_circle": "Cercle", + "arrowhead_circle_outline": "Contour du cercle", "arrowhead_triangle": "Triangle", + "arrowhead_triangle_outline": "Triangle (contour)", + "arrowhead_diamond": "Losange", + "arrowhead_diamond_outline": "", "fontSize": "Taille de la police", "fontFamily": "Police", "addWatermark": "Ajouter \"Réalisé avec Excalidraw\"", @@ -113,7 +119,7 @@ "create": "Ajouter un lien", "createEmbed": "Créer un lien & intégrer", "label": "Lien", - "labelEmbed": "", + "labelEmbed": "Lier & intégrer", "empty": "Aucun lien défini" }, "lineEditor": { @@ -130,7 +136,9 @@ "sidebarLock": "Maintenir la barre latérale ouverte", "selectAllElementsInFrame": "Sélectionner tous les éléments du cadre", "removeAllElementsFromFrame": "Supprimer tous les éléments du cadre", - "eyeDropper": "" + "eyeDropper": "Choisir la couleur depuis la toile", + "textToDiagram": "Texte vers Diagramme", + "prompt": "Consignes" }, "library": { "noItems": "Aucun élément n'a encore été ajouté ...", @@ -203,12 +211,13 @@ "imageInsertError": "Impossible d'insérer l'image. Réessayez plus tard...", "fileTooBig": "Le fichier est trop volumineux. La taille maximale autorisée est de {{maxSize}}.", "svgImageInsertError": "Impossible d'insérer l'image SVG. Le balisage SVG semble invalide.", - "failedToFetchImage": "", + "failedToFetchImage": "Échec de récupération de l'image.", "invalidSVGString": "SVG invalide.", "cannotResolveCollabServer": "Impossible de se connecter au serveur collaboratif. Veuillez recharger la page et réessayer.", "importLibraryError": "Impossible de charger la bibliothèque", "collabSaveFailed": "Impossible d'enregistrer dans la base de données en arrière-plan. Si des problèmes persistent, vous devriez enregistrer votre fichier localement pour vous assurer de ne pas perdre votre travail.", "collabSaveFailed_sizeExceeded": "Impossible d'enregistrer dans la base de données en arrière-plan, le tableau semble trop grand. Vous devriez enregistrer le fichier localement pour vous assurer de ne pas perdre votre travail.", + "imageToolNotSupported": "Les images sont désactivées.", "brave_measure_text_error": { "line1": "On dirait que vous utilisez le navigateur Brave avec l'option Bloquer agressivement le fichage activée.", "line2": "Cela pourrait entraîner des problèmes avec les Éléments Textuels dans vos dessins.", @@ -217,8 +226,12 @@ }, "libraryElementTypeError": { "embeddable": "Les éléments intégrés ne peuvent pas être ajoutés à la librairie.", + "iframe": "", "image": "Le support pour l'ajout d'images à la librairie arrive bientôt !" - } + }, + "asyncPasteFailedOnRead": "Impossible de coller (impossible de lire le presse-papiers système).", + "asyncPasteFailedOnParse": "Impossible de coller.", + "copyToSystemClipboardFailed": "Échec de la copie dans le presse-papiers." }, "toolBar": { "selection": "Sélection", @@ -236,10 +249,13 @@ "link": "Ajouter/mettre à jour le lien pour une forme sélectionnée", "eraser": "Gomme", "frame": "Outil de cadre", + "magicframe": "", "embeddable": "Intégration Web", - "laser": "", + "laser": "Pointeur laser", "hand": "Mains (outil de déplacement de la vue)", - "extraTools": "Plus d'outils" + "extraTools": "Plus d'outils", + "mermaidToExcalidraw": "De Mermaid à Excalidraw", + "magicSettings": "Paramètres IA" }, "headings": { "canvasActions": "Actions du canevas", @@ -383,7 +399,7 @@ "darkMode": "Mode sombre", "embedScene": "Intégrer la scène", "scale": "Échelle", - "padding": "" + "padding": "Marge interne" }, "tooltip": { "embedScene": "Les données de la scène seront sauvegardées dans le fichier PNG/SVG exporté afin que la scène puisse être restaurée depuis celui-ci.\nCela augmentera la taille du fichier exporté." @@ -498,5 +514,12 @@ "description": "Charger un dessin externe va remplacer votre contenu existant.

Vous pouvez d'abord sauvegarder votre dessin en utilisant l'une des options ci-dessous." } } + }, + "mermaid": { + "title": "De Mermaid à Excalidraw", + "button": "Insérer", + "description": "", + "syntax": "Syntaxe Mermaid", + "preview": "Prévisualisation" } } diff --git a/src/locales/gl-ES.json b/packages/excalidraw/locales/gl-ES.json similarity index 97% rename from src/locales/gl-ES.json rename to packages/excalidraw/locales/gl-ES.json index 658563548..b0e6a7508 100644 --- a/src/locales/gl-ES.json +++ b/packages/excalidraw/locales/gl-ES.json @@ -11,6 +11,8 @@ "copyAsPng": "Copiar no portapapeis como PNG", "copyAsSvg": "Copiar no portapapeis como SVG", "copyText": "Copia no portapapeis como texto", + "copySource": "", + "convertToCode": "", "bringForward": "Traer cara adiante", "sendToBack": "Enviar cara atrás", "bringToFront": "Traer á fronte", @@ -36,8 +38,12 @@ "arrowhead_none": "Ningunha", "arrowhead_arrow": "Frecha", "arrowhead_bar": "Barra", - "arrowhead_dot": "Punto", + "arrowhead_circle": "", + "arrowhead_circle_outline": "", "arrowhead_triangle": "Triángulo", + "arrowhead_triangle_outline": "", + "arrowhead_diamond": "", + "arrowhead_diamond_outline": "", "fontSize": "Tamaño da fonte", "fontFamily": "Tipo de fonte", "addWatermark": "Engadir \"Feito con Excalidraw\"", @@ -130,7 +136,9 @@ "sidebarLock": "Manter a barra lateral aberta", "selectAllElementsInFrame": "", "removeAllElementsFromFrame": "", - "eyeDropper": "" + "eyeDropper": "", + "textToDiagram": "", + "prompt": "" }, "library": { "noItems": "Aínda non hai elementos engadidos...", @@ -209,6 +217,7 @@ "importLibraryError": "Non se puido cargar a biblioteca", "collabSaveFailed": "Non se puido gardar na base de datos. Se o problema persiste, deberías gardar o teu arquivo de maneira local para asegurarte de non perdelo teu traballo.", "collabSaveFailed_sizeExceeded": "Non se puido gardar na base de datos, o lenzo semella demasiado grande. Deberías gardar o teu arquivo de maneira local para asegurarte de non perdelo teu traballo.", + "imageToolNotSupported": "", "brave_measure_text_error": { "line1": "", "line2": "", @@ -217,8 +226,12 @@ }, "libraryElementTypeError": { "embeddable": "", + "iframe": "", "image": "" - } + }, + "asyncPasteFailedOnRead": "", + "asyncPasteFailedOnParse": "", + "copyToSystemClipboardFailed": "" }, "toolBar": { "selection": "Selección", @@ -236,10 +249,13 @@ "link": "Engadir/ Actualizar ligazón para a forma seleccionada", "eraser": "Goma de borrar", "frame": "", + "magicframe": "", "embeddable": "Inserir na web", "laser": "Punteiro láser", "hand": "Man (ferramenta de desprazamento)", - "extraTools": "Máis ferramentas" + "extraTools": "Máis ferramentas", + "mermaidToExcalidraw": "", + "magicSettings": "" }, "headings": { "canvasActions": "Accións do lenzo", @@ -498,5 +514,12 @@ "description": "" } } + }, + "mermaid": { + "title": "", + "button": "", + "description": "", + "syntax": "", + "preview": "" } } diff --git a/src/locales/he-IL.json b/packages/excalidraw/locales/he-IL.json similarity index 97% rename from src/locales/he-IL.json rename to packages/excalidraw/locales/he-IL.json index aec18f955..83bf3e718 100644 --- a/src/locales/he-IL.json +++ b/packages/excalidraw/locales/he-IL.json @@ -11,6 +11,8 @@ "copyAsPng": "העתק ללוח כ PNG", "copyAsSvg": "העתק ללוח כ SVG", "copyText": "העתק ללוח כטקסט", + "copySource": "", + "convertToCode": "", "bringForward": "הבא שכבה קדימה", "sendToBack": "שלח אחורה", "bringToFront": "העבר לחזית", @@ -36,8 +38,12 @@ "arrowhead_none": "ללא", "arrowhead_arrow": "חץ", "arrowhead_bar": "קצה אנכי", - "arrowhead_dot": "נקודה", + "arrowhead_circle": "", + "arrowhead_circle_outline": "", "arrowhead_triangle": "משולש", + "arrowhead_triangle_outline": "", + "arrowhead_diamond": "", + "arrowhead_diamond_outline": "", "fontSize": "גודל גופן", "fontFamily": "גופן", "addWatermark": "הוסף \"נוצר באמצעות Excalidraw\"", @@ -130,7 +136,9 @@ "sidebarLock": "שמור את סרגל הצד פתוח", "selectAllElementsInFrame": "בחר את כל האלמנטים במסגרת", "removeAllElementsFromFrame": "הסר את כל האלמנטים שבמסגרת", - "eyeDropper": "" + "eyeDropper": "", + "textToDiagram": "", + "prompt": "" }, "library": { "noItems": "עוד לא הוספת דברים...", @@ -209,6 +217,7 @@ "importLibraryError": "לא ניתן היה לטעון את הספריה", "collabSaveFailed": "לא הצלחתי להתחבר למסד הנתונים האחורי. אם הבעיה ממשיכה, כדאי שתשמור את הקובץ מקומית כדי לוודא שלא תאבד את העבודה שלך.", "collabSaveFailed_sizeExceeded": "לא הצלחתי לשמור למסד הנתונים האחורי, נראה שהקנבס שלך גדול מדי. כדאי שתשמור את הקובץ מקומית כדי לוודא שלא תאבד את העבודה שלך.", + "imageToolNotSupported": "", "brave_measure_text_error": { "line1": "", "line2": "", @@ -217,8 +226,12 @@ }, "libraryElementTypeError": { "embeddable": "", + "iframe": "", "image": "" - } + }, + "asyncPasteFailedOnRead": "", + "asyncPasteFailedOnParse": "", + "copyToSystemClipboardFailed": "" }, "toolBar": { "selection": "בחירה", @@ -236,10 +249,13 @@ "link": "הוספה/עדכון קישור של הצורה שנבחרה", "eraser": "מחק", "frame": "", + "magicframe": "", "embeddable": "", "laser": "", "hand": "יד (כלי הזזה)", - "extraTools": "" + "extraTools": "", + "mermaidToExcalidraw": "", + "magicSettings": "" }, "headings": { "canvasActions": "פעולות קנבאס", @@ -498,5 +514,12 @@ "description": "" } } + }, + "mermaid": { + "title": "", + "button": "", + "description": "", + "syntax": "", + "preview": "" } } diff --git a/src/locales/hi-IN.json b/packages/excalidraw/locales/hi-IN.json similarity index 94% rename from src/locales/hi-IN.json rename to packages/excalidraw/locales/hi-IN.json index 0f3d135cb..4cac567aa 100644 --- a/src/locales/hi-IN.json +++ b/packages/excalidraw/locales/hi-IN.json @@ -11,6 +11,8 @@ "copyAsPng": "क्लिपबोर्ड पर कॉपी करें ,पीएनजी के रूप में", "copyAsSvg": "क्लिपबोर्ड पर कॉपी करें,एसवीजी के रूप में", "copyText": "लेखन के रूप में पटल पर कॉपी करें", + "copySource": "स्त्रोत को प्रति-फलक पे प्रतिलिपित करे.", + "convertToCode": "सांकेतिक लिपि में परिवर्तित करे", "bringForward": "सामने लाएं", "sendToBack": "पीछे भेजें", "bringToFront": "सामने लाएँ", @@ -36,8 +38,12 @@ "arrowhead_none": "कोई भी नहीं", "arrowhead_arrow": "तीर", "arrowhead_bar": "बार", - "arrowhead_dot": "बिंदु", + "arrowhead_circle": "", + "arrowhead_circle_outline": "", "arrowhead_triangle": "त्रिकोण", + "arrowhead_triangle_outline": "", + "arrowhead_diamond": "", + "arrowhead_diamond_outline": "", "fontSize": "फ़ॉन्ट का आकार", "fontFamily": "फ़ॉन्ट का परिवार", "addWatermark": "ऐड \"मेड विथ एक्सकैलिडराव\"", @@ -100,15 +106,15 @@ "showStroke": "", "showBackground": "पृष्ठभूमि रंग वरक़ दिखाये", "toggleTheme": "", - "personalLib": "", - "excalidrawLib": "", + "personalLib": "वैयक्तिक समूहकोष", + "excalidrawLib": "एक्सकेलीड्रॉ समूहकोष", "decreaseFontSize": "आकार घटाइऐ", "increaseFontSize": "फ़ॉन्ट आकार बढ़ाएँ", - "unbindText": "", + "unbindText": "लिपि को बंधमुक्त करें", "bindText": "लेखन को कोश से जोड़े", "createContainerFromText": "मूलपाठ कंटेनर में मोड के दिखाए", "link": { - "edit": "", + "edit": "कड़ी संपादित करे", "editEmbed": "", "create": "", "createEmbed": "", @@ -130,7 +136,9 @@ "sidebarLock": "साइडबार खुला रखे.", "selectAllElementsInFrame": "", "removeAllElementsFromFrame": "", - "eyeDropper": "चित्रफलक से रंग चुने" + "eyeDropper": "चित्रफलक से रंग चुने", + "textToDiagram": "", + "prompt": "" }, "library": { "noItems": "अभी तक कोई आइटम जोडा नहीं गया.", @@ -209,6 +217,7 @@ "importLibraryError": "संग्रह प्रतिष्ठापित नहीं किया जा सका", "collabSaveFailed": "किसी कारण वश अंदरूनी डेटाबेस में सहेजा नहीं जा सका। यदि समस्या बनी रहती है, तो किये काम को खोने न देने के लिये अपनी फ़ाइल को स्थानीय रूप से सहेजे।", "collabSaveFailed_sizeExceeded": "लगता है कि पृष्ठ तल काफ़ी बड़ा है, इस्कारण अंदरूनी डेटाबेस में सहेजा नहीं जा सका। किये काम को खोने न देने के लिये अपनी फ़ाइल को स्थानीय रूप से सहेजे।", + "imageToolNotSupported": "प्रतिमायें अक्षम की गयी हैं", "brave_measure_text_error": { "line1": "लगता है कि आप Brave ब्राउज़र का उपयोग कर रहे और साथ में आक्रामक उँगलियो के छाप का चयन किया हुवा है", "line2": "यह आपके चित्रों के पाठ तत्वोंको खंडित कर सकता हैं", @@ -217,8 +226,12 @@ }, "libraryElementTypeError": { "embeddable": "", + "iframe": "आयफ़्रेम तत्व समूहकोष में जोडा नहीं जा सका.", "image": "" - } + }, + "asyncPasteFailedOnRead": "चिपकाया नहीं जा सका (सिस्टम क्लिपबोर्ड से पढ़ा नहीं जा सका).", + "asyncPasteFailedOnParse": "चिपकाया नहीं जा सका.", + "copyToSystemClipboardFailed": "क्लिपबोर्ड पर प्रतिलिपि नहीं बनाई जा सकी." }, "toolBar": { "selection": "चयन", @@ -236,10 +249,13 @@ "link": "", "eraser": "रबड़", "frame": "", + "magicframe": "तारिक ढाँचें को सांकेतिक लिपि में", "embeddable": "", "laser": "लेसर टॉर्च", "hand": "हाथ ( खिसकाने का औज़ार)", - "extraTools": "" + "extraTools": "", + "mermaidToExcalidraw": "मर्मेड से एक्सकाली में", + "magicSettings": "कृतिम बुद्धिमत्ता सेटिंग्स" }, "headings": { "canvasActions": "कैनवास क्रिया", @@ -498,5 +514,12 @@ "description": "बाहर का चित्र लोड करने पर यह आपके कार्य की जगह लेलेगा

आप आपकी ड्रॉइंग पहले निम्न दर्शित विकल्पो में से एक चुनके और उपयोग करके सम्हाल सकते हों." } } + }, + "mermaid": { + "title": "मर्मेड से एक्सकाली में", + "button": "अंदर डाले", + "description": "", + "syntax": "मर्मेड संरचना नियम", + "preview": "पूर्वावलोकन" } } diff --git a/src/locales/hu-HU.json b/packages/excalidraw/locales/hu-HU.json similarity index 89% rename from src/locales/hu-HU.json rename to packages/excalidraw/locales/hu-HU.json index 130c388c8..e367aa6e9 100644 --- a/src/locales/hu-HU.json +++ b/packages/excalidraw/locales/hu-HU.json @@ -1,7 +1,7 @@ { "labels": { "paste": "Beillesztés", - "pasteAsPlaintext": "", + "pasteAsPlaintext": "Beillesztés formázatlan szövegként", "pasteCharts": "Grafikon beillesztése", "selectAll": "Összes kijelölése", "multiSelect": "Elem hozzáadása a kijelöléshez", @@ -10,7 +10,9 @@ "copy": "Másolás", "copyAsPng": "Vágólapra másolás mint PNG", "copyAsSvg": "Vágólapra másolás mint SVG", - "copyText": "", + "copyText": "Vágólapra másolás szövegként", + "copySource": "", + "convertToCode": "", "bringForward": "Előrébb hozás", "sendToBack": "Hátraküldés", "bringToFront": "Előrehozás", @@ -36,8 +38,12 @@ "arrowhead_none": "Nincs", "arrowhead_arrow": "Nyíl", "arrowhead_bar": "Oszlop", - "arrowhead_dot": "Pont", + "arrowhead_circle": "", + "arrowhead_circle_outline": "", "arrowhead_triangle": "Háromszög", + "arrowhead_triangle_outline": "", + "arrowhead_diamond": "", + "arrowhead_diamond_outline": "", "fontSize": "Betűméret", "fontFamily": "Betűkészlet család", "addWatermark": "Add hozzá, hogy \"Excalidraw-val készült\"", @@ -50,7 +56,7 @@ "veryLarge": "Nagyon nagy", "solid": "Kitöltött", "hachure": "Vonalkázott", - "zigzag": "", + "zigzag": "Cikkcakk", "crossHatch": "Keresztcsíkozott", "thin": "Vékony", "bold": "Félkövér", @@ -69,7 +75,7 @@ "layers": "Rétegek", "actions": "Műveletek", "language": "Nyelv", - "liveCollaboration": "", + "liveCollaboration": "Élő együttműködés...", "duplicateSelection": "Duplikálás", "untitled": "Névtelen", "name": "Név", @@ -106,12 +112,12 @@ "increaseFontSize": "Betűméret növelése", "unbindText": "Szövegkötés feloldása", "bindText": "", - "createContainerFromText": "", + "createContainerFromText": "Szöveg bekeretezése", "link": { "edit": "Hivatkozás szerkesztése", - "editEmbed": "", + "editEmbed": "Link szerkesztése / beágyazása", "create": "Hivatkozás létrehozása", - "createEmbed": "", + "createEmbed": "Link létrehozása / beágyazása", "label": "Hivatkozás", "labelEmbed": "", "empty": "" @@ -121,16 +127,18 @@ "exit": "" }, "elementLock": { - "lock": "", - "unlock": "", - "lockAll": "", - "unlockAll": "" + "lock": "Rögzítés", + "unlock": "Rögzítés feloldása", + "lockAll": "Összes rögzítése", + "unlockAll": "Összes feloldása" }, "statusPublished": "", "sidebarLock": "", "selectAllElementsInFrame": "", "removeAllElementsFromFrame": "", - "eyeDropper": "" + "eyeDropper": "", + "textToDiagram": "", + "prompt": "" }, "library": { "noItems": "", @@ -140,12 +148,12 @@ "buttons": { "clearReset": "Vászon törlése", "exportJSON": "Exportálás fájlba", - "exportImage": "", - "export": "", + "exportImage": "Kép exportálása...", + "export": "Mentés másként...", "copyToClipboard": "Vágólapra másolás", "save": "Mentés az aktuális fájlba", "saveAs": "Mentés másként", - "load": "", + "load": "Megnyitás", "getShareableLink": "Megosztható link létrehozása", "close": "Bezárás", "selectLanguage": "Nyelv kiválasztása", @@ -209,6 +217,7 @@ "importLibraryError": "", "collabSaveFailed": "", "collabSaveFailed_sizeExceeded": "", + "imageToolNotSupported": "", "brave_measure_text_error": { "line1": "", "line2": "", @@ -217,8 +226,12 @@ }, "libraryElementTypeError": { "embeddable": "", + "iframe": "", "image": "" - } + }, + "asyncPasteFailedOnRead": "", + "asyncPasteFailedOnParse": "", + "copyToSystemClipboardFailed": "" }, "toolBar": { "selection": "Kijelölés", @@ -234,12 +247,15 @@ "lock": "Rajzolás után az aktív eszközt tartsa kijelölve", "penMode": "", "link": "Hivatkozás hozzáadása/frissítése a kiválasztott alakzathoz", - "eraser": "", + "eraser": "Radír", "frame": "", - "embeddable": "", - "laser": "", + "magicframe": "", + "embeddable": "Weblap beágyazása", + "laser": "Lézermutató", "hand": "", - "extraTools": "" + "extraTools": "További eszközök", + "mermaidToExcalidraw": "", + "magicSettings": "" }, "headings": { "canvasActions": "Vászon műveletek", @@ -394,9 +410,9 @@ "copyPngToClipboard": "" }, "button": { - "exportToPng": "", - "exportToSvg": "", - "copyPngToClipboard": "" + "exportToPng": "PNG", + "exportToSvg": "SVG", + "copyPngToClipboard": "Vágólapra másolás" } }, "encrypted": { @@ -433,20 +449,20 @@ }, "colors": { "transparent": "Átlátszó", - "black": "", - "white": "", - "red": "", - "pink": "", + "black": "Fekete", + "white": "Fehér", + "red": "Piros", + "pink": "Rózsaszín", "grape": "", - "violet": "", - "gray": "", - "blue": "", - "cyan": "", - "teal": "", - "green": "", - "yellow": "", - "orange": "", - "bronze": "" + "violet": "Ibolya", + "gray": "Szürke", + "blue": "Kék", + "cyan": "Cián", + "teal": "Kékes-zöld", + "green": "Zöld", + "yellow": "Sárga", + "orange": "Narancssárga", + "bronze": "Bronz" }, "welcomeScreen": { "app": { @@ -465,38 +481,45 @@ "mostUsedCustomColors": "", "colors": "", "shades": "", - "hexCode": "", + "hexCode": "Hexadecimális kód", "noShades": "" }, "overwriteConfirm": { "action": { "exportToImage": { - "title": "", - "button": "", + "title": "Exportálás képként", + "button": "Exportálás képként", "description": "" }, "saveToDisk": { - "title": "", - "button": "", + "title": "Mentés a lemezre", + "button": "Mentés a lemezre", "description": "" }, "excalidrawPlus": { - "title": "", + "title": "Excalidraw+", "button": "", "description": "" } }, "modal": { "loadFromFile": { - "title": "", - "button": "", + "title": "Betöltés fájlból", + "button": "Betöltés fájlból", "description": "" }, "shareableLink": { - "title": "", + "title": "Feltöltás linkből", "button": "", "description": "" } } + }, + "mermaid": { + "title": "", + "button": "", + "description": "", + "syntax": "", + "preview": "" } } diff --git a/src/locales/id-ID.json b/packages/excalidraw/locales/id-ID.json similarity index 97% rename from src/locales/id-ID.json rename to packages/excalidraw/locales/id-ID.json index bb0274cf2..b35e1bae4 100644 --- a/src/locales/id-ID.json +++ b/packages/excalidraw/locales/id-ID.json @@ -11,6 +11,8 @@ "copyAsPng": "Salin ke papan klip sebagai PNG", "copyAsSvg": "Salin ke papan klip sebagai SVG", "copyText": "Salin ke papan klip sebagai teks", + "copySource": "", + "convertToCode": "", "bringForward": "Bawa maju", "sendToBack": "Kirim ke belakang", "bringToFront": "Bawa ke depan", @@ -36,8 +38,12 @@ "arrowhead_none": "Tidak ada", "arrowhead_arrow": "Panah", "arrowhead_bar": "Batang", - "arrowhead_dot": "Titik", + "arrowhead_circle": "", + "arrowhead_circle_outline": "", "arrowhead_triangle": "Segitiga", + "arrowhead_triangle_outline": "", + "arrowhead_diamond": "", + "arrowhead_diamond_outline": "", "fontSize": "Ukuran font", "fontFamily": "Jenis font", "addWatermark": "Tambahkan \"Dibuat dengan Excalidraw\"", @@ -130,7 +136,9 @@ "sidebarLock": "Biarkan sidebar tetap terbuka", "selectAllElementsInFrame": "Pilih semua elemen di bingkai", "removeAllElementsFromFrame": "Hapus semua elemen dari bingkai", - "eyeDropper": "Ambil warna dari kanvas" + "eyeDropper": "Ambil warna dari kanvas", + "textToDiagram": "", + "prompt": "" }, "library": { "noItems": "Belum ada item yang ditambahkan...", @@ -209,6 +217,7 @@ "importLibraryError": "Tidak dapat memuat pustaka", "collabSaveFailed": "Tidak dapat menyimpan ke dalam basis data server. Jika masih berlanjut, Anda sebaiknya simpan berkas Anda secara lokal untuk memastikan pekerjaan Anda tidak hilang.", "collabSaveFailed_sizeExceeded": "Tidak dapat menyimpan ke dalam basis data server, tampaknya ukuran kanvas terlalu besar. Anda sebaiknya simpan berkas Anda secara lokal untuk memastikan pekerjaan Anda tidak hilang.", + "imageToolNotSupported": "", "brave_measure_text_error": { "line1": "Sepertinya Anda menggunkan peramban Brave dengan pengaturan Blokir Fingerprinting yang Agresif diaktifkan.", "line2": "Ini dapat membuat Elemen Teks dalam gambar mu.", @@ -217,8 +226,12 @@ }, "libraryElementTypeError": { "embeddable": "", + "iframe": "", "image": "" - } + }, + "asyncPasteFailedOnRead": "", + "asyncPasteFailedOnParse": "", + "copyToSystemClipboardFailed": "" }, "toolBar": { "selection": "Pilihan", @@ -236,10 +249,13 @@ "link": "Tambah/Perbarui tautan untuk bentuk yang dipilih", "eraser": "Penghapus", "frame": "Alat bingkai", + "magicframe": "", "embeddable": "", "laser": "", "hand": "Tangan (alat panning)", - "extraTools": "Alat-alat lain" + "extraTools": "Alat-alat lain", + "mermaidToExcalidraw": "", + "magicSettings": "" }, "headings": { "canvasActions": "Opsi Kanvas", @@ -498,5 +514,12 @@ "description": "Memuat dari file yang akan menggantikan konten Anda sekarang.

Anda dapat mencadangkan gambar anda dulu menggunakan opsi-opsi ini." } } + }, + "mermaid": { + "title": "", + "button": "", + "description": "", + "syntax": "", + "preview": "" } } diff --git a/src/locales/it-IT.json b/packages/excalidraw/locales/it-IT.json similarity index 95% rename from src/locales/it-IT.json rename to packages/excalidraw/locales/it-IT.json index 23490ed5e..6ab03f0ab 100644 --- a/src/locales/it-IT.json +++ b/packages/excalidraw/locales/it-IT.json @@ -11,6 +11,8 @@ "copyAsPng": "Copia negli appunti come PNG", "copyAsSvg": "Copia negli appunti come SVG", "copyText": "Copia negli appunti come testo", + "copySource": "Copia sorgente negli appunti", + "convertToCode": "Converti in codice", "bringForward": "Porta avanti", "sendToBack": "Manda in fondo", "bringToFront": "Porta in cima", @@ -36,8 +38,12 @@ "arrowhead_none": "Nessuno", "arrowhead_arrow": "Freccia", "arrowhead_bar": "Barra", - "arrowhead_dot": "Punto", + "arrowhead_circle": "Cerchio", + "arrowhead_circle_outline": "Cerchio (contorno)", "arrowhead_triangle": "Triangolo", + "arrowhead_triangle_outline": "Triangolo (contorno)", + "arrowhead_diamond": "Diamante", + "arrowhead_diamond_outline": "Diamante (contorno)", "fontSize": "Dimensione carattere", "fontFamily": "Carattere", "addWatermark": "Aggiungi \"Creato con Excalidraw\"", @@ -130,7 +136,9 @@ "sidebarLock": "Mantieni aperta la barra laterale", "selectAllElementsInFrame": "Seleziona tutti gli elementi nel riquadro", "removeAllElementsFromFrame": "Rimuovi tutti gli elementi dal riquadro", - "eyeDropper": "Scegli il colore della tela" + "eyeDropper": "Scegli il colore della tela", + "textToDiagram": "Testo a diagramma", + "prompt": "Prompt" }, "library": { "noItems": "Nessun elemento ancora aggiunto...", @@ -203,12 +211,13 @@ "imageInsertError": "Non è stato possibile inserire l'immagine. Riprova più tardi...", "fileTooBig": "Il file è troppo grande. La dimensione massima consentita è {{maxSize}}.", "svgImageInsertError": "Impossibile inserire l'immagine SVG. Il markup SVG non sembra corretto.", - "failedToFetchImage": "", + "failedToFetchImage": "Impossibile recuperare l'immagine.", "invalidSVGString": "SVG non valido.", "cannotResolveCollabServer": "Impossibile connettersi al server di collab. Ricarica la pagina e riprova.", "importLibraryError": "Impossibile caricare la libreria", "collabSaveFailed": "Impossibile salvare nel database di backend. Se i problemi persistono, dovresti salvare il tuo file localmente per assicurarti di non perdere il tuo lavoro.", "collabSaveFailed_sizeExceeded": "Impossibile salvare nel database di backend, la tela sembra essere troppo grande. Dovresti salvare il file localmente per assicurarti di non perdere il tuo lavoro.", + "imageToolNotSupported": "Le immagini sono disabilitate.", "brave_measure_text_error": { "line1": "Sembra che tu stia utilizzando il browser Brave con l'impostazione Blocco aggressivo delle impronte digitali abilitata.", "line2": "Ciò potrebbe causare la rottura degli Elementi di testo nei tuoi disegni.", @@ -217,8 +226,12 @@ }, "libraryElementTypeError": { "embeddable": "Gli elementi incorporabili non possono essere aggiunti alla libreria.", + "iframe": "Gli elementi IFrame non possono essere aggiunti alla libreria.", "image": "Il supporto per l'aggiunta d'immagini alla libreria verrà aggiunto a breve!" - } + }, + "asyncPasteFailedOnRead": "Impossibile incollare (non è possibile leggere dagli appunti di sistema).", + "asyncPasteFailedOnParse": "Impossibile incollare.", + "copyToSystemClipboardFailed": "Impossibile copiare negli appunti." }, "toolBar": { "selection": "Selezione", @@ -236,10 +249,13 @@ "link": "Aggiungi/ aggiorna il link per una forma selezionata", "eraser": "Gomma", "frame": "Strumento riquadro", + "magicframe": "", "embeddable": "Incorporamento Web", "laser": "Puntatore laser", "hand": "Mano (strumento di panoramica)", - "extraTools": "Altri strumenti" + "extraTools": "Altri strumenti", + "mermaidToExcalidraw": "", + "magicSettings": "Impostazioni di IA" }, "headings": { "canvasActions": "Azioni sulla Tela", @@ -498,5 +514,12 @@ "description": "Il caricamento da file sostituirà il contenuto esistente.

Puoi salvare il tuo disegno prima usando una delle opzioni qui sotto." } } + }, + "mermaid": { + "title": "", + "button": "Inserisci", + "description": "", + "syntax": "", + "preview": "Anteprima" } } diff --git a/src/locales/ja-JP.json b/packages/excalidraw/locales/ja-JP.json similarity index 97% rename from src/locales/ja-JP.json rename to packages/excalidraw/locales/ja-JP.json index 8951739a6..611a8c6a8 100644 --- a/src/locales/ja-JP.json +++ b/packages/excalidraw/locales/ja-JP.json @@ -11,6 +11,8 @@ "copyAsPng": "PNGとしてクリップボードへコピー", "copyAsSvg": "SVGとしてクリップボードへコピー", "copyText": "テキストとしてクリップボードにコピー", + "copySource": "", + "convertToCode": "", "bringForward": "前面に移動", "sendToBack": "最背面に移動", "bringToFront": "最前面に移動", @@ -36,8 +38,12 @@ "arrowhead_none": "なし", "arrowhead_arrow": "矢印", "arrowhead_bar": "バー", - "arrowhead_dot": "ドット", + "arrowhead_circle": "", + "arrowhead_circle_outline": "", "arrowhead_triangle": "三角", + "arrowhead_triangle_outline": "", + "arrowhead_diamond": "", + "arrowhead_diamond_outline": "", "fontSize": "フォントの大きさ", "fontFamily": "フォントの種類", "addWatermark": "\"Made with Excalidraw\"と表示", @@ -130,7 +136,9 @@ "sidebarLock": "サイドバーを開いたままにする", "selectAllElementsInFrame": "フレーム内のすべての要素を選択", "removeAllElementsFromFrame": "フレーム内のすべての要素を削除", - "eyeDropper": "キャンバスから色を選択" + "eyeDropper": "キャンバスから色を選択", + "textToDiagram": "", + "prompt": "" }, "library": { "noItems": "まだアイテムが追加されていません…", @@ -209,6 +217,7 @@ "importLibraryError": "ライブラリを読み込めませんでした。", "collabSaveFailed": "バックエンドデータベースに保存できませんでした。問題が解決しない場合は、作業を失わないようにローカルにファイルを保存してください。", "collabSaveFailed_sizeExceeded": "キャンバスが大きすぎるため、バックエンドデータベースに保存できませんでした。問題が解決しない場合は、作業を失わないようにローカルにファイルを保存してください。", + "imageToolNotSupported": "", "brave_measure_text_error": { "line1": "Aggressly Block Fingerprinting の設定が有効なBraveブラウザを使用しているようです。", "line2": "これにより、図面の テキスト要素 が壊れる可能性があります。", @@ -217,8 +226,12 @@ }, "libraryElementTypeError": { "embeddable": "", + "iframe": "", "image": "" - } + }, + "asyncPasteFailedOnRead": "", + "asyncPasteFailedOnParse": "", + "copyToSystemClipboardFailed": "" }, "toolBar": { "selection": "選択", @@ -236,10 +249,13 @@ "link": "選択した図形のリンクを追加/更新", "eraser": "消しゴム", "frame": "フレームツール", + "magicframe": "", "embeddable": "Web埋め込み", "laser": "", "hand": "手 (パンニングツール)", - "extraTools": "その他のツール" + "extraTools": "その他のツール", + "mermaidToExcalidraw": "", + "magicSettings": "" }, "headings": { "canvasActions": "キャンバス操作", @@ -498,5 +514,12 @@ "description": "" } } + }, + "mermaid": { + "title": "", + "button": "", + "description": "", + "syntax": "", + "preview": "" } } diff --git a/src/locales/kaa.json b/packages/excalidraw/locales/kaa.json similarity index 88% rename from src/locales/kaa.json rename to packages/excalidraw/locales/kaa.json index c04589095..cdca8cbe6 100644 --- a/src/locales/kaa.json +++ b/packages/excalidraw/locales/kaa.json @@ -9,8 +9,10 @@ "cut": "Qıyıw", "copy": "Kóshirip alıw", "copyAsPng": "Almasıw buferine PNG retinde kóshirip alıw", - "copyAsSvg": "", - "copyText": "", + "copyAsSvg": "Almasıw buferine SVG retinde kóshirip alıw", + "copyText": "Almasıw buferine tekst retinde kóshirip alıw", + "copySource": "", + "convertToCode": "", "bringForward": "", "sendToBack": "", "bringToFront": "", @@ -33,19 +35,23 @@ "sharp": "", "round": "", "arrowheads": "", - "arrowhead_none": "", + "arrowhead_none": "Joq", "arrowhead_arrow": "Jebe", "arrowhead_bar": "", - "arrowhead_dot": "Noqat", + "arrowhead_circle": "", + "arrowhead_circle_outline": "", "arrowhead_triangle": "", + "arrowhead_triangle_outline": "", + "arrowhead_diamond": "", + "arrowhead_diamond_outline": "", "fontSize": "Shrift ólshemi", - "fontFamily": "", + "fontFamily": "Shrift toplamı", "addWatermark": "", "handDrawn": "", "normal": "", "code": "Kod", "small": "", - "medium": "", + "medium": "Ortasha", "large": "Úlken", "veryLarge": "Júdá úlken", "solid": "", @@ -59,14 +65,14 @@ "right": "", "extraBold": "", "architect": "", - "artist": "", + "artist": "Súwretshi", "cartoonist": "", "fileTitle": "Fayl ataması", - "colorPicker": "", + "colorPicker": "Reńdi tańlaw", "canvasColors": "", "canvasBackground": "", "drawingCanvas": "", - "layers": "", + "layers": "Qatlamlar", "actions": "Háreketler", "language": "Til", "liveCollaboration": "", @@ -130,7 +136,9 @@ "sidebarLock": "", "selectAllElementsInFrame": "", "removeAllElementsFromFrame": "", - "eyeDropper": "" + "eyeDropper": "", + "textToDiagram": "", + "prompt": "" }, "library": { "noItems": "", @@ -170,7 +178,7 @@ "clear": "Tazalaw", "remove": "Óshiriw", "embed": "", - "publishLibrary": "", + "publishLibrary": "Jariyalaw", "submit": "Jiberiw", "confirm": "Tastıyıqlaw", "embeddableInteractionButton": "" @@ -209,6 +217,7 @@ "importLibraryError": "Kitapxananı júklew ámelge aspadı", "collabSaveFailed": "", "collabSaveFailed_sizeExceeded": "", + "imageToolNotSupported": "", "brave_measure_text_error": { "line1": "", "line2": "", @@ -217,8 +226,12 @@ }, "libraryElementTypeError": { "embeddable": "", + "iframe": "", "image": "" - } + }, + "asyncPasteFailedOnRead": "", + "asyncPasteFailedOnParse": "", + "copyToSystemClipboardFailed": "" }, "toolBar": { "selection": "", @@ -236,10 +249,13 @@ "link": "", "eraser": "Óshirgish", "frame": "", + "magicframe": "", "embeddable": "", "laser": "", "hand": "", - "extraTools": "" + "extraTools": "", + "mermaidToExcalidraw": "", + "magicSettings": "" }, "headings": { "canvasActions": "", @@ -271,7 +287,7 @@ "disableSnapping": "" }, "canvasError": { - "cannotShowPreview": "", + "cannotShowPreview": "Aldınnan kóriwdi kórsetiw múmkin emes", "canvasTooBig": "", "canvasTooBigTip": "" }, @@ -350,15 +366,15 @@ "website": "Veb-sayt", "placeholder": { "authorName": "Atıńız yamasa paydalanıwshı atı", - "libraryName": "", + "libraryName": "Kitapxanańız ataması", "libraryDesc": "", "githubHandle": "", "twitterHandle": "", - "website": "" + "website": "Jeke veb-saytıńız yamasa basqa saytqa silteme (májbúriy emes)" }, "errors": { - "required": "", - "website": "" + "required": "Májbúriy", + "website": "Jaramlı URL mánzil kirgiziń" }, "noteDescription": "", "noteGuidelines": "", @@ -368,7 +384,7 @@ "republishWarning": "" }, "publishSuccessDialog": { - "title": "", + "title": "Kitapxana jiberildi", "content": "" }, "confirmDialog": { @@ -382,7 +398,7 @@ "onlySelected": "", "darkMode": "Qarańǵı tema", "embedScene": "", - "scale": "", + "scale": "Kólem", "padding": "" }, "tooltip": { @@ -416,7 +432,7 @@ "version": "Versiya", "versionCopy": "Kóshirip alıw ushın basıń", "versionNotAvailable": "", - "width": "" + "width": "Eni" }, "toast": { "addedToLibrary": "Kitapxanaǵa qosıldı", @@ -451,18 +467,18 @@ "welcomeScreen": { "app": { "center_heading": "", - "center_heading_plus": "", + "center_heading_plus": "Excalidraw+ ge ótiwdi qáleysiz be?", "menuHint": "Eksportlaw, sazlawlar, tiller, ..." }, "defaults": { "menuHint": "Eksportlaw, sazlawlar hám basqa...", - "center_heading": "", + "center_heading": "Diagrammalar. Ápiwayı.", "toolbarHint": "", "helpHint": "" } }, "colorPicker": { - "mostUsedCustomColors": "", + "mostUsedCustomColors": "Kóp qollanılatuǵın arnawlı reńler", "colors": "Reńler", "shades": "", "hexCode": "", @@ -498,5 +514,12 @@ "description": "" } } + }, + "mermaid": { + "title": "", + "button": "", + "description": "", + "syntax": "", + "preview": "" } } diff --git a/src/locales/kab-KAB.json b/packages/excalidraw/locales/kab-KAB.json similarity index 97% rename from src/locales/kab-KAB.json rename to packages/excalidraw/locales/kab-KAB.json index ffd3aa079..ed10f51d0 100644 --- a/src/locales/kab-KAB.json +++ b/packages/excalidraw/locales/kab-KAB.json @@ -11,6 +11,8 @@ "copyAsPng": "Nɣel ɣer tecfawit am PNG", "copyAsSvg": "Nɣel ɣer tecfawit am SVG", "copyText": "Nɣel ɣer tecfawit am uḍris", + "copySource": "", + "convertToCode": "", "bringForward": "Awi ɣer sdat", "sendToBack": "Awi s agilal", "bringToFront": "Err ɣer deffir", @@ -36,8 +38,12 @@ "arrowhead_none": "Ulac", "arrowhead_arrow": "Taneccabt", "arrowhead_bar": "Afeggag", - "arrowhead_dot": "Tanqiḍt", + "arrowhead_circle": "", + "arrowhead_circle_outline": "", "arrowhead_triangle": "Akerdis", + "arrowhead_triangle_outline": "", + "arrowhead_diamond": "", + "arrowhead_diamond_outline": "", "fontSize": "Tiddi n tsefsit", "fontFamily": "Tawacult n tsefsiyin", "addWatermark": "Seddu \"Yettwaxdem s Excalidraw\"", @@ -130,7 +136,9 @@ "sidebarLock": "Eǧǧ afeggag n yidis yeldi", "selectAllElementsInFrame": "", "removeAllElementsFromFrame": "", - "eyeDropper": "" + "eyeDropper": "", + "textToDiagram": "", + "prompt": "" }, "library": { "noItems": "Ulac iferdisen yettwarnan yakan...", @@ -209,6 +217,7 @@ "importLibraryError": "Ur d-ssalay ara tamkarḍit", "collabSaveFailed": "Ulamek asekles deg uzadur n yisefka deg ugilal. Ma ikemmel wugur, isefk ad teskelseḍ afaylu s wudem adigan akken ad tetḥeqqeḍ ur tesruḥuyeḍ ara amahil-inek•inem.", "collabSaveFailed_sizeExceeded": "Ulamek asekles deg uzadur n yisefka deg ugilal, taɣzut n usuneɣ tettban-d temqer aṭas. Isefk ad teskelseḍ afaylu s wudem adigan akken ad tetḥeqqeḍ ur tesruḥuyeḍ ara amahil-inek•inem.", + "imageToolNotSupported": "", "brave_measure_text_error": { "line1": "", "line2": "Ayagi yezmer ad d-iglu s truẓi nIferdisen n uḍrisdeg wunuɣen-inek.", @@ -217,8 +226,12 @@ }, "libraryElementTypeError": { "embeddable": "", + "iframe": "", "image": "" - } + }, + "asyncPasteFailedOnRead": "", + "asyncPasteFailedOnParse": "", + "copyToSystemClipboardFailed": "" }, "toolBar": { "selection": "Tafrayt", @@ -236,10 +249,13 @@ "link": "Rnu/leqqem aseɣwen i talɣa yettwafernen", "eraser": "Sfeḍ", "frame": "", + "magicframe": "", "embeddable": "", "laser": "", "hand": "Afus (afecku n usmutti n tmuɣli)", - "extraTools": "" + "extraTools": "", + "mermaidToExcalidraw": "", + "magicSettings": "" }, "headings": { "canvasActions": "Tigawin n teɣzut n usuneɣ", @@ -498,5 +514,12 @@ "description": "" } } + }, + "mermaid": { + "title": "", + "button": "", + "description": "", + "syntax": "", + "preview": "" } } diff --git a/src/locales/kk-KZ.json b/packages/excalidraw/locales/kk-KZ.json similarity index 95% rename from src/locales/kk-KZ.json rename to packages/excalidraw/locales/kk-KZ.json index fd86a8ba7..9b11fcaff 100644 --- a/src/locales/kk-KZ.json +++ b/packages/excalidraw/locales/kk-KZ.json @@ -11,6 +11,8 @@ "copyAsPng": "", "copyAsSvg": "", "copyText": "", + "copySource": "", + "convertToCode": "", "bringForward": "", "sendToBack": "", "bringToFront": "", @@ -36,8 +38,12 @@ "arrowhead_none": "Жоқ", "arrowhead_arrow": "Нұсқар", "arrowhead_bar": "Тосқауыл", - "arrowhead_dot": "Нүкте", + "arrowhead_circle": "", + "arrowhead_circle_outline": "", "arrowhead_triangle": "", + "arrowhead_triangle_outline": "", + "arrowhead_diamond": "", + "arrowhead_diamond_outline": "", "fontSize": "Қаріп өлшемі", "fontFamily": "Қаріп тобы", "addWatermark": "", @@ -130,7 +136,9 @@ "sidebarLock": "", "selectAllElementsInFrame": "", "removeAllElementsFromFrame": "", - "eyeDropper": "" + "eyeDropper": "", + "textToDiagram": "", + "prompt": "" }, "library": { "noItems": "", @@ -209,6 +217,7 @@ "importLibraryError": "", "collabSaveFailed": "", "collabSaveFailed_sizeExceeded": "", + "imageToolNotSupported": "", "brave_measure_text_error": { "line1": "", "line2": "", @@ -217,8 +226,12 @@ }, "libraryElementTypeError": { "embeddable": "", + "iframe": "", "image": "" - } + }, + "asyncPasteFailedOnRead": "", + "asyncPasteFailedOnParse": "", + "copyToSystemClipboardFailed": "" }, "toolBar": { "selection": "", @@ -236,10 +249,13 @@ "link": "", "eraser": "", "frame": "", + "magicframe": "", "embeddable": "", "laser": "", "hand": "", - "extraTools": "" + "extraTools": "", + "mermaidToExcalidraw": "", + "magicSettings": "" }, "headings": { "canvasActions": "", @@ -498,5 +514,12 @@ "description": "" } } + }, + "mermaid": { + "title": "", + "button": "", + "description": "", + "syntax": "", + "preview": "" } } diff --git a/src/locales/km-KH.json b/packages/excalidraw/locales/km-KH.json similarity index 98% rename from src/locales/km-KH.json rename to packages/excalidraw/locales/km-KH.json index 9ad1ac345..3aca3ce9b 100644 --- a/src/locales/km-KH.json +++ b/packages/excalidraw/locales/km-KH.json @@ -11,6 +11,8 @@ "copyAsPng": "ចម្លងទៅក្តារតម្បៀតខ្ទាស់ជា​ PNG", "copyAsSvg": "ចម្លងទៅក្តារតម្បៀតខ្ទាស់ជា​ SVG", "copyText": "ចម្លងទៅក្តារតម្បៀតខ្ទាស់ជា​អត្ថបទ", + "copySource": "", + "convertToCode": "", "bringForward": "នាំយកទៅលើ", "sendToBack": "នាំយកទៅក្រោយបង្អស់", "bringToFront": "នាំយកទៅលើបង្អស់", @@ -36,8 +38,12 @@ "arrowhead_none": "គ្មាន", "arrowhead_arrow": "ព្រួញ", "arrowhead_bar": "របារ", - "arrowhead_dot": "ចំណុច", + "arrowhead_circle": "", + "arrowhead_circle_outline": "", "arrowhead_triangle": "ត្រីកោណ", + "arrowhead_triangle_outline": "", + "arrowhead_diamond": "", + "arrowhead_diamond_outline": "", "fontSize": "ទំហំពុម្ពអក្សរ", "fontFamily": "ក្រុម​ពុម្ពអក្សរ", "addWatermark": "បន្ថែមវ៉ាត់ធើម៉ាក \"Made with Excalidraw\"", @@ -130,7 +136,9 @@ "sidebarLock": "ទុករបារចំហៀងបើក", "selectAllElementsInFrame": "", "removeAllElementsFromFrame": "", - "eyeDropper": "" + "eyeDropper": "", + "textToDiagram": "", + "prompt": "" }, "library": { "noItems": "មិនទាន់មានធាតុបន្ថែមទេ...", @@ -209,6 +217,7 @@ "importLibraryError": "មិនអាចផ្ទុកបណ្ណាល័យបានទេ។", "collabSaveFailed": "មិនអាចរក្សាទុកទៅម៉ាស៊ីនមេបានទេ។ ប្រសិនបើបញ្ហានៅតែបន្តកើតមាន​ អ្នកគួរតែរក្សាទុកឯកសាររបស់អ្នកនៅលើកុំព្យូទ័ររបស់អ្នកសិន ដើម្បីធានាថាការងាររបស់អ្នកមិនបាត់បង់។", "collabSaveFailed_sizeExceeded": "មិនអាចរក្សាទុកទៅម៉ាស៊ីនមេបានទេ, ផ្ទាំងបាវហាក់ដូចជាធំពេក។ អ្នកគួរតែរក្សាទុកឯកសាររបស់អ្នកនៅលើកុំព្យូទ័ររបស់អ្នកសិន ដើម្បីធានាថាការងាររបស់អ្នកមិនបាត់បង់។", + "imageToolNotSupported": "", "brave_measure_text_error": { "line1": "អ្នកហាក់ដូចជាកំពុងប្រើប្រាស់កម្មវិធីរុករកតាមអ៊ីនធឺណិត Brave ជាមួយនឹងការកំណត់ ការពារស្នាមម្រាមដៃយ៉ាងធ្ងន់ធ្ងរ ត្រូវបានបើក។", "line2": "វាអាចបណ្តាលឱ្យមានការបំបែក ធាតុអត្ថបទ នៅក្នុងគំនូររបស់អ្នក។", @@ -217,8 +226,12 @@ }, "libraryElementTypeError": { "embeddable": "", + "iframe": "", "image": "" - } + }, + "asyncPasteFailedOnRead": "", + "asyncPasteFailedOnParse": "", + "copyToSystemClipboardFailed": "" }, "toolBar": { "selection": "ការជ្រើសរើស", @@ -236,10 +249,13 @@ "link": "បន្ថែម/ធ្វើបច្ចុប្បន្នភាពតំណភ្ជាប់សម្រាប់រូបរាងដែលបានជ្រើសរើស", "eraser": "ជ័រលុប", "frame": "", + "magicframe": "", "embeddable": "", "laser": "", "hand": "ដៃ (panning tool)", - "extraTools": "" + "extraTools": "", + "mermaidToExcalidraw": "", + "magicSettings": "" }, "headings": { "canvasActions": "សកម្មភាពបាវ", @@ -498,5 +514,12 @@ "description": "" } } + }, + "mermaid": { + "title": "", + "button": "", + "description": "", + "syntax": "", + "preview": "" } } diff --git a/src/locales/ko-KR.json b/packages/excalidraw/locales/ko-KR.json similarity index 94% rename from src/locales/ko-KR.json rename to packages/excalidraw/locales/ko-KR.json index fb59bea0b..bb518ea34 100644 --- a/src/locales/ko-KR.json +++ b/packages/excalidraw/locales/ko-KR.json @@ -11,6 +11,8 @@ "copyAsPng": "클립보드로 PNG 이미지 복사", "copyAsSvg": "클립보드로 SVG 이미지 복사", "copyText": "클립보드로 텍스트 복사", + "copySource": "소스코드를 클립보드로 복사", + "convertToCode": "코드로 변환", "bringForward": "앞으로 가져오기", "sendToBack": "맨 뒤로 보내기", "bringToFront": "맨 앞으로 가져오기", @@ -36,8 +38,12 @@ "arrowhead_none": "없음", "arrowhead_arrow": "화살표", "arrowhead_bar": "막대", - "arrowhead_dot": "점", + "arrowhead_circle": "원", + "arrowhead_circle_outline": "원 (외곽선)", "arrowhead_triangle": "삼각형", + "arrowhead_triangle_outline": "삼각형 (외곽선)", + "arrowhead_diamond": "마름모", + "arrowhead_diamond_outline": "마름모 (외곽선)", "fontSize": "글자 크기", "fontFamily": "글꼴", "addWatermark": "\"Made with Excalidraw\" 추가", @@ -130,7 +136,9 @@ "sidebarLock": "사이드바 유지", "selectAllElementsInFrame": "프레임의 모든 요소 선택", "removeAllElementsFromFrame": "프레임의 모든 요소 삭제", - "eyeDropper": "캔버스에서 색상 고르기" + "eyeDropper": "캔버스에서 색상 고르기", + "textToDiagram": "텍스트를 다이어그램으로", + "prompt": "프롬프트" }, "library": { "noItems": "추가된 아이템 없음", @@ -209,6 +217,7 @@ "importLibraryError": "라이브러리를 불러오지 못했습니다.", "collabSaveFailed": "데이터베이스에 저장하지 못했습니다. 문제가 계속 된다면, 작업 내용을 잃지 않도록 로컬 저장소에 저장해 주세요.", "collabSaveFailed_sizeExceeded": "데이터베이스에 저장하지 못했습니다. 캔버스가 너무 큰 거 같습니다. 문제가 계속 된다면, 작업 내용을 잃지 않도록 로컬 저장소에 저장해 주세요.", + "imageToolNotSupported": "이미지가 비활성화 되었습니다.", "brave_measure_text_error": { "line1": "귀하께서는 강력한 지문 차단 설정이 활성화된 Brave browser를 사용하고 계신 것 같습니다.", "line2": "이 기능으로 인해 화이트보드의 텍스트 요소들이 손상될 수 있습니다.", @@ -217,8 +226,12 @@ }, "libraryElementTypeError": { "embeddable": "임베드 요소들은 라이브러리에 추가할 수 없습니다.", + "iframe": "IFrame 요소들은 라이브러리에 추가할 수 없습니다.", "image": "라이브러리에 이미지 삽입 기능은 곧 지원될 예정입니다!" - } + }, + "asyncPasteFailedOnRead": "붙여넣는데 실패했습니다. (시스템 클립보드를 읽는데 실패했습니다)", + "asyncPasteFailedOnParse": "붙여넣는데 실패했습니다.", + "copyToSystemClipboardFailed": "클립보드로 복사하는데 실패했습니다." }, "toolBar": { "selection": "선택", @@ -236,10 +249,13 @@ "link": "선택한 도형에 대해서 링크를 추가/업데이트", "eraser": "지우개", "frame": "프레임 도구", + "magicframe": "와이어프레임을 코드로", "embeddable": "웹 임베드", "laser": "레이저 포인터", "hand": "손 (패닝 도구)", - "extraTools": "다른 도구" + "extraTools": "다른 도구", + "mermaidToExcalidraw": "Mermaid에서 불러오기", + "magicSettings": "AI 설정" }, "headings": { "canvasActions": "캔버스 동작", @@ -498,5 +514,12 @@ "description": "외부 작업물을 불러오면 현재 작성된 데이터를 덮어쓰게 됩니다.

다음 옵션 중 하나를 선택하여 작업물을 백업해 둘 수 있습니다." } } + }, + "mermaid": { + "title": "Mermaid에서 불러오기", + "button": "삽입하기", + "description": "지금은 순서도, 시퀀스, 클래스 다이어그램만 지원합니다. 다른 형식들은 Excalidraw에서는 이미지로 표시됩니다.", + "syntax": "Mermaid 구문", + "preview": "미리보기" } } diff --git a/src/locales/ku-TR.json b/packages/excalidraw/locales/ku-TR.json similarity index 97% rename from src/locales/ku-TR.json rename to packages/excalidraw/locales/ku-TR.json index b968d0163..c7e60bbaf 100644 --- a/src/locales/ku-TR.json +++ b/packages/excalidraw/locales/ku-TR.json @@ -11,6 +11,8 @@ "copyAsPng": "PNGلەبەرگرتنەوە بۆ تەختەنووس وەک", "copyAsSvg": "SVGلەبەرگرتنەوە بۆ تەختەنووس وەک", "copyText": "لەبەرگرتنەوە بۆ تەختەنووس وەک نوسین", + "copySource": "", + "convertToCode": "", "bringForward": "بهێنە پێشتر", "sendToBack": "بنێرە دواوە", "bringToFront": "بهێنە پێشەوە", @@ -36,8 +38,12 @@ "arrowhead_none": "هیچیان", "arrowhead_arrow": "تیر", "arrowhead_bar": "هێڵ", - "arrowhead_dot": "خاڵ", + "arrowhead_circle": "", + "arrowhead_circle_outline": "", "arrowhead_triangle": "سێگۆشە", + "arrowhead_triangle_outline": "", + "arrowhead_diamond": "", + "arrowhead_diamond_outline": "", "fontSize": "قەبارەی فۆنت", "fontFamily": "خێزانی فۆنت", "addWatermark": "زیادبکە \"Made with Excalidraw\"", @@ -130,7 +136,9 @@ "sidebarLock": "هێشتنەوەی شریتی لا بە کراوەیی", "selectAllElementsInFrame": "هەموو توخمەکانی ناو چوارچێوەکە دیاری بکە", "removeAllElementsFromFrame": "هەموو توخمەکانی ناو چوارچێوەکە لابەرە", - "eyeDropper": "ڕەنگێک لەسەر تابلۆکە هەڵبژێرە" + "eyeDropper": "ڕەنگێک لەسەر تابلۆکە هەڵبژێرە", + "textToDiagram": "", + "prompt": "" }, "library": { "noItems": "هێشتا هیچ بڕگەیەک زیاد نەکراوە...", @@ -209,6 +217,7 @@ "importLibraryError": "نەیتوانی کتێبخانە بار بکات", "collabSaveFailed": "نەتوانرا لە بنکەدراوەی ڕاژەدا پاشەکەوت بکرێت. ئەگەر کێشەکان بەردەوام بوون، پێویستە فایلەکەت لە ناوخۆدا هەڵبگریت بۆ ئەوەی دڵنیا بیت کە کارەکانت لەدەست نادەیت.", "collabSaveFailed_sizeExceeded": "نەتوانرا لە بنکەدراوەی ڕاژەدا پاشەکەوت بکرێت، پێدەچێت تابلۆکە زۆر گەورە بێت. پێویستە فایلەکە لە ناوخۆدا هەڵبگریت بۆ ئەوەی دڵنیا بیت کە کارەکانت لەدەست نادەیت.", + "imageToolNotSupported": "", "brave_measure_text_error": { "line1": "وادیارە وێبگەڕی Brave بەکاردەهێنیت و ڕێکخستنی Aggressively Block Fingerprinting ـت چالاک کردووە.", "line2": "ئەمە ئەکرێ ببێتە هۆی تێکدانی دانە دەقییەکان لە وێنەکێشانەکانتدا.", @@ -217,8 +226,12 @@ }, "libraryElementTypeError": { "embeddable": "", + "iframe": "", "image": "" - } + }, + "asyncPasteFailedOnRead": "", + "asyncPasteFailedOnParse": "", + "copyToSystemClipboardFailed": "" }, "toolBar": { "selection": "دەستنیشانکردن", @@ -236,10 +249,13 @@ "link": "زیادکردن/ نوێکردنەوەی لینک بۆ شێوەی دیاریکراو", "eraser": "سڕەر", "frame": "ئامرازی چوارچێوە", + "magicframe": "", "embeddable": "", "laser": "", "hand": "دەست (ئامرازی پانکردن)", - "extraTools": "ئامرازی زیاتر" + "extraTools": "ئامرازی زیاتر", + "mermaidToExcalidraw": "", + "magicSettings": "" }, "headings": { "canvasActions": "کردارەکانی تابلۆ", @@ -498,5 +514,12 @@ "description": "" } } + }, + "mermaid": { + "title": "", + "button": "", + "description": "", + "syntax": "", + "preview": "" } } diff --git a/src/locales/lt-LT.json b/packages/excalidraw/locales/lt-LT.json similarity index 95% rename from src/locales/lt-LT.json rename to packages/excalidraw/locales/lt-LT.json index 5dc9b3328..2ddffe6e2 100644 --- a/src/locales/lt-LT.json +++ b/packages/excalidraw/locales/lt-LT.json @@ -11,6 +11,8 @@ "copyAsPng": "Kopijuoti į iškarpinę kaip PNG", "copyAsSvg": "Kopijuoti į iškarpinę kaip SVG", "copyText": "Kopijuoti į iškarpinę kaip tekstą", + "copySource": "", + "convertToCode": "", "bringForward": "Kelti priekio link", "sendToBack": "Nustumti į užnugarį", "bringToFront": "Iškelti į priekį", @@ -36,8 +38,12 @@ "arrowhead_none": "Jokios", "arrowhead_arrow": "Rodyklė", "arrowhead_bar": "Brukšnys", - "arrowhead_dot": "Taškas", + "arrowhead_circle": "", + "arrowhead_circle_outline": "", "arrowhead_triangle": "Trikampis", + "arrowhead_triangle_outline": "", + "arrowhead_diamond": "", + "arrowhead_diamond_outline": "", "fontSize": "Šrifto dydis", "fontFamily": "Šriftas", "addWatermark": "Sukurta su Excalidraw", @@ -130,7 +136,9 @@ "sidebarLock": "", "selectAllElementsInFrame": "", "removeAllElementsFromFrame": "", - "eyeDropper": "" + "eyeDropper": "", + "textToDiagram": "", + "prompt": "" }, "library": { "noItems": "", @@ -209,6 +217,7 @@ "importLibraryError": "Nepavyko įkelti bibliotekos", "collabSaveFailed": "", "collabSaveFailed_sizeExceeded": "", + "imageToolNotSupported": "", "brave_measure_text_error": { "line1": "", "line2": "", @@ -217,8 +226,12 @@ }, "libraryElementTypeError": { "embeddable": "", + "iframe": "", "image": "" - } + }, + "asyncPasteFailedOnRead": "", + "asyncPasteFailedOnParse": "", + "copyToSystemClipboardFailed": "" }, "toolBar": { "selection": "Žymėjimas", @@ -236,10 +249,13 @@ "link": "Pridėti / Atnaujinti pasirinktos figūros nuorodą", "eraser": "Trintukas", "frame": "", + "magicframe": "", "embeddable": "", "laser": "", "hand": "", - "extraTools": "" + "extraTools": "", + "mermaidToExcalidraw": "", + "magicSettings": "" }, "headings": { "canvasActions": "Veiksmai su drobe", @@ -498,5 +514,12 @@ "description": "" } } + }, + "mermaid": { + "title": "", + "button": "", + "description": "", + "syntax": "", + "preview": "" } } diff --git a/src/locales/lv-LV.json b/packages/excalidraw/locales/lv-LV.json similarity index 97% rename from src/locales/lv-LV.json rename to packages/excalidraw/locales/lv-LV.json index df142b63b..edfb28e51 100644 --- a/src/locales/lv-LV.json +++ b/packages/excalidraw/locales/lv-LV.json @@ -11,6 +11,8 @@ "copyAsPng": "Kopēt starpliktuvē kā PNG", "copyAsSvg": "Kopēt starpliktuvē kā SVG", "copyText": "Kopēt starpliktuvē kā tekstu", + "copySource": "", + "convertToCode": "", "bringForward": "Pārvietot vienu slāni augstāk", "sendToBack": "Pārvietot uz zemāko slāni", "bringToFront": "Pārvietot uz virsējo slāni", @@ -36,8 +38,12 @@ "arrowhead_none": "Nekādas", "arrowhead_arrow": "Bulta", "arrowhead_bar": "Svītra", - "arrowhead_dot": "Punkts", + "arrowhead_circle": "", + "arrowhead_circle_outline": "", "arrowhead_triangle": "Trijstūris", + "arrowhead_triangle_outline": "", + "arrowhead_diamond": "", + "arrowhead_diamond_outline": "", "fontSize": "Teksta lielums", "fontFamily": "Fontu saime", "addWatermark": "Pievienot \"Radīts ar Excalidraw\"", @@ -130,7 +136,9 @@ "sidebarLock": "Paturēt atvērtu sānjoslu", "selectAllElementsInFrame": "", "removeAllElementsFromFrame": "", - "eyeDropper": "" + "eyeDropper": "", + "textToDiagram": "", + "prompt": "" }, "library": { "noItems": "Neviena vienība vēl nav pievienota...", @@ -209,6 +217,7 @@ "importLibraryError": "Nevarēja ielādēt bibliotēku", "collabSaveFailed": "Darbs nav saglabāts datubāzē. Ja problēma turpinās, saglabājiet datni lokālajā krātuvē, lai nodrošinātos pret darba pazaudēšanu.", "collabSaveFailed_sizeExceeded": "Darbs nav saglabāts datubāzē, šķiet, ka tāfele ir pārāk liela. Saglabājiet datni lokālajā krātuvē, lai nodrošinātos pret darba pazaudēšanu.", + "imageToolNotSupported": "", "brave_measure_text_error": { "line1": "", "line2": "", @@ -217,8 +226,12 @@ }, "libraryElementTypeError": { "embeddable": "", + "iframe": "", "image": "" - } + }, + "asyncPasteFailedOnRead": "", + "asyncPasteFailedOnParse": "", + "copyToSystemClipboardFailed": "" }, "toolBar": { "selection": "Atlase", @@ -236,10 +249,13 @@ "link": "Pievienot/rediģēt atlasītās figūras saiti", "eraser": "Dzēšgumija", "frame": "", + "magicframe": "", "embeddable": "", "laser": "", "hand": "Roka (panoramēšanas rīks)", - "extraTools": "" + "extraTools": "", + "mermaidToExcalidraw": "", + "magicSettings": "" }, "headings": { "canvasActions": "Tāfeles darbības", @@ -498,5 +514,12 @@ "description": "" } } + }, + "mermaid": { + "title": "", + "button": "", + "description": "", + "syntax": "", + "preview": "" } } diff --git a/src/locales/mr-IN.json b/packages/excalidraw/locales/mr-IN.json similarity index 89% rename from src/locales/mr-IN.json rename to packages/excalidraw/locales/mr-IN.json index 114bbe1f2..5d13a2ffe 100644 --- a/src/locales/mr-IN.json +++ b/packages/excalidraw/locales/mr-IN.json @@ -11,6 +11,8 @@ "copyAsPng": "PNG रूपे फळी वर कॉपी करा", "copyAsSvg": "SVG रूपे फळी वर कॉपी करा", "copyText": "लिखित रूपे फळी वर कॉपी करा", + "copySource": "स्त्रोत फळी वर कॉपी करा", + "convertToCode": "सांकेतिक लिपित रूपांतरित करा", "bringForward": "पुढे पुढे आणा", "sendToBack": "सर्वात मागे करा", "bringToFront": "सर्वात पुढे आणा", @@ -36,8 +38,12 @@ "arrowhead_none": "कुठलाहि नाही", "arrowhead_arrow": "बाण", "arrowhead_bar": "दांडुक", - "arrowhead_dot": "ठिपका", + "arrowhead_circle": "", + "arrowhead_circle_outline": "", "arrowhead_triangle": "त्रिकोण", + "arrowhead_triangle_outline": "", + "arrowhead_diamond": "", + "arrowhead_diamond_outline": "", "fontSize": "अक्षर आकार", "fontFamily": "अक्षर समूह", "addWatermark": "\"एक्सकेलीड्रॉ ने बनवलेलं\" जोडा", @@ -109,12 +115,12 @@ "createContainerFromText": "मजकूर कंटेनर मधे मोडून दाखवा", "link": { "edit": "दुवा संपादन", - "editEmbed": "", + "editEmbed": "कड़ी सम्पादित करा आणि रुतवा", "create": "दुवा तयार करा", - "createEmbed": "", + "createEmbed": "नवीन कड़ी बनवा आणि रुतवा", "label": "दुवा", - "labelEmbed": "", - "empty": "" + "labelEmbed": "कड़ी आणि रूतवणे", + "empty": "कुठलिही कड़ी दिली नाही" }, "lineEditor": { "edit": "रेघ संपादन", @@ -128,9 +134,11 @@ }, "statusPublished": "प्रकाशित करा", "sidebarLock": "साइडबार उघडं ठेवा", - "selectAllElementsInFrame": "", - "removeAllElementsFromFrame": "", - "eyeDropper": "चित्रफलकातून रंग निवडा" + "selectAllElementsInFrame": "चौकटीतले सर्व तत्वांचे चयन करा", + "removeAllElementsFromFrame": "चौकटीतून सर्व काढून टाका", + "eyeDropper": "चित्रफलकातून रंग निवडा", + "textToDiagram": "", + "prompt": "" }, "library": { "noItems": "अजून कोणतेही आइटम जोडलेले नाही...", @@ -169,11 +177,11 @@ "cancel": "रद्द", "clear": "स्वछ", "remove": "हटवा", - "embed": "", + "embed": "रुतवणे उलटे करा", "publishLibrary": "प्रकाशित करा", "submit": "जमा करा", "confirm": "पुष्टि करा", - "embeddableInteractionButton": "" + "embeddableInteractionButton": "संवादा साठी क्लिक करा" }, "alerts": { "clearReset": "पटल स्वच्छ होणार, तुम्हाला खात्री आहे का?", @@ -203,12 +211,13 @@ "imageInsertError": "प्रतिमा आत घालता येत नाही. नंतर पुन्हा प्रयत्न करा...", "fileTooBig": "फाइल फार मोठी आहे. आकाराची कमाल परवानगी {{maxSize}} आहे.", "svgImageInsertError": "एस-वी-जी प्रतिमा आत घालवू शकलो नाही. एस-वी-जी-मार्क-अप यंत्र अयोग्य आहे.", - "failedToFetchImage": "", + "failedToFetchImage": "प्रतिमा आणणे नाही जमले.", "invalidSVGString": "अयोग्य एस-वी-जी.", "cannotResolveCollabServer": "कॉलेब-सर्वर हे पोहोचत नाही आहे. पान परत लोड करायचा प्रयत्न करावे.", "importLibraryError": "संग्रह प्रतिस्थापित नाही करता आला", "collabSaveFailed": "काही कारणा निमित्त आतल्या डेटाबेसमध्ये जतन करू शकत नाही। समस्या तशिस राहिल्यास, तुम्ही तुमचे काम गमावणार नाही याची खात्री करण्यासाठी तुम्ही तुमची फाइल स्थानिक जतन करावी.", "collabSaveFailed_sizeExceeded": "लगता है कि पृष्ठ तल काफ़ी बड़ा है, इस्कारण अंदरूनी डेटाबेस में सहेजा नहीं जा सका। किये काम को खोने न देने के लिये अपनी फ़ाइल को स्थानीय रूप से सहेजे।\n\nबॅकएंड डेटाबेसमध्ये जतन करू शकत नाही, कॅनव्हास खूप मोठा असल्याचे दिसते. तुम्ही तुमचे काम गमावणार नाही याची खात्री करण्यासाठी तुम्ही फाइल स्थानिक पातळीवर जतन करावी.", + "imageToolNotSupported": "प्रतिमां अक्षम केली गेली आहेत.", "brave_measure_text_error": { "line1": "असं वाटते की तुम्हीं Brave ब्राउज़र वापरतात आहात आणि त्या बरोबार आक्रामक पद्धति चें बोटांचे ठसे हां सेटिंग्स चा विकल्प चयन केलेला आहे.", "line2": "हे तुमच्या चित्रांच्या पाठ तत्वांनां खंडित करू शकतात.", @@ -216,9 +225,13 @@ "line4": "ही सेटिंग अक्षम करूनही पृष्ठ योग्यरित्या प्रदर्शित होत नसल्यास, आमच्या GitHub वर समस्या सबमिट करा, किव्हा डिस्कॉर्ड वर आम्हाला लिहा" }, "libraryElementTypeError": { - "embeddable": "", - "image": "" - } + "embeddable": "रुतलेले तत्व समूह कोषात जोडले जाऊ शकत नाही.", + "iframe": "आयफ़्रेम तत्व समूहकोषात जोडला जाऊ शकत नाही.", + "image": "प्रतिमा समूह कोषात जोड़ायचे लवकरच येत आहेत!" + }, + "asyncPasteFailedOnRead": "चिटकवता नाही जमले ( सिस्टम क्लिप्बॉर्ड पासून वाचणे नाही जमले).", + "asyncPasteFailedOnParse": "चिटकवता नाही जमले.", + "copyToSystemClipboardFailed": "क्लिपबोर्ड वर प्रतिलिपि करणे नाही जमले." }, "toolBar": { "selection": "निवड", @@ -235,11 +248,14 @@ "penMode": "पेन चा मोड - स्पर्श टाळा", "link": "निवडलेल्या आकारासाठी दुवा जोडा/बदल करा", "eraser": "खोड रबर", - "frame": "", - "embeddable": "", + "frame": "चौकट यंत्र", + "magicframe": "वायरफ़्रेम पासून सांकेतिक लिपि", + "embeddable": "वेब रुतवा", "laser": "लेसर टॉर्च", "hand": "हात ( सरकवण्या चे उपकरण)", - "extraTools": "" + "extraTools": "आणिक यंत्रे", + "mermaidToExcalidraw": "मर्मेड पासून एक्सकाली मधे", + "magicSettings": "कृतिम बुद्दिवत्ता सेटिंग्स" }, "headings": { "canvasActions": "पटल क्रिया", @@ -251,7 +267,7 @@ "linearElement": "अनेक बिंदु साठी क्लिक करा, रेघे साठी ड्रैग करा", "freeDraw": "क्लिक आणि ड्रैग करा, झालं तेव्हा सोडा", "text": "टीप: तुम्हीं निवड यंत्रानी कोठेही दुहेरी क्लिक करून टेक्स्ट जोडू शकता", - "embeddable": "", + "embeddable": "वेबसाइट रुतोण्या साठी दाबून-खेचा (क्लिक-ड्रैग करा)", "text_selected": "लेखन संपादन साठी दुहेरी क्लिक करा किव्हा एंटर दाबा", "text_editing": "संपादन संपवायचं असल्यास एस्केप दाबा किव्हा कंट्रोल या कम्मांड बरोबार एंटर दाबा", "linearElementMulti": "शेवटच्या बिंदु वर क्लिक करा किव्हा एस्केप या एंटर दाबा", @@ -376,27 +392,27 @@ "removeItemsFromLib": "निवडलेले आयटम्स संग्रहातून काढा" }, "imageExportDialog": { - "header": "", + "header": "प्रतिमा निर्यात करा", "label": { - "withBackground": "", - "onlySelected": "", - "darkMode": "", - "embedScene": "", - "scale": "", - "padding": "" + "withBackground": "पार्श्वभूमि", + "onlySelected": "फक्त चयन केलेले", + "darkMode": "अंधारमय स्थिति", + "embedScene": "दृश्य रुतवा", + "scale": "क़िती पट", + "padding": "ग़ादी" }, "tooltip": { - "embedScene": "" + "embedScene": "दृश्य डेटा निर्यात केलेल्या पी-एन-जी किव्हा एस-वी-जी फ़ाईल मधे सुरक्षीत केला जाईल, त्याने दृश्य पुनः परत आणता येईल. निर्यात केलेली फ़ाईल चा आकार त्याने वाढेल." }, "title": { - "exportToPng": "", - "exportToSvg": "", - "copyPngToClipboard": "" + "exportToPng": "पी-एन-जी स्वरूपात बाहेर ठेवा", + "exportToSvg": "एस-वी-जी स्वरूपात बाहेर ठेवा", + "copyPngToClipboard": "पी-एन-जी रूपे फळी वर कॉपी करा" }, "button": { - "exportToPng": "", - "exportToSvg": "", - "copyPngToClipboard": "" + "exportToPng": "पीएनजी", + "exportToSvg": "एसविज़ी", + "copyPngToClipboard": "फळी वर कॉपी करा" } }, "encrypted": { @@ -428,8 +444,8 @@ "canvas": "पटल", "selection": "निवड", "pasteAsSingleElement": "एक घटक म्हणून चिपकावण्या साठी {{shortcut}} वापरा,\nकिंवा विद्यमान मजकूर संपादकात चिपकवा", - "unableToEmbed": "", - "unrecognizedLinkFormat": "" + "unableToEmbed": "युआरेल रूतवणे प्रतिबंधित आहेत. आपली युआरेल श्वेतसूचित आणण्या साठी कृपया एक मुद्दा ग़िटहब वर उठवा", + "unrecognizedLinkFormat": "जी युआरेल तुम्हीं रतवली आहे, ती आपेक्षित प्रकारे नाही आहे. कृपया स्त्रोत साइट नी 'रूतवण्या साठी दिलेलि लिपि' चिपकावण्याचा प्रयास करा" }, "colors": { "transparent": "पारदर्शक", @@ -498,5 +514,12 @@ "description": "बाहरी चित्र लोड केल्या वर ते तुमचा सध्याचा कामा ठिकाणि एईल

तुम्हीं तुमचं चित्र एकाधं खाली दिलेलं विकल्प निवडुन पहले सुरक्षीत करु शकता." } } + }, + "mermaid": { + "title": "मर्मेड पासून एक्सकाली मधे", + "button": "शिरवा", + "description": "", + "syntax": "मर्मेड संरचना नियम", + "preview": "पूर्वावलोकन" } } diff --git a/src/locales/my-MM.json b/packages/excalidraw/locales/my-MM.json similarity index 96% rename from src/locales/my-MM.json rename to packages/excalidraw/locales/my-MM.json index 2a54c0af2..290fa4ae4 100644 --- a/src/locales/my-MM.json +++ b/packages/excalidraw/locales/my-MM.json @@ -11,6 +11,8 @@ "copyAsPng": "PNG အနေဖြင့်ကူး", "copyAsSvg": "SVG အနေဖြင့်ကူး", "copyText": "", + "copySource": "", + "convertToCode": "", "bringForward": "ရှေ့ပို့", "sendToBack": "နောက်ဆုံးထား", "bringToFront": "ရှေ့ဆုံးထား", @@ -36,8 +38,12 @@ "arrowhead_none": "ဘာမျှမရှိ", "arrowhead_arrow": "မြှား", "arrowhead_bar": "", - "arrowhead_dot": "အစက်", + "arrowhead_circle": "", + "arrowhead_circle_outline": "", "arrowhead_triangle": "", + "arrowhead_triangle_outline": "", + "arrowhead_diamond": "", + "arrowhead_diamond_outline": "", "fontSize": "စာလုံးအရွယ်", "fontFamily": "စာလုံးပုံစံ", "addWatermark": "\"Excalidraw ဖြင့်ဖန်တီးသည်။\" စာသားထည့်", @@ -130,7 +136,9 @@ "sidebarLock": "", "selectAllElementsInFrame": "", "removeAllElementsFromFrame": "", - "eyeDropper": "" + "eyeDropper": "", + "textToDiagram": "", + "prompt": "" }, "library": { "noItems": "", @@ -209,6 +217,7 @@ "importLibraryError": "", "collabSaveFailed": "", "collabSaveFailed_sizeExceeded": "", + "imageToolNotSupported": "", "brave_measure_text_error": { "line1": "", "line2": "", @@ -217,8 +226,12 @@ }, "libraryElementTypeError": { "embeddable": "", + "iframe": "", "image": "" - } + }, + "asyncPasteFailedOnRead": "", + "asyncPasteFailedOnParse": "", + "copyToSystemClipboardFailed": "" }, "toolBar": { "selection": "ရွေးချယ်", @@ -236,10 +249,13 @@ "link": "", "eraser": "", "frame": "", + "magicframe": "", "embeddable": "", "laser": "", "hand": "", - "extraTools": "" + "extraTools": "", + "mermaidToExcalidraw": "", + "magicSettings": "" }, "headings": { "canvasActions": "ကားချပ်လုပ်ဆောင်ချက်", @@ -498,5 +514,12 @@ "description": "" } } + }, + "mermaid": { + "title": "", + "button": "", + "description": "", + "syntax": "", + "preview": "" } } diff --git a/src/locales/nb-NO.json b/packages/excalidraw/locales/nb-NO.json similarity index 97% rename from src/locales/nb-NO.json rename to packages/excalidraw/locales/nb-NO.json index 8ebc986e3..13372b59f 100644 --- a/src/locales/nb-NO.json +++ b/packages/excalidraw/locales/nb-NO.json @@ -11,6 +11,8 @@ "copyAsPng": "Kopier til PNG", "copyAsSvg": "Kopier til utklippstavlen som SVG", "copyText": "Kopier til utklippstavlen som tekst", + "copySource": "", + "convertToCode": "", "bringForward": "Flytt framover", "sendToBack": "Send bakerst", "bringToFront": "Flytt forrest", @@ -36,8 +38,12 @@ "arrowhead_none": "Ingen", "arrowhead_arrow": "Pil", "arrowhead_bar": "Søyle", - "arrowhead_dot": "Prikk", + "arrowhead_circle": "", + "arrowhead_circle_outline": "", "arrowhead_triangle": "Trekant", + "arrowhead_triangle_outline": "", + "arrowhead_diamond": "", + "arrowhead_diamond_outline": "", "fontSize": "Skriftstørrelse", "fontFamily": "Fontfamilie", "addWatermark": "Legg til \"Laget med Excalidraw\"", @@ -130,7 +136,9 @@ "sidebarLock": "Holde sidemenyen åpen", "selectAllElementsInFrame": "Velg alle elementene i rammen", "removeAllElementsFromFrame": "Fjern alle elementer fra rammen", - "eyeDropper": "Velg farge fra lerretet" + "eyeDropper": "Velg farge fra lerretet", + "textToDiagram": "", + "prompt": "" }, "library": { "noItems": "Ingen elementer lagt til ennå...", @@ -209,6 +217,7 @@ "importLibraryError": "Kunne ikke laste bibliotek", "collabSaveFailed": "Kan ikke lagre i backend-databasen. Hvis problemer vedvarer, bør du lagre filen lokalt for å sikre at du ikke mister arbeidet.", "collabSaveFailed_sizeExceeded": "Kunne ikke lagre til backend-databasen, lerretet ser ut til å være for stort. Du bør lagre filen lokalt for å sikre at du ikke mister arbeidet ditt.", + "imageToolNotSupported": "", "brave_measure_text_error": { "line1": "Ser ut som om du bruker Brave nettleser med Aggressivt Block Finger -innstillingen aktivert.", "line2": "Dette kan resultere i å bryte tekst-elementene i tegningene.", @@ -217,8 +226,12 @@ }, "libraryElementTypeError": { "embeddable": "Innebygde elementer kan ikke legges til i biblioteket.", + "iframe": "", "image": "Støtte for å legge til bilder i biblioteket kommer snart!" - } + }, + "asyncPasteFailedOnRead": "", + "asyncPasteFailedOnParse": "", + "copyToSystemClipboardFailed": "" }, "toolBar": { "selection": "Velg", @@ -236,10 +249,13 @@ "link": "Legg til / oppdater link for en valgt figur", "eraser": "Viskelær", "frame": "Rammeverktøy", + "magicframe": "", "embeddable": "Nettinnbygging", "laser": "", "hand": "Hånd (panoreringsverktøy)", - "extraTools": "Flere verktøy" + "extraTools": "Flere verktøy", + "mermaidToExcalidraw": "", + "magicSettings": "" }, "headings": { "canvasActions": "Handlinger: lerret", @@ -498,5 +514,12 @@ "description": "Lasting av ekstern tegning vil erstatte ditt eksisterende innhold.

Du kan sikkerhetskopiere tegningen din først ved å bruke en av valgene nedenfor." } } + }, + "mermaid": { + "title": "", + "button": "", + "description": "", + "syntax": "", + "preview": "" } } diff --git a/src/locales/nl-NL.json b/packages/excalidraw/locales/nl-NL.json similarity index 96% rename from src/locales/nl-NL.json rename to packages/excalidraw/locales/nl-NL.json index 84471044b..5ef98343b 100644 --- a/src/locales/nl-NL.json +++ b/packages/excalidraw/locales/nl-NL.json @@ -11,6 +11,8 @@ "copyAsPng": "Kopieer als PNG", "copyAsSvg": "Kopieer naar klembord als SVG", "copyText": "Kopieer naar klembord als tekst", + "copySource": "", + "convertToCode": "", "bringForward": "Breng naar voren", "sendToBack": "Stuur naar achtergrond", "bringToFront": "Breng naar voorgrond", @@ -36,8 +38,12 @@ "arrowhead_none": "Geen", "arrowhead_arrow": "Pijl", "arrowhead_bar": "Balk", - "arrowhead_dot": "Punt", + "arrowhead_circle": "", + "arrowhead_circle_outline": "", "arrowhead_triangle": "Driehoek", + "arrowhead_triangle_outline": "", + "arrowhead_diamond": "", + "arrowhead_diamond_outline": "", "fontSize": "Tekstgrootte", "fontFamily": "Lettertype", "addWatermark": "Voeg \"Gemaakt met Excalidraw\" toe", @@ -130,7 +136,9 @@ "sidebarLock": "Zijbalk open houden", "selectAllElementsInFrame": "", "removeAllElementsFromFrame": "", - "eyeDropper": "" + "eyeDropper": "", + "textToDiagram": "", + "prompt": "" }, "library": { "noItems": "Nog geen items toegevoegd...", @@ -209,6 +217,7 @@ "importLibraryError": "Kon bibliotheek niet laden", "collabSaveFailed": "Kan niet opslaan in de backend database. Als de problemen blijven bestaan, moet u het bestand lokaal opslaan om ervoor te zorgen dat u uw werk niet verliest.", "collabSaveFailed_sizeExceeded": "Kan de backend database niet opslaan, het canvas lijkt te groot te zijn. U moet het bestand lokaal opslaan om ervoor te zorgen dat u uw werk niet verliest.", + "imageToolNotSupported": "", "brave_measure_text_error": { "line1": "", "line2": "", @@ -217,8 +226,12 @@ }, "libraryElementTypeError": { "embeddable": "Ingesloten elementen kunnen niet worden toegevoegd aan de bibliotheek.", + "iframe": "", "image": "Ondersteuning voor het toevoegen van afbeeldingen aan de bibliotheek komt binnenkort!" - } + }, + "asyncPasteFailedOnRead": "", + "asyncPasteFailedOnParse": "", + "copyToSystemClipboardFailed": "" }, "toolBar": { "selection": "Selectie", @@ -236,10 +249,13 @@ "link": "Link toevoegen / bijwerken voor een geselecteerde vorm", "eraser": "Gum", "frame": "", + "magicframe": "", "embeddable": "Web insluiten", "laser": "", "hand": "", - "extraTools": "" + "extraTools": "", + "mermaidToExcalidraw": "", + "magicSettings": "" }, "headings": { "canvasActions": "Canvasacties", @@ -498,5 +514,12 @@ "description": "" } } + }, + "mermaid": { + "title": "", + "button": "", + "description": "", + "syntax": "", + "preview": "" } } diff --git a/src/locales/nn-NO.json b/packages/excalidraw/locales/nn-NO.json similarity index 96% rename from src/locales/nn-NO.json rename to packages/excalidraw/locales/nn-NO.json index 8d110945b..6658dba2b 100644 --- a/src/locales/nn-NO.json +++ b/packages/excalidraw/locales/nn-NO.json @@ -11,6 +11,8 @@ "copyAsPng": "Kopier til utklippstavla som PNG", "copyAsSvg": "Kopier til utklippstavla som SVG", "copyText": "", + "copySource": "", + "convertToCode": "", "bringForward": "Flytt framover", "sendToBack": "Send heilt bak", "bringToFront": "Flytt heilt fram", @@ -36,8 +38,12 @@ "arrowhead_none": "Ingen", "arrowhead_arrow": "Pil", "arrowhead_bar": "Stolpe", - "arrowhead_dot": "Prikk", + "arrowhead_circle": "", + "arrowhead_circle_outline": "", "arrowhead_triangle": "Trekant", + "arrowhead_triangle_outline": "", + "arrowhead_diamond": "", + "arrowhead_diamond_outline": "", "fontSize": "Skriftstorleik", "fontFamily": "Skrifttype", "addWatermark": "Legg til «Laga med Excalidraw»", @@ -130,7 +136,9 @@ "sidebarLock": "", "selectAllElementsInFrame": "", "removeAllElementsFromFrame": "", - "eyeDropper": "" + "eyeDropper": "", + "textToDiagram": "", + "prompt": "" }, "library": { "noItems": "", @@ -209,6 +217,7 @@ "importLibraryError": "", "collabSaveFailed": "", "collabSaveFailed_sizeExceeded": "", + "imageToolNotSupported": "", "brave_measure_text_error": { "line1": "", "line2": "", @@ -217,8 +226,12 @@ }, "libraryElementTypeError": { "embeddable": "", + "iframe": "", "image": "" - } + }, + "asyncPasteFailedOnRead": "", + "asyncPasteFailedOnParse": "", + "copyToSystemClipboardFailed": "" }, "toolBar": { "selection": "Vel", @@ -236,10 +249,13 @@ "link": "Legg til/ oppdater lenke til valt figur", "eraser": "Viskelêr", "frame": "", + "magicframe": "", "embeddable": "", "laser": "", "hand": "", - "extraTools": "" + "extraTools": "", + "mermaidToExcalidraw": "", + "magicSettings": "" }, "headings": { "canvasActions": "Handlingar: lerret", @@ -498,5 +514,12 @@ "description": "" } } + }, + "mermaid": { + "title": "", + "button": "", + "description": "", + "syntax": "", + "preview": "" } } diff --git a/src/locales/oc-FR.json b/packages/excalidraw/locales/oc-FR.json similarity index 85% rename from src/locales/oc-FR.json rename to packages/excalidraw/locales/oc-FR.json index 37b9e91d4..3d9594c04 100644 --- a/src/locales/oc-FR.json +++ b/packages/excalidraw/locales/oc-FR.json @@ -11,6 +11,8 @@ "copyAsPng": "Copiar al quichapapièrs coma PNG", "copyAsSvg": "Copiar al quichapapièrs coma SVG", "copyText": "Copiar al quichapapièrs coma tèxt", + "copySource": "", + "convertToCode": "", "bringForward": "En avant", "sendToBack": "En arrièr", "bringToFront": "A l’endavant", @@ -36,8 +38,12 @@ "arrowhead_none": "Cap", "arrowhead_arrow": "Sageta", "arrowhead_bar": "Barra", - "arrowhead_dot": "Ponch", + "arrowhead_circle": "", + "arrowhead_circle_outline": "", "arrowhead_triangle": "Triangle", + "arrowhead_triangle_outline": "", + "arrowhead_diamond": "", + "arrowhead_diamond_outline": "", "fontSize": "Talha polissa", "fontFamily": "Familha de polissa", "addWatermark": "Apondre « Fabricat amb Excalidraw »", @@ -50,7 +56,7 @@ "veryLarge": "Gradassa", "solid": "Solide", "hachure": "Raia", - "zigzag": "", + "zigzag": "Zigzag", "crossHatch": "Raia crosada", "thin": "Fin", "bold": "Espés", @@ -106,15 +112,15 @@ "increaseFontSize": "Aumentar talha polissa", "unbindText": "Dessociar lo tèxte", "bindText": "Ligar lo tèxt al contenidor", - "createContainerFromText": "", + "createContainerFromText": "Envelopar lo tèxte dins un contenedor", "link": { "edit": "Modificar lo ligam", - "editEmbed": "", + "editEmbed": "Modificar lo ligam e l’integracion", "create": "Crear un ligam", - "createEmbed": "", + "createEmbed": "Crear un ligam e son integracion", "label": "Ligam", - "labelEmbed": "", - "empty": "" + "labelEmbed": "Ligam e integracion", + "empty": "Cap de ligam pas definit" }, "lineEditor": { "edit": "Modificar la linha", @@ -128,9 +134,11 @@ }, "statusPublished": "Publicat", "sidebarLock": "Gardar la barra laterala dobèrta", - "selectAllElementsInFrame": "", - "removeAllElementsFromFrame": "", - "eyeDropper": "" + "selectAllElementsInFrame": "Seleccionar totes los elements del quadre", + "removeAllElementsFromFrame": "Tirar totes los elements d’al quadre", + "eyeDropper": "Prendre la color a partir d’un canabàs", + "textToDiagram": "", + "prompt": "" }, "library": { "noItems": "Cap d’element pas encara apondut...", @@ -164,16 +172,16 @@ "darkMode": "Mòde escur", "lightMode": "Mòde clar", "zenMode": "Mòde escur", - "objectsSnapMode": "", + "objectsSnapMode": "Ancorar als objèctes", "exitZenMode": "Sortir del mòde zen", "cancel": "Anullar", "clear": "Escafar", "remove": "Tirar", - "embed": "", + "embed": "Alternar l’integracion", "publishLibrary": "Publicar", "submit": "Enviar", "confirm": "Confirmar", - "embeddableInteractionButton": "" + "embeddableInteractionButton": "Clicar per interagir" }, "alerts": { "clearReset": "Aquò suprimirà lo canabàs complèt. O volètz vertadièrament ?", @@ -203,12 +211,13 @@ "imageInsertError": "Insercion d’imatge impossibla. Tornatz ensajar mai tard...", "fileTooBig": "Fichièr tròp pesuc. La talha maximala autorizada es {{maxSize}}.", "svgImageInsertError": "Insercion d’imatge SVG impossibla. Las balisas SVG semblan invalidas.", - "failedToFetchImage": "", + "failedToFetchImage": "Fracàs de la recuperacion de l’imatge.", "invalidSVGString": "SVG invalid.", "cannotResolveCollabServer": "Connexion impossibla al servidor collab. Mercés de recargar la pagina e tornar ensajar.", "importLibraryError": "Impossible de cargar la bibliotèca", "collabSaveFailed": "", "collabSaveFailed_sizeExceeded": "", + "imageToolNotSupported": "", "brave_measure_text_error": { "line1": "", "line2": "", @@ -217,8 +226,12 @@ }, "libraryElementTypeError": { "embeddable": "", + "iframe": "", "image": "" - } + }, + "asyncPasteFailedOnRead": "Empegatge impossible (lectura impossibla a partir del quichapapièrs).", + "asyncPasteFailedOnParse": "Empegatge impossible.", + "copyToSystemClipboardFailed": "Còpia impossibla al quichapapièrs." }, "toolBar": { "selection": "Seleccion", @@ -235,11 +248,14 @@ "penMode": "Mòde estilo - empachar lo contact", "link": "Apondre/Actualizar lo ligam per una fòrma seleccionada", "eraser": "Goma", - "frame": "", - "embeddable": "", - "laser": "", + "frame": "Esplech quadre", + "magicframe": "", + "embeddable": "Integracion Web", + "laser": "Puntador laser", "hand": "Man (aisina de desplaçament de la vista)", - "extraTools": "" + "extraTools": "Mai d’aisinas", + "mermaidToExcalidraw": "De Mermaid cap a Excalidraw", + "magicSettings": "" }, "headings": { "canvasActions": "Accions del canabàs", @@ -319,7 +335,7 @@ "drag": "lisar", "editor": "Editor", "editLineArrowPoints": "", - "editText": "", + "editText": "Modificar lo tèxte / apondre etiqueta", "github": "Problèma trobat ? Senhalatz-lo", "howto": "Seguissètz nòstras guidas", "or": "o", @@ -376,27 +392,27 @@ "removeItemsFromLib": "Tirar los elements seleccionats de la bibliotèca" }, "imageExportDialog": { - "header": "", + "header": "Exportar imatges", "label": { - "withBackground": "", - "onlySelected": "", - "darkMode": "", - "embedScene": "", - "scale": "", - "padding": "" + "withBackground": "Rèireplan", + "onlySelected": "Seleccion sonque", + "darkMode": "Mòde escur", + "embedScene": "Embarcar la scèna", + "scale": "Escala", + "padding": "Espaçament" }, "tooltip": { "embedScene": "" }, "title": { - "exportToPng": "", - "exportToSvg": "", - "copyPngToClipboard": "" + "exportToPng": "Exportar en PNG", + "exportToSvg": "Exportar en SVG", + "copyPngToClipboard": "Copiar PNG al quichapapièrs" }, "button": { - "exportToPng": "", - "exportToSvg": "", - "copyPngToClipboard": "" + "exportToPng": "PNG", + "exportToSvg": "SVG", + "copyPngToClipboard": "Copiar al quichapapièrs" } }, "encrypted": { @@ -433,20 +449,20 @@ }, "colors": { "transparent": "Transparéncia", - "black": "", - "white": "", - "red": "", - "pink": "", - "grape": "", - "violet": "", - "gray": "", - "blue": "", - "cyan": "", - "teal": "", - "green": "", - "yellow": "", - "orange": "", - "bronze": "" + "black": "Negre", + "white": "Blanc", + "red": "Roge", + "pink": "Ròse", + "grape": "Bordèu", + "violet": "Violet", + "gray": "Gris", + "blue": "Blau", + "cyan": "Cian", + "teal": "Sarcèla", + "green": "Verd", + "yellow": "Jaune", + "orange": "Irange", + "bronze": "Bronze" }, "welcomeScreen": { "app": { @@ -462,41 +478,48 @@ } }, "colorPicker": { - "mostUsedCustomColors": "", - "colors": "", - "shades": "", - "hexCode": "", - "noShades": "" + "mostUsedCustomColors": "Colors personalizadas mai utilizadas", + "colors": "Colors", + "shades": "Nuanças", + "hexCode": "Còdi exadecimal", + "noShades": "Cap de nuança pas disponibla per aquesta color" }, "overwriteConfirm": { "action": { "exportToImage": { - "title": "", - "button": "", + "title": "Exportar coma imatge", + "button": "Exportar coma imatge", "description": "" }, "saveToDisk": { - "title": "", - "button": "", - "description": "" + "title": "Salvar al disc", + "button": "Salvar al disc", + "description": "Exportar las donadas de la scèna cap a un fichièr que podètz importar mai tard." }, "excalidrawPlus": { - "title": "", - "button": "", - "description": "" + "title": "Excalidraw+", + "button": "Exportar dins Excalidraw+", + "description": "Enregistrar la scèna dins vòstre espaci de trabalh Excalidraw+." } }, "modal": { "loadFromFile": { - "title": "", - "button": "", + "title": "Cargar d’un fichièr", + "button": "Cargar d’un fichièr", "description": "" }, "shareableLink": { - "title": "", - "button": "", + "title": "Cargar d’un ligam", + "button": "Remplaçar mon contengut", "description": "" } } + }, + "mermaid": { + "title": "De Mermaid cap a Excalidraw", + "button": "Inserir", + "description": "", + "syntax": "Sintaxi Mermaid", + "preview": "Apercebut" } } diff --git a/src/locales/pa-IN.json b/packages/excalidraw/locales/pa-IN.json similarity index 97% rename from src/locales/pa-IN.json rename to packages/excalidraw/locales/pa-IN.json index 5f51d1a2c..7bfe56fb1 100644 --- a/src/locales/pa-IN.json +++ b/packages/excalidraw/locales/pa-IN.json @@ -11,6 +11,8 @@ "copyAsPng": "ਕਲਿੱਪਬੋਰਡ 'ਤੇ PNG ਵਜੋਂ ਕਾਪੀ ਕਰੋ", "copyAsSvg": "ਕਲਿੱਪਬੋਰਡ 'ਤੇ SVG ਵਜੋਂ ਕਾਪੀ ਕਰੋ", "copyText": "ਕਲਿੱਪਬੋਰਡ 'ਤੇ ਪਾਠ ਵਜੋਂ ਕਾਪੀ ਕਰੋ", + "copySource": "", + "convertToCode": "", "bringForward": "ਅੱਗੇ ਲਿਆਓ", "sendToBack": "ਸਭ ਤੋਂ ਪਿੱਛੇ ਭੇਜੋ", "bringToFront": "ਸਭ ਤੋਂ ਅੱਗੇ ਲਿਆਓ", @@ -36,8 +38,12 @@ "arrowhead_none": "ਕੋਈ ਨਹੀਂ", "arrowhead_arrow": "ਤੀਰ", "arrowhead_bar": "ਡੰਡੀ", - "arrowhead_dot": "ਬਿੰਦੀ", + "arrowhead_circle": "", + "arrowhead_circle_outline": "", "arrowhead_triangle": "ਤਿਕੋਣ", + "arrowhead_triangle_outline": "", + "arrowhead_diamond": "", + "arrowhead_diamond_outline": "", "fontSize": "ਫੌਂਟ ਅਕਾਰ", "fontFamily": "ਫੌਂਟ ਪਰਿਵਾਰ", "addWatermark": "\"Excalidraw ਨਾਲ ਬਣਾਇਆ\" ਜੋੜੋ", @@ -130,7 +136,9 @@ "sidebarLock": "ਸਾਈਡਬਾਰ ਨੂੰ ਖੁੱਲ੍ਹਾ ਰੱਖੋ", "selectAllElementsInFrame": "", "removeAllElementsFromFrame": "", - "eyeDropper": "" + "eyeDropper": "", + "textToDiagram": "", + "prompt": "" }, "library": { "noItems": "ਹਾਲੇ ਤੱਕ ਕੋਈ ਚੀਜ ਜੋੜੀ ਨਹੀਂ ਗਈ...", @@ -209,6 +217,7 @@ "importLibraryError": "ਲਾਇਬ੍ਰੇਰੀ ਲੋਡ ਨਹੀਂ ਕੀਤੀ ਜਾ ਸਕੀ", "collabSaveFailed": "", "collabSaveFailed_sizeExceeded": "", + "imageToolNotSupported": "", "brave_measure_text_error": { "line1": "", "line2": "", @@ -217,8 +226,12 @@ }, "libraryElementTypeError": { "embeddable": "", + "iframe": "", "image": "" - } + }, + "asyncPasteFailedOnRead": "", + "asyncPasteFailedOnParse": "", + "copyToSystemClipboardFailed": "" }, "toolBar": { "selection": "ਚੋਣਕਾਰ", @@ -236,10 +249,13 @@ "link": "", "eraser": "ਰਬੜ", "frame": "", + "magicframe": "", "embeddable": "", "laser": "", "hand": "", - "extraTools": "" + "extraTools": "", + "mermaidToExcalidraw": "", + "magicSettings": "" }, "headings": { "canvasActions": "ਕੈਨਵਸ ਦੀਆਂ ਕਾਰਵਾਈਆਂ", @@ -498,5 +514,12 @@ "description": "" } } + }, + "mermaid": { + "title": "", + "button": "", + "description": "", + "syntax": "", + "preview": "" } } diff --git a/packages/excalidraw/locales/percentages.json b/packages/excalidraw/locales/percentages.json new file mode 100644 index 000000000..eb3cd087b --- /dev/null +++ b/packages/excalidraw/locales/percentages.json @@ -0,0 +1,56 @@ +{ + "ar-SA": 94, + "az-AZ": 17, + "bg-BG": 71, + "bn-BD": 52, + "ca-ES": 83, + "cs-CZ": 86, + "da-DK": 61, + "de-DE": 100, + "el-GR": 80, + "en": 100, + "es-ES": 96, + "eu-ES": 97, + "fa-IR": 84, + "fi-FI": 76, + "fr-FR": 99, + "gl-ES": 86, + "he-IL": 77, + "hi-IN": 76, + "hu-HU": 76, + "id-ID": 91, + "it-IT": 98, + "ja-JP": 90, + "kaa": 36, + "kab-KAB": 76, + "kk-KZ": 18, + "km-KH": 83, + "ko-KR": 100, + "ku-TR": 87, + "lt-LT": 48, + "lv-LV": 77, + "mr-IN": 98, + "my-MM": 35, + "nb-NO": 93, + "nl-NL": 75, + "nn-NO": 67, + "oc-FR": 92, + "pa-IN": 78, + "pl-PL": 99, + "pt-BR": 91, + "pt-PT": 83, + "ro-RO": 99, + "ru-RU": 92, + "si-LK": 7, + "sk-SK": 100, + "sl-SI": 100, + "sv-SE": 100, + "ta-IN": 81, + "th-TH": 44, + "tr-TR": 87, + "uk-UA": 93, + "vi-VN": 49, + "zh-CN": 100, + "zh-HK": 22, + "zh-TW": 100 +} diff --git a/src/locales/pl-PL.json b/packages/excalidraw/locales/pl-PL.json similarity index 94% rename from src/locales/pl-PL.json rename to packages/excalidraw/locales/pl-PL.json index ffe5247ea..bc869c19c 100644 --- a/src/locales/pl-PL.json +++ b/packages/excalidraw/locales/pl-PL.json @@ -11,6 +11,8 @@ "copyAsPng": "Skopiuj do schowka jako plik PNG", "copyAsSvg": "Skopiuj do schowka jako plik SVG", "copyText": "Skopiuj do schowka jako tekst", + "copySource": "Skopiuj źródło do schowka", + "convertToCode": "Skonwertuj do kodu", "bringForward": "Przenieś wyżej", "sendToBack": "Przenieś na spód", "bringToFront": "Przenieś na wierzch", @@ -36,8 +38,12 @@ "arrowhead_none": "Brak", "arrowhead_arrow": "Strzałka", "arrowhead_bar": "Kreska", - "arrowhead_dot": "Kropka", + "arrowhead_circle": "Okrąg", + "arrowhead_circle_outline": "Okrąg (obrys)", "arrowhead_triangle": "Trójkąt", + "arrowhead_triangle_outline": "Trójkąt (obrys)", + "arrowhead_diamond": "Romb", + "arrowhead_diamond_outline": "Romb (obrys)", "fontSize": "Rozmiar tekstu", "fontFamily": "Krój pisma", "addWatermark": "Dodaj \"Zrobione w Excalidraw\"", @@ -114,7 +120,7 @@ "createEmbed": "Stwórz i osadź link", "label": "Łącze", "labelEmbed": "Podlinkuj i osadź", - "empty": "Brakujący link" + "empty": "Nie ustawiono linku" }, "lineEditor": { "edit": "Edytuj linię", @@ -130,7 +136,9 @@ "sidebarLock": "Panel boczny zawsze otwarty", "selectAllElementsInFrame": "Zaznacz wszystkie elementy w ramce", "removeAllElementsFromFrame": "Usuń wszystkie elementy z ramki", - "eyeDropper": "Wybierz kolor z płótna" + "eyeDropper": "Wybierz kolor z płótna", + "textToDiagram": "Tekst do diagramu", + "prompt": "" }, "library": { "noItems": "Nie dodano jeszcze żadnych elementów...", @@ -209,6 +217,7 @@ "importLibraryError": "Wystąpił błąd w trakcie ładowania biblioteki", "collabSaveFailed": "Nie udało się zapisać w bazie danych. Jeśli problemy nie ustąpią, zapisz plik lokalnie, aby nie utracić swojej pracy.", "collabSaveFailed_sizeExceeded": "Nie udało się zapisać w bazie danych — dokument jest za duży. Zapisz plik lokalnie, aby nie utracić swojej pracy.", + "imageToolNotSupported": "Dodawanie obrazów jest wyłączone.", "brave_measure_text_error": { "line1": "Wygląda na to, że używasz przeglądarki Brave z włączonym ustawieniem Agressively Block Fingerprinting.", "line2": "Może to doprowadzić do złamania elementów tekstu na rysunkach.", @@ -217,8 +226,12 @@ }, "libraryElementTypeError": { "embeddable": "Elementy osadzone nie mogą zostać dodane do biblioteki.", + "iframe": "Elementy IFrame nie mogą zostać dodane do biblioteki.", "image": "Dodawania obrazów do biblioteki nadejdzie wkrótce!" - } + }, + "asyncPasteFailedOnRead": "Nie udało się wkleić (nie udało się odczytać ze schowka systemowego).", + "asyncPasteFailedOnParse": "Nie udało się wkleić.", + "copyToSystemClipboardFailed": "Nie udało się skopiować do schowka." }, "toolBar": { "selection": "Zaznaczenie", @@ -236,10 +249,13 @@ "link": "Dodaj/aktualizuj link dla wybranego kształtu", "eraser": "Gumka", "frame": "Ramka", + "magicframe": "Wireframe do kodu", "embeddable": "Osadzenie z internetu", "laser": "Wskaźnik laserowy", "hand": "Ręka (narzędzie do przesuwania)", - "extraTools": "Więcej narzędzi" + "extraTools": "Więcej narzędzi", + "mermaidToExcalidraw": "Konwertuj diagram Mermaid do Excalidraw", + "magicSettings": "Ustawienia AI" }, "headings": { "canvasActions": "Narzędzia", @@ -498,5 +514,12 @@ "description": "Wczytanie zewnętrznego pliku nadpisze istniejącą zawartość.

Możesz najpierw utworzyć kopię zapasową swojego rysunku, używając jednej z poniższych opcji." } } + }, + "mermaid": { + "title": "Konwertuj diagram Mermaid do Excalidraw", + "button": "Wstaw", + "description": "Obecnie wspierane są jedynie proste grafy, sekwencje i diagramy klas. Pozostałe typy będą wyświetlane jako obrazy w Excalidraw.", + "syntax": "Składnia diagramów Mermaid", + "preview": "Podgląd" } } diff --git a/src/locales/pt-BR.json b/packages/excalidraw/locales/pt-BR.json similarity index 97% rename from src/locales/pt-BR.json rename to packages/excalidraw/locales/pt-BR.json index 68c0059ed..b9701b0e5 100644 --- a/src/locales/pt-BR.json +++ b/packages/excalidraw/locales/pt-BR.json @@ -11,6 +11,8 @@ "copyAsPng": "Copiar para a área de transferência como PNG", "copyAsSvg": "Copiar para a área de transferência como SVG", "copyText": "Copiar para área de transferência como texto", + "copySource": "", + "convertToCode": "", "bringForward": "Trazer para a frente", "sendToBack": "Enviar para o fundo", "bringToFront": "Trazer para o primeiro plano", @@ -36,8 +38,12 @@ "arrowhead_none": "Nenhuma", "arrowhead_arrow": "Flecha", "arrowhead_bar": "Barra", - "arrowhead_dot": "Ponto", + "arrowhead_circle": "", + "arrowhead_circle_outline": "", "arrowhead_triangle": "Triângulo", + "arrowhead_triangle_outline": "", + "arrowhead_diamond": "", + "arrowhead_diamond_outline": "", "fontSize": "Tamanho da fonte", "fontFamily": "Família da fonte", "addWatermark": "Adicionar \"Feito com Excalidraw\"", @@ -130,7 +136,9 @@ "sidebarLock": "Manter barra lateral aberta", "selectAllElementsInFrame": "Selecionar todos os elementos no quadro", "removeAllElementsFromFrame": "Remover todos os elementos do quadro", - "eyeDropper": "Escolher cor da tela" + "eyeDropper": "Escolher cor da tela", + "textToDiagram": "", + "prompt": "" }, "library": { "noItems": "Nenhum item adicionado ainda...", @@ -209,6 +217,7 @@ "importLibraryError": "Não foi possível carregar a biblioteca", "collabSaveFailed": "Não foi possível salvar no banco de dados do servidor. Se os problemas persistirem, salve o arquivo localmente para garantir que não perca o seu trabalho.", "collabSaveFailed_sizeExceeded": "Não foi possível salvar no banco de dados do servidor, a tela parece ser muito grande. Se os problemas persistirem, salve o arquivo localmente para garantir que não perca o seu trabalho.", + "imageToolNotSupported": "", "brave_measure_text_error": { "line1": "Parece que você está usando o navegador Brave com a configuração Bloquear Impressões Digitais no modo agressivo.", "line2": "Isso pode acabar quebrando Elementos de Texto em seus desenhos.", @@ -217,8 +226,12 @@ }, "libraryElementTypeError": { "embeddable": "", + "iframe": "", "image": "" - } + }, + "asyncPasteFailedOnRead": "", + "asyncPasteFailedOnParse": "", + "copyToSystemClipboardFailed": "" }, "toolBar": { "selection": "Seleção", @@ -236,10 +249,13 @@ "link": "Adicionar/Atualizar link para uma forma selecionada", "eraser": "Borracha", "frame": "Ferramenta de quadro", + "magicframe": "", "embeddable": "", "laser": "", "hand": "Mão (ferramenta de rolagem)", - "extraTools": "Mais ferramentas" + "extraTools": "Mais ferramentas", + "mermaidToExcalidraw": "", + "magicSettings": "" }, "headings": { "canvasActions": "Ações da tela", @@ -498,5 +514,12 @@ "description": "Carregar um desenho externo irá substituir seu conteúdo existente.

Você pode salvar seu desenho antes utilizando uma das opções abaixo." } } + }, + "mermaid": { + "title": "", + "button": "", + "description": "", + "syntax": "", + "preview": "" } } diff --git a/src/locales/pt-PT.json b/packages/excalidraw/locales/pt-PT.json similarity index 97% rename from src/locales/pt-PT.json rename to packages/excalidraw/locales/pt-PT.json index 401a45ac5..283853b68 100644 --- a/src/locales/pt-PT.json +++ b/packages/excalidraw/locales/pt-PT.json @@ -11,6 +11,8 @@ "copyAsPng": "Copiar para a área de transferência como PNG", "copyAsSvg": "Copiar para a área de transferência como SVG", "copyText": "Copiar para Área de Transferência como texto", + "copySource": "", + "convertToCode": "", "bringForward": "Trazer para o primeiro plano", "sendToBack": "Enviar para o plano de fundo", "bringToFront": "Trazer para o primeiro plano", @@ -36,8 +38,12 @@ "arrowhead_none": "Nenhuma", "arrowhead_arrow": "Seta", "arrowhead_bar": "Barra", - "arrowhead_dot": "Ponto", + "arrowhead_circle": "", + "arrowhead_circle_outline": "", "arrowhead_triangle": "Triângulo", + "arrowhead_triangle_outline": "", + "arrowhead_diamond": "", + "arrowhead_diamond_outline": "", "fontSize": "Tamanho da fonte", "fontFamily": "Família da fontes", "addWatermark": "Adicionar \"Feito com Excalidraw\"", @@ -130,7 +136,9 @@ "sidebarLock": "Manter a barra lateral aberta", "selectAllElementsInFrame": "", "removeAllElementsFromFrame": "", - "eyeDropper": "" + "eyeDropper": "", + "textToDiagram": "", + "prompt": "" }, "library": { "noItems": "Ainda não foram adicionados nenhuns itens...", @@ -209,6 +217,7 @@ "importLibraryError": "Não foi possível carregar a biblioteca", "collabSaveFailed": "Não foi possível guardar na base de dados de backend. Se os problemas persistirem, guarde o ficheiro localmente para garantir que não perde o seu trabalho.", "collabSaveFailed_sizeExceeded": "Não foi possível guardar na base de dados de backend, o ecrã parece estar muito grande. Deve guardar o ficheiro localmente para garantir que não perde o seu trabalho.", + "imageToolNotSupported": "", "brave_measure_text_error": { "line1": "", "line2": "", @@ -217,8 +226,12 @@ }, "libraryElementTypeError": { "embeddable": "", + "iframe": "", "image": "" - } + }, + "asyncPasteFailedOnRead": "", + "asyncPasteFailedOnParse": "", + "copyToSystemClipboardFailed": "" }, "toolBar": { "selection": "Seleção", @@ -236,10 +249,13 @@ "link": "Acrescentar/ Adicionar ligação para uma forma seleccionada", "eraser": "Borracha", "frame": "", + "magicframe": "", "embeddable": "", "laser": "", "hand": "Mão (ferramenta de movimento da tela)", - "extraTools": "" + "extraTools": "", + "mermaidToExcalidraw": "", + "magicSettings": "" }, "headings": { "canvasActions": "Ações da área de desenho", @@ -498,5 +514,12 @@ "description": "" } } + }, + "mermaid": { + "title": "", + "button": "", + "description": "", + "syntax": "", + "preview": "" } } diff --git a/src/locales/ro-RO.json b/packages/excalidraw/locales/ro-RO.json similarity index 94% rename from src/locales/ro-RO.json rename to packages/excalidraw/locales/ro-RO.json index 125c28663..32bcf9818 100644 --- a/src/locales/ro-RO.json +++ b/packages/excalidraw/locales/ro-RO.json @@ -11,6 +11,8 @@ "copyAsPng": "Copiere în memoria temporară ca PNG", "copyAsSvg": "Copiere în memoria temporară ca SVG", "copyText": "Copiere în memoria temporară ca text", + "copySource": "Copiere sursă în memoria temporară", + "convertToCode": "Convertire în cod", "bringForward": "Aducere în plan apropiat", "sendToBack": "Trimitere în ultimul plan", "bringToFront": "Aducere în prim plan", @@ -36,8 +38,12 @@ "arrowhead_none": "Niciunul", "arrowhead_arrow": "Săgeată", "arrowhead_bar": "Bară", - "arrowhead_dot": "Bulină", + "arrowhead_circle": "Cerc", + "arrowhead_circle_outline": "Cerc (contur)", "arrowhead_triangle": "Triunghi", + "arrowhead_triangle_outline": "Triunghi (contur)", + "arrowhead_diamond": "Romb", + "arrowhead_diamond_outline": "Romb (contur)", "fontSize": "Dimensiune font", "fontFamily": "Familia de fonturi", "addWatermark": "Adaugă „Realizat cu Excalidraw”", @@ -130,7 +136,9 @@ "sidebarLock": "Păstrează deschisă bara laterală", "selectAllElementsInFrame": "", "removeAllElementsFromFrame": "", - "eyeDropper": "Alegere culoare din pânză" + "eyeDropper": "Alegere culoare din pânză", + "textToDiagram": "Text la diagramă", + "prompt": "Solicitare" }, "library": { "noItems": "Niciun element adăugat încă...", @@ -203,12 +211,13 @@ "imageInsertError": "Imaginea nu a putut fi introdusă. Reîncearcă mai târziu...", "fileTooBig": "Fișierul este prea mare. Dimensiunea maximă permisă este de {{maxSize}}.", "svgImageInsertError": "Imaginea SVG nu a putut fi introdus. Marcajul SVG pare invalid.", - "failedToFetchImage": "", + "failedToFetchImage": "Preluarea imaginii a eșuat.", "invalidSVGString": "SVG invalid.", "cannotResolveCollabServer": "Nu a putut fi realizată conexiunea la serverul de colaborare. Reîncarcă pagina și încearcă din nou.", "importLibraryError": "Biblioteca nu a putut fi încărcată", "collabSaveFailed": "Nu s-a putut salva în baza de date la nivel de server. Dacă problemele persistă, ar trebui să salvezi fișierul la nivel local pentru a te asigura că nu îți pierzi munca.", "collabSaveFailed_sizeExceeded": "Nu s-a putut salva în baza de date la nivel de server, întrucât se pare că pânza este prea mare. Ar trebui să salvezi fișierul la nivel local pentru a te asigura că nu îți pierzi munca.", + "imageToolNotSupported": "Imaginile sunt dezactivate.", "brave_measure_text_error": { "line1": "Se pare că folosești navigatorul Brave cu opțiunea strictă pentru blocarea amprentării.", "line2": "Acest lucru poate duce la întreruperea elementelor text din desene.", @@ -217,8 +226,12 @@ }, "libraryElementTypeError": { "embeddable": "Elementele încorporabile nu pot fi adăugate la bibliotecă.", + "iframe": "Elementele iFrame nu pot fi adăugate la bibliotecă.", "image": "În curând vor putea fi adăugate imagini în bibliotecă!" - } + }, + "asyncPasteFailedOnRead": "Lipirea nu a putut fi efectuată (nu s-a putut citit din memoria temporară a sistemului).", + "asyncPasteFailedOnParse": "Lipirea nu a putut fi efectuată.", + "copyToSystemClipboardFailed": "Nu s-a putut copia în memoria temporară." }, "toolBar": { "selection": "Selecție", @@ -236,10 +249,13 @@ "link": "Adăugare/actualizare URL pentru forma selectată", "eraser": "Radieră", "frame": "", + "magicframe": "Structură-de-fire la cod", "embeddable": "Încorporare web", "laser": "Indicator laser", "hand": "Mână (instrument de panoramare)", - "extraTools": "" + "extraTools": "", + "mermaidToExcalidraw": "Mermaid la Excalidraw", + "magicSettings": "Setări IA" }, "headings": { "canvasActions": "Acțiuni pentru pânză", @@ -498,5 +514,12 @@ "description": "Încărcarea unui desen extern va înlocui conținutul existent.

Poți face mai întâi o copie de rezervă a desenului folosind una dintre opțiunile de mai jos." } } + }, + "mermaid": { + "title": "Mermaid la Excalidraw", + "button": "Introducere", + "description": "În prezent, numai Organigramele, Diagramele de secvență și Diagramele de clasă sunt acceptate. Celelalte tipuri vor fi redate ca imagine în Excalidraw.", + "syntax": "Sintaxă Mermaid", + "preview": "Previzualizare" } } diff --git a/src/locales/ru-RU.json b/packages/excalidraw/locales/ru-RU.json similarity index 96% rename from src/locales/ru-RU.json rename to packages/excalidraw/locales/ru-RU.json index 7c489022c..41df7d759 100644 --- a/src/locales/ru-RU.json +++ b/packages/excalidraw/locales/ru-RU.json @@ -11,6 +11,8 @@ "copyAsPng": "Скопировать в буфер обмена как PNG", "copyAsSvg": "Скопировать в буфер обмена как SVG", "copyText": "Скопировать в буфер обмена как текст", + "copySource": "Копировать источник в буфер обмена", + "convertToCode": "Преобразовать в код", "bringForward": "Переместить вперед", "sendToBack": "На задний план", "bringToFront": "На передний план", @@ -36,8 +38,12 @@ "arrowhead_none": "Нет", "arrowhead_arrow": "Cтрелка", "arrowhead_bar": "Черта", - "arrowhead_dot": "Точка", + "arrowhead_circle": "", + "arrowhead_circle_outline": "", "arrowhead_triangle": "Треугольник", + "arrowhead_triangle_outline": "", + "arrowhead_diamond": "", + "arrowhead_diamond_outline": "", "fontSize": "Размер шрифта", "fontFamily": "Семейство шрифтов", "addWatermark": "Добавить «Создано в Excalidraw»", @@ -130,7 +136,9 @@ "sidebarLock": "Держать боковую панель открытой", "selectAllElementsInFrame": "", "removeAllElementsFromFrame": "", - "eyeDropper": "Взять образец цвета с холста" + "eyeDropper": "Взять образец цвета с холста", + "textToDiagram": "Текст в диаграмму", + "prompt": "" }, "library": { "noItems": "Пока ничего не добавлено...", @@ -209,6 +217,7 @@ "importLibraryError": "Не удалось загрузить библиотеку", "collabSaveFailed": "Не удалось сохранить в базу данных. Если проблема повторится, нужно будет сохранить файл локально, чтобы быть уверенным, что вы не потеряете вашу работу.", "collabSaveFailed_sizeExceeded": "Не удалось сохранить в базу данных. Похоже, что холст слишком большой. Нужно сохранить файл локально, чтобы быть уверенным, что вы не потеряете вашу работу.", + "imageToolNotSupported": "Изображения отключены.", "brave_measure_text_error": { "line1": "Похоже, вы используете браузер Brave с включенной опцией Агрессивно блокировать отслеживание.", "line2": "Это может привести к поломке Текстовых объектов на рисунке.", @@ -217,8 +226,12 @@ }, "libraryElementTypeError": { "embeddable": "", + "iframe": "Элементы IFrame не могут быть добавлены в библиотеку.", "image": "" - } + }, + "asyncPasteFailedOnRead": "Не удалось вставить (невозможно прочитать из системного буфера обмена).", + "asyncPasteFailedOnParse": "Не удалось вставить.", + "copyToSystemClipboardFailed": "Не удалось скопировать в буфер обмена." }, "toolBar": { "selection": "Выделение области", @@ -236,10 +249,13 @@ "link": "Добавить/обновить ссылку для выбранной фигуры", "eraser": "Ластик", "frame": "", + "magicframe": "", "embeddable": "", "laser": "Лазерная указка", "hand": "Рука (перемещение холста)", - "extraTools": "" + "extraTools": "", + "mermaidToExcalidraw": "Из Mermaid в Excalidraw", + "magicSettings": "Параметры AI" }, "headings": { "canvasActions": "Операции холста", @@ -498,5 +514,12 @@ "description": "" } } + }, + "mermaid": { + "title": "Из Mermaid в Excalidraw", + "button": "Вставить", + "description": "", + "syntax": "Синтаксис Mermaid", + "preview": "Предпросмотр" } } diff --git a/src/locales/si-LK.json b/packages/excalidraw/locales/si-LK.json similarity index 94% rename from src/locales/si-LK.json rename to packages/excalidraw/locales/si-LK.json index 8cc93b03a..95f1914c9 100644 --- a/src/locales/si-LK.json +++ b/packages/excalidraw/locales/si-LK.json @@ -11,6 +11,8 @@ "copyAsPng": "PNG ලෙස පිටපත් කරන්න", "copyAsSvg": "SVG ලෙස පිටපත් කරන්න", "copyText": "", + "copySource": "", + "convertToCode": "", "bringForward": "ඉදිරියට ගෙන්න", "sendToBack": "පසුපසටම ගෙනියන්න", "bringToFront": "ඉදිරියටම ගෙන්න", @@ -36,8 +38,12 @@ "arrowhead_none": "", "arrowhead_arrow": "", "arrowhead_bar": "", - "arrowhead_dot": "", + "arrowhead_circle": "", + "arrowhead_circle_outline": "", "arrowhead_triangle": "", + "arrowhead_triangle_outline": "", + "arrowhead_diamond": "", + "arrowhead_diamond_outline": "", "fontSize": "", "fontFamily": "", "addWatermark": "", @@ -130,7 +136,9 @@ "sidebarLock": "", "selectAllElementsInFrame": "", "removeAllElementsFromFrame": "", - "eyeDropper": "" + "eyeDropper": "", + "textToDiagram": "", + "prompt": "" }, "library": { "noItems": "", @@ -209,6 +217,7 @@ "importLibraryError": "", "collabSaveFailed": "", "collabSaveFailed_sizeExceeded": "", + "imageToolNotSupported": "", "brave_measure_text_error": { "line1": "", "line2": "", @@ -217,8 +226,12 @@ }, "libraryElementTypeError": { "embeddable": "", + "iframe": "", "image": "" - } + }, + "asyncPasteFailedOnRead": "", + "asyncPasteFailedOnParse": "", + "copyToSystemClipboardFailed": "" }, "toolBar": { "selection": "", @@ -236,10 +249,13 @@ "link": "", "eraser": "", "frame": "", + "magicframe": "", "embeddable": "", "laser": "", "hand": "", - "extraTools": "" + "extraTools": "", + "mermaidToExcalidraw": "", + "magicSettings": "" }, "headings": { "canvasActions": "", @@ -498,5 +514,12 @@ "description": "" } } + }, + "mermaid": { + "title": "", + "button": "", + "description": "", + "syntax": "", + "preview": "" } } diff --git a/src/locales/sk-SK.json b/packages/excalidraw/locales/sk-SK.json similarity index 94% rename from src/locales/sk-SK.json rename to packages/excalidraw/locales/sk-SK.json index b0d9e4b84..9f4135e20 100644 --- a/src/locales/sk-SK.json +++ b/packages/excalidraw/locales/sk-SK.json @@ -11,6 +11,8 @@ "copyAsPng": "Kopírovať do schránky ako PNG", "copyAsSvg": "Kopírovať do schránky ako SVG", "copyText": "Kopírovať do schránky ako text", + "copySource": "Kopírovať kód do schránky", + "convertToCode": "Konvertovať na kód", "bringForward": "Presunúť o úroveň dopredu", "sendToBack": "Presunúť dozadu", "bringToFront": "Presunúť dopredu", @@ -36,8 +38,12 @@ "arrowhead_none": "Žiadne", "arrowhead_arrow": "Šípka", "arrowhead_bar": "Čiara", - "arrowhead_dot": "Bod", + "arrowhead_circle": "Kruh", + "arrowhead_circle_outline": "Kruh (obrys)", "arrowhead_triangle": "Trojuholník", + "arrowhead_triangle_outline": "Trojuholník (obrys)", + "arrowhead_diamond": "Diamant", + "arrowhead_diamond_outline": "Diamant (obrys)", "fontSize": "Veľkosť písma", "fontFamily": "Písmo", "addWatermark": "Pridať \"Vytvorené s Excalidraw\"", @@ -130,7 +136,9 @@ "sidebarLock": "Nechať bočný panel otvorený", "selectAllElementsInFrame": "Vybrať všetky prvky v ráme", "removeAllElementsFromFrame": "Odstrániť všetky prvky z rámu", - "eyeDropper": "Vybrať farbu z plátna" + "eyeDropper": "Vybrať farbu z plátna", + "textToDiagram": "Text na diagram", + "prompt": "Inštrukcia" }, "library": { "noItems": "Zatiaľ neboli pridané žiadne položky...", @@ -203,12 +211,13 @@ "imageInsertError": "Nepodarilo sa vložiť obrázok. Skúste to znova neskôr...", "fileTooBig": "Súbor je príliš veľký. Maximálna povolená veľkosť je {{maxSize}}.", "svgImageInsertError": "Nepodarilo sa vložiť SVG obrázok. SVG formát je pravdepodobne nevalidný.", - "failedToFetchImage": "", + "failedToFetchImage": "Načítanie obrázka zlyhalo.", "invalidSVGString": "Nevalidné SVG.", "cannotResolveCollabServer": "Nepodarilo sa pripojiť ku kolaboračnému serveru. Prosím obnovte stránku a skúste to znovu.", "importLibraryError": "Nepodarilo sa načítať knižnicu", "collabSaveFailed": "Uloženie do databázy sa nepodarilo. Ak tento problém pretrváva uložte si váš súbor lokálne aby ste nestratili vašu prácu.", "collabSaveFailed_sizeExceeded": "Uloženie do databázy sa nepodarilo, pretože veľkosť plátna je príliš veľká. Uložte si váš súbor lokálne aby ste nestratili vašu prácu.", + "imageToolNotSupported": "Obrázky sú vypnuté.", "brave_measure_text_error": { "line1": "Vyzerá to, že používate prehliadač Brave so zapnutým nastavením pre agresívne blokovanie.", "line2": "To môže spôsobiť nesprávne zobrazenie textových prvkov vo vašej kresbe.", @@ -217,8 +226,12 @@ }, "libraryElementTypeError": { "embeddable": "Zapustené prvky nie je možné pridať do knižnice.", + "iframe": "Vložené rámce IFrame nie je možné pridať do knižnice.", "image": "Podpora pre pridávanie obrázkov do knižnice bude dostupná už čoskoro!" - } + }, + "asyncPasteFailedOnRead": "Vloženie sa nepodarilo (nebolo možné prečítať obsah schránky).", + "asyncPasteFailedOnParse": "Vloženie sa nepodarilo.", + "copyToSystemClipboardFailed": "Kopírovanie do schránky sa nepodarilo." }, "toolBar": { "selection": "Výber", @@ -236,10 +249,13 @@ "link": "Pridať/ Upraviť odkaz pre vybraný tvar", "eraser": "Guma", "frame": "Nástroj rám", + "magicframe": "Drôtený model na kód", "embeddable": "Web Embed", - "laser": "", + "laser": "Laserový ukazovateľ", "hand": "Ruka (nástroj pre pohyb plátna)", - "extraTools": "Ďalšie nástroje" + "extraTools": "Ďalšie nástroje", + "mermaidToExcalidraw": "Mermaid do Excalidraw", + "magicSettings": "AI nastavenia" }, "headings": { "canvasActions": "Akcie plátna", @@ -498,5 +514,12 @@ "description": "Načítanie externej kresby nahradí váš existujúci obsah.

Vašu kresbu môžete zálohovať jednou z nižšie uvedených možností." } } + }, + "mermaid": { + "title": "Mermaid do Excalidraw", + "button": "Vložiť", + "description": "Aktuálne sú podporované iba vývojové diagramy, sekvenčné diagramy a diagramy tried. Ostatné typy budú v Excalidraw vykreslené ako obrázky.", + "syntax": "Mermaid syntax", + "preview": "Ukážka" } } diff --git a/src/locales/sl-SI.json b/packages/excalidraw/locales/sl-SI.json similarity index 94% rename from src/locales/sl-SI.json rename to packages/excalidraw/locales/sl-SI.json index 5a6ea8c6f..2cd7e289f 100644 --- a/src/locales/sl-SI.json +++ b/packages/excalidraw/locales/sl-SI.json @@ -11,6 +11,8 @@ "copyAsPng": "Kopiraj v odložišče kot PNG", "copyAsSvg": "Kopiraj v odložišče kot SVG", "copyText": "Kopiraj v odložišče kot besedilo", + "copySource": "Kopiraj vir v odložišče", + "convertToCode": "Pretvori v kodo", "bringForward": "Postavi naprej", "sendToBack": "Pomakni v ozadje", "bringToFront": "Pomakni v ospredje", @@ -36,8 +38,12 @@ "arrowhead_none": "Brez", "arrowhead_arrow": "Puščica", "arrowhead_bar": "Palica", - "arrowhead_dot": "Pika", + "arrowhead_circle": "Krog", + "arrowhead_circle_outline": "Krog (oris)", "arrowhead_triangle": "Trikotnik", + "arrowhead_triangle_outline": "Trikotnik (oris)", + "arrowhead_diamond": "Diamant", + "arrowhead_diamond_outline": "Diamant (oris)", "fontSize": "Velikost pisave", "fontFamily": "Družina pisave", "addWatermark": "Dodaj \"Izdelano z Excalidraw\"", @@ -130,7 +136,9 @@ "sidebarLock": "Obdrži stransko vrstico odprto", "selectAllElementsInFrame": "Izberi vse elemente v okvirju", "removeAllElementsFromFrame": "Izbriši vse elemente v okvirju", - "eyeDropper": "Izberi barvo s platna" + "eyeDropper": "Izberi barvo s platna", + "textToDiagram": "Besedilo v diagram", + "prompt": "Poziv" }, "library": { "noItems": "Dodan še ni noben element...", @@ -209,6 +217,7 @@ "importLibraryError": "Nalaganje knjižnice ni uspelo", "collabSaveFailed": "Ni bilo mogoče shraniti v zaledno bazo podatkov. Če se težave nadaljujejo, shranite datoteko lokalno, da ne boste izgubili svojega dela.", "collabSaveFailed_sizeExceeded": "Ni bilo mogoče shraniti v zaledno bazo podatkov, zdi se, da je platno preveliko. Datoteko shranite lokalno, da ne izgubite svojega dela.", + "imageToolNotSupported": "Slike so onemogočene.", "brave_measure_text_error": { "line1": "Videti je, da uporabljate brskalnik Brave z omogočeno nastavitvijo Agresivno blokiranje prstnih odtisov.", "line2": "To bi lahko povzročilo motnje v obnašanju besedilnih elementov v vaših risbah.", @@ -217,8 +226,12 @@ }, "libraryElementTypeError": { "embeddable": "Vdelani elementi ne morejo biti dodani v knjižnico.", + "iframe": "Elementov iFrame ni mogoče dodati v knjižnico.", "image": "Podpora za dodajanje slik v knjižnico prihaja kmalu!" - } + }, + "asyncPasteFailedOnRead": "Ni bilo mogoče prilepiti (ni bilo mogoče brati iz sistemskega odložišča).", + "asyncPasteFailedOnParse": "Ni bilo mogoče prilepiti.", + "copyToSystemClipboardFailed": "Ni bilo mogoče kopirati v odložišče." }, "toolBar": { "selection": "Izbor", @@ -236,10 +249,13 @@ "link": "Dodaj/posodobi povezavo za izbrano obliko", "eraser": "Radirka", "frame": "Okvir", + "magicframe": "Žični okvir v kodo", "embeddable": "Spletna vdelava", "laser": "Laserski kazalec", "hand": "Roka (orodje za premikanje)", - "extraTools": "Več orodij" + "extraTools": "Več orodij", + "mermaidToExcalidraw": "Mermaid v Excalidraw", + "magicSettings": "Nastavitve AI" }, "headings": { "canvasActions": "Dejanja za platno", @@ -498,5 +514,12 @@ "description": "Nalaganje zunanje risbe bo prepisalo vašo obstoječo vsebino.

Svojo risbo lahko najprej varnostno kopirate z eno od spodnjih možnosti." } } + }, + "mermaid": { + "title": "Mermaid v Excalidraw", + "button": "Vstavi", + "description": "Trenutno so podprti samo diagrami poteka, diagrami zaporedij in Razredni diagrami. Druge vrste bodo upodobljene kot slike v Excalidraw.", + "syntax": "Sintaksa Mermaid", + "preview": "Predogled" } } diff --git a/src/locales/sv-SE.json b/packages/excalidraw/locales/sv-SE.json similarity index 94% rename from src/locales/sv-SE.json rename to packages/excalidraw/locales/sv-SE.json index 94a09610c..d5a788fd4 100644 --- a/src/locales/sv-SE.json +++ b/packages/excalidraw/locales/sv-SE.json @@ -11,6 +11,8 @@ "copyAsPng": "Kopiera till urklipp som PNG", "copyAsSvg": "Kopiera till urklipp som SVG", "copyText": "Kopiera till urklipp som text", + "copySource": "Kopiera källa till urklipp", + "convertToCode": "Konvertera till kod", "bringForward": "Flytta framåt", "sendToBack": "Flytta underst", "bringToFront": "Flytta främst", @@ -36,8 +38,12 @@ "arrowhead_none": "Inga", "arrowhead_arrow": "Pil", "arrowhead_bar": "Stolpe", - "arrowhead_dot": "Punkt", + "arrowhead_circle": "Cirkel", + "arrowhead_circle_outline": "Cirkel (kontur)", "arrowhead_triangle": "Triangel", + "arrowhead_triangle_outline": "Triangel (kontur)", + "arrowhead_diamond": "Diamant", + "arrowhead_diamond_outline": "Diamant (kontur)", "fontSize": "Teckenstorlek", "fontFamily": "Teckensnitt", "addWatermark": "Lägg till \"Skapad med Excalidraw\"", @@ -130,7 +136,9 @@ "sidebarLock": "Håll sidofältet öppet", "selectAllElementsInFrame": "Markera alla element i rutan", "removeAllElementsFromFrame": "Ta bort alla element från rutan", - "eyeDropper": "Välj färg från canvas" + "eyeDropper": "Välj färg från canvas", + "textToDiagram": "Text till diagram", + "prompt": "Fråga" }, "library": { "noItems": "Inga objekt tillagda ännu...", @@ -209,6 +217,7 @@ "importLibraryError": "Kunde inte ladda bibliotek", "collabSaveFailed": "Det gick inte att spara i backend-databasen. Om problemen kvarstår bör du spara filen lokalt för att se till att du inte förlorar ditt arbete.", "collabSaveFailed_sizeExceeded": "Det gick inte att spara till backend-databasen, whiteboarden verkar vara för stor. Du bör spara filen lokalt för att du inte ska förlora ditt arbete.", + "imageToolNotSupported": "Bilder är inaktiverade.", "brave_measure_text_error": { "line1": "Det ser ut som om du använder Brave-webbläsaren med Aggressivt Blockera fingeravtryck inställningen aktiverad.", "line2": "Detta kan resultera i trasiga Textelement i dina ritningar.", @@ -217,8 +226,12 @@ }, "libraryElementTypeError": { "embeddable": "Inbäddbara element kan inte läggas till i biblioteket.", + "iframe": "IFrame-element kan inte läggas till i biblioteket.", "image": "Stöd för att lägga till bilder till biblioteket kommer snart!" - } + }, + "asyncPasteFailedOnRead": "Kunde inte klistra in (kunde inte läsa från urklipp).", + "asyncPasteFailedOnParse": "Kunde inte klistra in.", + "copyToSystemClipboardFailed": "Kunde inte kopiera till urklipp." }, "toolBar": { "selection": "Markering", @@ -236,10 +249,13 @@ "link": "Lägg till / Uppdatera länk för en vald form", "eraser": "Radergummi", "frame": "Rutverktyg", + "magicframe": "Trådram till kod", "embeddable": "Bädda in (web)", "laser": "Laserpekare", "hand": "Hand (panoreringsverktyg)", - "extraTools": "Fler verktyg" + "extraTools": "Fler verktyg", + "mermaidToExcalidraw": "Mermaid till Excalidraw", + "magicSettings": "AI-inställningar" }, "headings": { "canvasActions": "Canvas-åtgärder", @@ -498,5 +514,12 @@ "description": "Inläsning av en extern ritning kommer ersätta ditt befintliga innehåll.

Du kan säkerhetskopiera din ritning först genom att använda ett av alternativen nedan." } } + }, + "mermaid": { + "title": "Mermaid till Excalidraw", + "button": "Infoga", + "description": "För närvarande stöds endast Flödesdiagram, Sekvensdiagram och Klassdiagram. De andra typerna kommer att återges som bild i Excalidraw.", + "syntax": "Mermaid-syntax", + "preview": "Förhandsgranska" } } diff --git a/src/locales/ta-IN.json b/packages/excalidraw/locales/ta-IN.json similarity index 98% rename from src/locales/ta-IN.json rename to packages/excalidraw/locales/ta-IN.json index 05d8c3dae..577d5bb9e 100644 --- a/src/locales/ta-IN.json +++ b/packages/excalidraw/locales/ta-IN.json @@ -11,6 +11,8 @@ "copyAsPng": "நகலகத்திற்கு PNG ஆக நகலெடு", "copyAsSvg": "நகலகத்திற்கு SVG ஆக நகலெடு", "copyText": "நகலகத்திற்கு உரையாக நகலெடு", + "copySource": "", + "convertToCode": "", "bringForward": "முன்நோக்கி கொண்டுவா", "sendToBack": "பின்னே அனுப்பு", "bringToFront": "முன்னே கொண்டுவா", @@ -36,8 +38,12 @@ "arrowhead_none": "ஏதுமில்லை", "arrowhead_arrow": "அம்பு", "arrowhead_bar": "பட்டை", - "arrowhead_dot": "புள்ளி", + "arrowhead_circle": "", + "arrowhead_circle_outline": "", "arrowhead_triangle": "முக்கோணம்", + "arrowhead_triangle_outline": "", + "arrowhead_diamond": "", + "arrowhead_diamond_outline": "", "fontSize": "எழுத்துரு அளவு", "fontFamily": "எழுத்துரு குடும்பம்", "addWatermark": "\"எக்ஸ்கேலிட்ரா கொண்டு ஆனது\"-ஐச் சேர்", @@ -130,7 +136,9 @@ "sidebarLock": "பக்கப்பட்டையைத் திறந்தே வை", "selectAllElementsInFrame": "", "removeAllElementsFromFrame": "", - "eyeDropper": "கித்தானிலிருந்து நிறம் தேர்ந்தெடு" + "eyeDropper": "கித்தானிலிருந்து நிறம் தேர்ந்தெடு", + "textToDiagram": "", + "prompt": "" }, "library": { "noItems": "இதுவரை உருப்படிகள் சேரக்கப்படவில்லை...", @@ -209,6 +217,7 @@ "importLibraryError": "நூலகத்தை ஏற்ற முடியவில்லை", "collabSaveFailed": "பின்முனை தரவுத்தளத்தில் சேமிக்க முடியவில்லை. சிக்கல்கள் நீடித்தால், உமது வேலைகளை இழக்காமலிருப்பதை உறுதிசெய்ய உமது கோப்பை உள்ளகத்தில் சேமிக்க வேண்டும்.", "collabSaveFailed_sizeExceeded": "பின்முனை தரவுத்தளத்தில் சேமிக்க முடியவில்லை, கித்தான் மிகப்பெரிதாகத் தெரிகிறது. உமது வேலைகளை இழக்காமலிருப்பதை உறுதிசெய்ய உமது கோப்பை உள்ளகத்தில் சேமிக்க வேண்டும்.", + "imageToolNotSupported": "", "brave_measure_text_error": { "line1": "", "line2": "", @@ -217,8 +226,12 @@ }, "libraryElementTypeError": { "embeddable": "", + "iframe": "", "image": "" - } + }, + "asyncPasteFailedOnRead": "", + "asyncPasteFailedOnParse": "", + "copyToSystemClipboardFailed": "" }, "toolBar": { "selection": "தெரிவு", @@ -236,10 +249,13 @@ "link": "தேர்தெடுத்த வடிவத்திற்குத் தொடுப்பைச் சேர்/ புதுப்பி", "eraser": "அழிப்பி", "frame": "சட்டகம் கருவி", + "magicframe": "", "embeddable": "", "laser": "", "hand": "கை (பார்வை நகர்கும் கருவி)", - "extraTools": "மற்ற கருவிகள்" + "extraTools": "மற்ற கருவிகள்", + "mermaidToExcalidraw": "", + "magicSettings": "" }, "headings": { "canvasActions": "கித்தான் செயல்கள்", @@ -498,5 +514,12 @@ "description": "" } } + }, + "mermaid": { + "title": "", + "button": "", + "description": "", + "syntax": "", + "preview": "" } } diff --git a/src/locales/th-TH.json b/packages/excalidraw/locales/th-TH.json similarity index 96% rename from src/locales/th-TH.json rename to packages/excalidraw/locales/th-TH.json index 57258e2fa..4d27fe8b5 100644 --- a/src/locales/th-TH.json +++ b/packages/excalidraw/locales/th-TH.json @@ -11,6 +11,8 @@ "copyAsPng": "คัดลองไปยังคลิปบอร์ดเป็น PNG", "copyAsSvg": "คัดลองไปยังคลิปบอร์ดเป็น SVG", "copyText": "คัดลองไปยังคลิปบอร์ดเป็นข้อความ", + "copySource": "", + "convertToCode": "", "bringForward": "นำขึ้นข้างบน", "sendToBack": "ย้ายไปข้างล่าง", "bringToFront": "นำขึ้นข้างหน้า", @@ -36,8 +38,12 @@ "arrowhead_none": "ไม่มี", "arrowhead_arrow": "ลูกศร", "arrowhead_bar": "แถบ", - "arrowhead_dot": "จุด", + "arrowhead_circle": "", + "arrowhead_circle_outline": "", "arrowhead_triangle": "สามเหลี่ยม", + "arrowhead_triangle_outline": "", + "arrowhead_diamond": "", + "arrowhead_diamond_outline": "", "fontSize": "ขนาดตัวอักษร", "fontFamily": "แบบตัวอักษร", "addWatermark": "เพิ่มลายน้ำ \"สร้างด้วย Excalidraw\"", @@ -130,7 +136,9 @@ "sidebarLock": "", "selectAllElementsInFrame": "", "removeAllElementsFromFrame": "", - "eyeDropper": "" + "eyeDropper": "", + "textToDiagram": "", + "prompt": "" }, "library": { "noItems": "ยังไม่มีรายการที่เพิ่มเข้าไปได้", @@ -209,6 +217,7 @@ "importLibraryError": "", "collabSaveFailed": "", "collabSaveFailed_sizeExceeded": "", + "imageToolNotSupported": "", "brave_measure_text_error": { "line1": "", "line2": "", @@ -217,8 +226,12 @@ }, "libraryElementTypeError": { "embeddable": "การเพิ่มองค์ประกอบที่ฝังยังไม่สามารถเพิ่มเข้าไปในไลบลารีได้", + "iframe": "", "image": "การสนับสนุนสำหรับเพิ่มรูปภาพลงในไลบลารีจะมาในเร็ว ๆ นี้" - } + }, + "asyncPasteFailedOnRead": "", + "asyncPasteFailedOnParse": "", + "copyToSystemClipboardFailed": "" }, "toolBar": { "selection": "", @@ -236,10 +249,13 @@ "link": "", "eraser": "ยางลบ", "frame": "", + "magicframe": "", "embeddable": "ฝังเว็บ", "laser": "", "hand": "", - "extraTools": "เครื่องมืออื่นๆ" + "extraTools": "เครื่องมืออื่นๆ", + "mermaidToExcalidraw": "", + "magicSettings": "" }, "headings": { "canvasActions": "", @@ -498,5 +514,12 @@ "description": "" } } + }, + "mermaid": { + "title": "", + "button": "", + "description": "", + "syntax": "", + "preview": "" } } diff --git a/src/locales/tr-TR.json b/packages/excalidraw/locales/tr-TR.json similarity index 96% rename from src/locales/tr-TR.json rename to packages/excalidraw/locales/tr-TR.json index e9cc463e9..de5cf11c7 100644 --- a/src/locales/tr-TR.json +++ b/packages/excalidraw/locales/tr-TR.json @@ -11,6 +11,8 @@ "copyAsPng": "Panoya PNG olarak kopyala", "copyAsSvg": "Panoya SVG olarak kopyala", "copyText": "Panoya metin olarak kopyala", + "copySource": "", + "convertToCode": "", "bringForward": "Bir öne getir", "sendToBack": "Arkaya gönder", "bringToFront": "En öne getir", @@ -36,8 +38,12 @@ "arrowhead_none": "Yok", "arrowhead_arrow": "Ok", "arrowhead_bar": "Çizgi", - "arrowhead_dot": "Nokta", + "arrowhead_circle": "", + "arrowhead_circle_outline": "", "arrowhead_triangle": "Üçgen", + "arrowhead_triangle_outline": "", + "arrowhead_diamond": "", + "arrowhead_diamond_outline": "", "fontSize": "Yazı tipi boyutu", "fontFamily": "Yazı tipi ailesi", "addWatermark": "\"Excalidraw ile yapıldı\" yazısını ekle", @@ -130,7 +136,9 @@ "sidebarLock": "Kenar çubuğu açık kalsın", "selectAllElementsInFrame": "Çerçevedeki tüm bileşenleri seç", "removeAllElementsFromFrame": "Çerçevedeki tüm bileşenleri sil", - "eyeDropper": "Tuvalden renk seç" + "eyeDropper": "Tuvalden renk seç", + "textToDiagram": "", + "prompt": "" }, "library": { "noItems": "Öğe eklenmedi...", @@ -209,6 +217,7 @@ "importLibraryError": "Kütüphane yüklenemedi", "collabSaveFailed": "Backend veritabanına kaydedilemedi. Eğer problem devam ederse, çalışmanızı korumak için dosyayı yerel olarak kaydetmelisiniz.", "collabSaveFailed_sizeExceeded": "Backend veritabanına kaydedilemedi; tuval çok büyük. Çalışmanızı korumak için dosyayı yerel olarak kaydetmelisiniz.", + "imageToolNotSupported": "", "brave_measure_text_error": { "line1": "", "line2": "", @@ -217,8 +226,12 @@ }, "libraryElementTypeError": { "embeddable": "", + "iframe": "", "image": "Resimleri kütüphaneye ekleme desteği yakında geliyor!" - } + }, + "asyncPasteFailedOnRead": "", + "asyncPasteFailedOnParse": "", + "copyToSystemClipboardFailed": "" }, "toolBar": { "selection": "Seçme", @@ -236,10 +249,13 @@ "link": "Seçilen şekil için bağlantı Ekle/Güncelle", "eraser": "Silgi", "frame": "Çerçeve aracı", + "magicframe": "", "embeddable": "Web Yerleştirme", "laser": "Lazer işaretçisi", "hand": "", - "extraTools": "Daha fazla araç" + "extraTools": "Daha fazla araç", + "mermaidToExcalidraw": "", + "magicSettings": "" }, "headings": { "canvasActions": "Tuval eylemleri", @@ -498,5 +514,12 @@ "description": "" } } + }, + "mermaid": { + "title": "", + "button": "", + "description": "", + "syntax": "", + "preview": "" } } diff --git a/src/locales/uk-UA.json b/packages/excalidraw/locales/uk-UA.json similarity index 97% rename from src/locales/uk-UA.json rename to packages/excalidraw/locales/uk-UA.json index 5e3c4add9..8f0b43672 100644 --- a/src/locales/uk-UA.json +++ b/packages/excalidraw/locales/uk-UA.json @@ -11,6 +11,8 @@ "copyAsPng": "Копіювати як PNG", "copyAsSvg": "Копіювати як SVG", "copyText": "Копіювати в буфер обміну як текст", + "copySource": "", + "convertToCode": "", "bringForward": "Перемістити вперед", "sendToBack": "На задній план", "bringToFront": "На передній план", @@ -36,8 +38,12 @@ "arrowhead_none": "Жоден", "arrowhead_arrow": "Стрілка", "arrowhead_bar": "Колона", - "arrowhead_dot": "Точка", + "arrowhead_circle": "", + "arrowhead_circle_outline": "", "arrowhead_triangle": "Трикутник", + "arrowhead_triangle_outline": "", + "arrowhead_diamond": "", + "arrowhead_diamond_outline": "", "fontSize": "Розмір шрифту", "fontFamily": "Шрифт", "addWatermark": "Додати «Накреслене в Excalidraw»", @@ -130,7 +136,9 @@ "sidebarLock": "Не закривати бокове меню", "selectAllElementsInFrame": "Обрати всі елементи у фреймі", "removeAllElementsFromFrame": "Видалити всі елементи з фрейму", - "eyeDropper": "Вибрати колір з полотна" + "eyeDropper": "Вибрати колір з полотна", + "textToDiagram": "", + "prompt": "" }, "library": { "noItems": "Тут поки пусто...", @@ -209,6 +217,7 @@ "importLibraryError": "Не вдалося завантажити бібліотеку", "collabSaveFailed": "Не вдалося зберегти у базу даних сервера. Якщо проблеми не зникнуть, Вам слід зберегти файл локально, щоб не втратити роботу.", "collabSaveFailed_sizeExceeded": "Полотно завелике! Не вдалося зберегти у базу даних сервера. Вам слід зберегти файл локально, щоб не втратити свою роботу.", + "imageToolNotSupported": "", "brave_measure_text_error": { "line1": "Ви використовуєте браузер Brave з увімкненим налаштуванням Агресивного Блокування Розпізнавання Пристрою.", "line2": "Це може нашкодити текстовим елементам у ваших малюнках.", @@ -217,8 +226,12 @@ }, "libraryElementTypeError": { "embeddable": "Вбудовані елементи не можна додати в бібліотеку.", + "iframe": "", "image": "Підтримка додавання зображень в бібліотеку найближчим часом!" - } + }, + "asyncPasteFailedOnRead": "", + "asyncPasteFailedOnParse": "", + "copyToSystemClipboardFailed": "" }, "toolBar": { "selection": "Виділення", @@ -236,10 +249,13 @@ "link": "Додати/Оновити посилання для вибраної форми", "eraser": "Очищувач", "frame": "Інструмент фрейму", + "magicframe": "", "embeddable": "Веб вкладення", "laser": "", "hand": "Рука (інструмент для панорамування)", - "extraTools": "Інші інструменти" + "extraTools": "Інші інструменти", + "mermaidToExcalidraw": "", + "magicSettings": "" }, "headings": { "canvasActions": "Дії з полотном", @@ -498,5 +514,12 @@ "description": "Завантаження зовнішнього малюнка замінить ваш наявний вміст.

Ви можете спочатку створити резервну копію малюнка, скориставшись одним із наведених нижче способів." } } + }, + "mermaid": { + "title": "", + "button": "", + "description": "", + "syntax": "", + "preview": "" } } diff --git a/src/locales/vi-VN.json b/packages/excalidraw/locales/vi-VN.json similarity index 96% rename from src/locales/vi-VN.json rename to packages/excalidraw/locales/vi-VN.json index 2f8381b55..5e24dc3e3 100644 --- a/src/locales/vi-VN.json +++ b/packages/excalidraw/locales/vi-VN.json @@ -11,6 +11,8 @@ "copyAsPng": "Sao chép vào bộ nhớ tạm dưới dạng PNG", "copyAsSvg": "Sao chép vào bộ nhớ tạm dưới dạng SVG", "copyText": "Sao chép vào bộ nhớ tạm dưới dạng chữ", + "copySource": "", + "convertToCode": "", "bringForward": "Đưa ra trước", "sendToBack": "Hạ xuống dưới", "bringToFront": "Đưa ra đầu tiên", @@ -36,8 +38,12 @@ "arrowhead_none": "Không", "arrowhead_arrow": "Mũi tên", "arrowhead_bar": "Thanh", - "arrowhead_dot": "Chấm", + "arrowhead_circle": "", + "arrowhead_circle_outline": "", "arrowhead_triangle": "Tam giác", + "arrowhead_triangle_outline": "", + "arrowhead_diamond": "", + "arrowhead_diamond_outline": "", "fontSize": "Cỡ chữ", "fontFamily": "Phông chữ", "addWatermark": "Làm với Excalidraw\"", @@ -130,7 +136,9 @@ "sidebarLock": "Giữ thanh bên luôn mở", "selectAllElementsInFrame": "", "removeAllElementsFromFrame": "", - "eyeDropper": "" + "eyeDropper": "", + "textToDiagram": "", + "prompt": "" }, "library": { "noItems": "Chưa có món nào...", @@ -209,6 +217,7 @@ "importLibraryError": "Không thể tải thư viện", "collabSaveFailed": "Không thể lưu vào cơ sở dữ liệu. Nếu vấn đề tiếp tục xảy ra, bạn nên lưu tệp vào máy để đảm bảo bạn không bị mất công việc.", "collabSaveFailed_sizeExceeded": "Không thể lưu vào cơ sở dữ liệu, canvas có vẻ quá lớn. Bạn nên lưu tệp cục bộ để đảm bảo bạn không bị mất công việc.", + "imageToolNotSupported": "", "brave_measure_text_error": { "line1": "", "line2": "", @@ -217,8 +226,12 @@ }, "libraryElementTypeError": { "embeddable": "", + "iframe": "", "image": "" - } + }, + "asyncPasteFailedOnRead": "", + "asyncPasteFailedOnParse": "", + "copyToSystemClipboardFailed": "" }, "toolBar": { "selection": "Lựa chọn", @@ -236,10 +249,13 @@ "link": "Thêm/ Chỉnh sửa liên kết cho hình được chọn", "eraser": "Xóa", "frame": "", + "magicframe": "", "embeddable": "", "laser": "", "hand": "Tay kéo", - "extraTools": "" + "extraTools": "", + "mermaidToExcalidraw": "", + "magicSettings": "" }, "headings": { "canvasActions": "Hành động canvas", @@ -498,5 +514,12 @@ "description": "" } } + }, + "mermaid": { + "title": "", + "button": "", + "description": "", + "syntax": "", + "preview": "" } } diff --git a/src/locales/zh-CN.json b/packages/excalidraw/locales/zh-CN.json similarity index 93% rename from src/locales/zh-CN.json rename to packages/excalidraw/locales/zh-CN.json index 04c5b4d40..19d715750 100644 --- a/src/locales/zh-CN.json +++ b/packages/excalidraw/locales/zh-CN.json @@ -11,6 +11,8 @@ "copyAsPng": "复制为 PNG 到剪贴板", "copyAsSvg": "复制为 SVG 到剪贴板", "copyText": "复制文本到剪贴板", + "copySource": "复制源码到剪贴板", + "convertToCode": "转换成代码", "bringForward": "上移一层", "sendToBack": "置于底层", "bringToFront": "置于顶层", @@ -36,8 +38,12 @@ "arrowhead_none": "无", "arrowhead_arrow": "箭头", "arrowhead_bar": "条状", - "arrowhead_dot": "圆点", + "arrowhead_circle": "圆点", + "arrowhead_circle_outline": "圆点(空心)", "arrowhead_triangle": "三角箭头", + "arrowhead_triangle_outline": "三角箭头(空心)", + "arrowhead_diamond": "菱形", + "arrowhead_diamond_outline": "菱形(空心)", "fontSize": "字体大小", "fontFamily": "字体", "addWatermark": "添加 “使用 Excalidraw 创建” 水印", @@ -130,7 +136,9 @@ "sidebarLock": "侧边栏常驻", "selectAllElementsInFrame": "选择画框中的所有元素", "removeAllElementsFromFrame": "分离出画框中的所有元素", - "eyeDropper": "从画布上取色" + "eyeDropper": "从画布上取色", + "textToDiagram": "文字至图表", + "prompt": "Prompt" }, "library": { "noItems": "尚未添加任何项目……", @@ -203,12 +211,13 @@ "imageInsertError": "无法插入图像。请稍后再试……", "fileTooBig": "文件过大。最大允许的大小为 {{maxSize}}。", "svgImageInsertError": "无法插入 SVG 图像。该 SVG 标记似乎是无效的。", - "failedToFetchImage": "", + "failedToFetchImage": "无法获取图片。", "invalidSVGString": "无效的 SVG。", "cannotResolveCollabServer": "无法连接到实时协作服务器。请重新加载页面并重试。", "importLibraryError": "无法加载素材库", "collabSaveFailed": "无法保存到后端数据库。如果问题持续存在,您应该保存文件到本地,以确保您的工作不会丢失。", "collabSaveFailed_sizeExceeded": "无法保存到后端数据库,画布似乎过大。您应该保存文件到本地,以确保您的工作不会丢失。", + "imageToolNotSupported": "图片已被禁用。", "brave_measure_text_error": { "line1": "您似乎正在使用 Brave 浏览器并启用了积极阻止指纹识别的设置。", "line2": "这可能会破坏绘图中的 文本元素。", @@ -216,9 +225,13 @@ "line4": "如果禁用此设置无法修复文本元素的显示,请在 GitHub 上提交一个 issue ,或者在 Discord 上反馈" }, "libraryElementTypeError": { - "embeddable": "嵌入元素不能添加到素材库。", + "embeddable": "嵌入的元素不能被添加到素材库。", + "iframe": "不能将 IFrame 元素添加到素材库中。", "image": "我们不久将支持添加图片到素材库" - } + }, + "asyncPasteFailedOnRead": "无法粘贴(无法读取系统剪贴板)。", + "asyncPasteFailedOnParse": "无法粘贴。", + "copyToSystemClipboardFailed": "无法复制到剪贴板。" }, "toolBar": { "selection": "选择", @@ -236,10 +249,13 @@ "link": "为选中的形状添加/更新链接", "eraser": "橡皮", "frame": "画框工具", + "magicframe": "线框图至代码", "embeddable": "嵌入网页", "laser": "激光笔", "hand": "抓手(平移工具)", - "extraTools": "更多工具" + "extraTools": "更多工具", + "mermaidToExcalidraw": "Mermaid 至 Excalidraw", + "magicSettings": "AI 设置" }, "headings": { "canvasActions": "画布动作", @@ -268,7 +284,7 @@ "deepBoxSelect": "按住 CtrlOrCmd 以深度选择,并避免拖拽", "eraserRevert": "按住 Alt 以反选被标记删除的元素", "firefox_clipboard_write": "将高级配置首选项“dom.events.asyncClipboard.clipboardItem”设置为“true”可以启用此功能。要更改 Firefox 的高级配置首选项,请前往“about:config”页面。", - "disableSnapping": "按住 CtrlOrCmd 以禁用吸附" + "disableSnapping": "按住 Ctrl 或 Cmd 以禁用吸附" }, "canvasError": { "cannotShowPreview": "无法显示预览", @@ -320,7 +336,7 @@ "editor": "编辑器", "editLineArrowPoints": "编辑线条或箭头的点", "editText": "添加或编辑文本", - "github": "提交问题", + "github": "发现问题?提交反馈", "howto": "帮助文档", "or": "或", "preventBinding": "禁用箭头吸附", @@ -498,5 +514,12 @@ "description": "加载外部绘图将替换您现有的内容

您可以先使用下列方式备份您的绘图。" } } + }, + "mermaid": { + "title": "Mermaid 至 Excalidraw", + "button": "插入", + "description": "目前仅支持流程图序列图类图。其他类型在 Excalidraw 中将以图像呈现。", + "syntax": "Mermaid 语法", + "preview": "预览" } } diff --git a/src/locales/zh-HK.json b/packages/excalidraw/locales/zh-HK.json similarity index 94% rename from src/locales/zh-HK.json rename to packages/excalidraw/locales/zh-HK.json index 1ecdb0ce3..d1760a804 100644 --- a/src/locales/zh-HK.json +++ b/packages/excalidraw/locales/zh-HK.json @@ -11,6 +11,8 @@ "copyAsPng": "以 PNG 格式複製", "copyAsSvg": "以 SVG 格式複製", "copyText": "", + "copySource": "", + "convertToCode": "", "bringForward": "往上一層移動", "sendToBack": "移到最底層", "bringToFront": "移到最上層", @@ -36,8 +38,12 @@ "arrowhead_none": "無箭嘴", "arrowhead_arrow": "普通箭嘴", "arrowhead_bar": "平頭條狀", - "arrowhead_dot": "圓點", + "arrowhead_circle": "", + "arrowhead_circle_outline": "", "arrowhead_triangle": "三角箭嘴", + "arrowhead_triangle_outline": "", + "arrowhead_diamond": "", + "arrowhead_diamond_outline": "", "fontSize": "字型大小", "fontFamily": "字體", "addWatermark": "加入「使用 Excalidraw 製圖」水印", @@ -130,7 +136,9 @@ "sidebarLock": "", "selectAllElementsInFrame": "", "removeAllElementsFromFrame": "", - "eyeDropper": "" + "eyeDropper": "", + "textToDiagram": "", + "prompt": "" }, "library": { "noItems": "", @@ -209,6 +217,7 @@ "importLibraryError": "", "collabSaveFailed": "", "collabSaveFailed_sizeExceeded": "", + "imageToolNotSupported": "", "brave_measure_text_error": { "line1": "", "line2": "", @@ -217,8 +226,12 @@ }, "libraryElementTypeError": { "embeddable": "", + "iframe": "", "image": "" - } + }, + "asyncPasteFailedOnRead": "", + "asyncPasteFailedOnParse": "", + "copyToSystemClipboardFailed": "" }, "toolBar": { "selection": "", @@ -236,10 +249,13 @@ "link": "", "eraser": "", "frame": "", + "magicframe": "", "embeddable": "", "laser": "", "hand": "", - "extraTools": "" + "extraTools": "", + "mermaidToExcalidraw": "", + "magicSettings": "" }, "headings": { "canvasActions": "畫布動作", @@ -498,5 +514,12 @@ "description": "" } } + }, + "mermaid": { + "title": "", + "button": "", + "description": "", + "syntax": "", + "preview": "" } } diff --git a/src/locales/zh-TW.json b/packages/excalidraw/locales/zh-TW.json similarity index 94% rename from src/locales/zh-TW.json rename to packages/excalidraw/locales/zh-TW.json index 64826dc60..e116d9163 100644 --- a/src/locales/zh-TW.json +++ b/packages/excalidraw/locales/zh-TW.json @@ -11,6 +11,8 @@ "copyAsPng": "以PNG格式儲存到剪貼板", "copyAsSvg": "以SVG格式複製到剪貼板", "copyText": "以文字格式複製至剪貼簿", + "copySource": "複製來源至剪貼簿", + "convertToCode": "轉換為程式碼", "bringForward": "上移一層", "sendToBack": "移到最底層", "bringToFront": "置於最頂層", @@ -36,8 +38,12 @@ "arrowhead_none": "無", "arrowhead_arrow": "箭頭", "arrowhead_bar": "條狀箭頭", - "arrowhead_dot": "點箭頭", + "arrowhead_circle": "圓形", + "arrowhead_circle_outline": "圓形(外框)", "arrowhead_triangle": "三角形", + "arrowhead_triangle_outline": "三角形(外框)", + "arrowhead_diamond": "菱形", + "arrowhead_diamond_outline": "菱形(外框)", "fontSize": "字型大小", "fontFamily": "字體集", "addWatermark": "加上 \"Made with Excalidraw\" 浮水印", @@ -130,7 +136,9 @@ "sidebarLock": "側欄維持開啟", "selectAllElementsInFrame": "選取框架內的所有元素", "removeAllElementsFromFrame": "從框架內移除所有元素", - "eyeDropper": "從畫布中選取顏色" + "eyeDropper": "從畫布中選取顏色", + "textToDiagram": "文字轉圖表", + "prompt": "提示詞" }, "library": { "noItems": "尚未加入任何物件...", @@ -209,6 +217,7 @@ "importLibraryError": "無法載入資料庫", "collabSaveFailed": "無法儲存至後端資料庫。若此問題持續發生,請將檔案儲存於本機以確保資料不會遺失。", "collabSaveFailed_sizeExceeded": "無法儲存至後端資料庫,可能的原因為畫布尺寸過大。請將檔案儲存於本機以確保資料不會遺失。", + "imageToolNotSupported": "圖片已停用", "brave_measure_text_error": { "line1": "看起來您開啟了 Brave 瀏覽器的 Aggressively Block Fingerprinting 設定。", "line2": "這可能造成您畫布中 文字元素 的異常。", @@ -217,8 +226,12 @@ }, "libraryElementTypeError": { "embeddable": "可嵌入元素無法加入資料庫", + "iframe": "IFrame 元素無法加入資料庫", "image": "即將支援加入圖片至資料庫!" - } + }, + "asyncPasteFailedOnRead": "無法貼上(無法由系統剪貼簿讀入)", + "asyncPasteFailedOnParse": "無法貼上", + "copyToSystemClipboardFailed": "無法複製至剪貼簿" }, "toolBar": { "selection": "選取", @@ -236,10 +249,13 @@ "link": "為所選的形狀增加\b/更新連結", "eraser": "橡皮擦", "frame": "框架工具", + "magicframe": "線框稿轉為程式碼", "embeddable": "嵌入網站", "laser": "雷射筆", "hand": "手形(平移工具)", - "extraTools": "更多工具" + "extraTools": "更多工具", + "mermaidToExcalidraw": "Mermaid 至 Excalidraw", + "magicSettings": "AI 設定" }, "headings": { "canvasActions": "canvas 動作", @@ -498,5 +514,12 @@ "description": "載入外部繪圖將取代您目前的內容

可先使用下方的選項備份您的繪圖。" } } + }, + "mermaid": { + "title": "Mermaid 至 Excalidraw", + "button": "插入", + "description": "目前僅支援 FlowchartSequenceClass 圖表。其餘檔案類型在 Excalidraw 將會以圖像呈現。", + "syntax": "Mermaid 語法", + "preview": "預覽" } } diff --git a/src/math.test.ts b/packages/excalidraw/math.test.ts similarity index 100% rename from src/math.test.ts rename to packages/excalidraw/math.test.ts diff --git a/src/math.ts b/packages/excalidraw/math.ts similarity index 97% rename from src/math.ts rename to packages/excalidraw/math.ts index a56b97a72..8c0fb0ebd 100644 --- a/src/math.ts +++ b/packages/excalidraw/math.ts @@ -15,18 +15,20 @@ import { Mutable } from "./utility-types"; import { ShapeCache } from "./scene/ShapeCache"; export const rotate = ( - x1: number, - y1: number, - x2: number, - y2: number, + // target point to rotate + x: number, + y: number, + // point to rotate against + cx: number, + cy: number, angle: number, ): [number, number] => // 𝑎′𝑥=(𝑎𝑥−𝑐𝑥)cos𝜃−(𝑎𝑦−𝑐𝑦)sin𝜃+𝑐𝑥 // 𝑎′𝑦=(𝑎𝑥−𝑐𝑥)sin𝜃+(𝑎𝑦−𝑐𝑦)cos𝜃+𝑐𝑦. // https://math.stackexchange.com/questions/2204520/how-do-i-rotate-a-line-segment-in-a-specific-point-on-the-line [ - (x1 - x2) * Math.cos(angle) - (y1 - y2) * Math.sin(angle) + x2, - (x1 - x2) * Math.sin(angle) + (y1 - y2) * Math.cos(angle) + y2, + (x - cx) * Math.cos(angle) - (y - cy) * Math.sin(angle) + cx, + (x - cx) * Math.sin(angle) + (y - cy) * Math.cos(angle) + cy, ]; export const rotatePoint = ( @@ -303,7 +305,7 @@ export const getControlPointsForBezierCurve = ( element: NonDeleted, endPoint: Point, ) => { - const shape = ShapeCache.generateElementShape(element); + const shape = ShapeCache.generateElementShape(element, null); if (!shape) { return null; } diff --git a/packages/excalidraw/package.json b/packages/excalidraw/package.json new file mode 100644 index 000000000..f0b265fce --- /dev/null +++ b/packages/excalidraw/package.json @@ -0,0 +1,137 @@ +{ + "name": "@excalidraw/excalidraw", + "version": "0.17.1", + "main": "./dist/prod/index.js", + "type": "module", + "module": "./dist/prod/index.js", + "exports": { + ".": { + "development": "./dist/dev/index.js", + "types": "./dist/excalidraw/index.d.ts", + "default": "./dist/prod/index.js" + }, + "./index.css": { + "development": "./dist/dev/index.css", + "default": "./dist/prod/index.css" + } + }, + "types": "./dist/excalidraw/index.d.ts", + "files": [ + "dist/*" + ], + "publishConfig": { + "access": "public" + }, + "description": "Excalidraw as a React component", + "repository": "https://github.com/excalidraw/excalidraw", + "license": "MIT", + "keywords": [ + "excalidraw", + "excalidraw-embed", + "react", + "npm", + "npm excalidraw" + ], + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not ie <= 11", + "not op_mini all", + "not safari < 12", + "not kaios <= 2.5", + "not edge < 79", + "not chrome < 70", + "not and_uc < 13", + "not samsung < 10" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + }, + "peerDependencies": { + "react": "^17.0.2 || ^18.2.0", + "react-dom": "^17.0.2 || ^18.2.0" + }, + "dependencies": { + "@braintree/sanitize-url": "6.0.2", + "@excalidraw/laser-pointer": "1.3.1", + "@excalidraw/mermaid-to-excalidraw": "0.2.0", + "@excalidraw/random-username": "1.1.0", + "@radix-ui/react-popover": "1.0.3", + "@radix-ui/react-tabs": "1.0.2", + "@tldraw/vec": "1.7.1", + "browser-fs-access": "0.29.1", + "canvas-roundrect-polyfill": "0.0.1", + "clsx": "1.1.1", + "cross-env": "7.0.3", + "image-blob-reduce": "3.0.1", + "jotai": "1.13.1", + "lodash.throttle": "4.1.1", + "mathjax-full": "3.2.2", + "nanoid": "3.3.3", + "open-color": "1.9.1", + "pako": "1.0.11", + "patch-package": "8.0.0", + "perfect-freehand": "1.2.0", + "pica": "7.1.1", + "png-chunk-text": "1.0.0", + "png-chunks-encode": "1.0.0", + "png-chunks-extract": "1.0.0", + "points-on-curve": "1.0.1", + "postinstall-postinstall": "2.1.0", + "pwacompat": "2.0.17", + "roughjs": "4.6.4", + "sass": "1.51.0", + "tunnel-rat": "0.1.2" + }, + "devDependencies": { + "@babel/core": "7.18.9", + "@babel/plugin-transform-arrow-functions": "7.18.6", + "@babel/plugin-transform-async-to-generator": "7.18.6", + "@babel/plugin-transform-runtime": "7.18.9", + "@babel/plugin-transform-typescript": "7.18.8", + "@babel/preset-env": "7.18.6", + "@babel/preset-react": "7.18.6", + "@babel/preset-typescript": "7.18.6", + "@size-limit/preset-big-lib": "9.0.0", + "@types/pako": "1.0.3", + "@types/pica": "5.1.3", + "@types/resize-observer-browser": "0.1.7", + "autoprefixer": "10.4.7", + "babel-loader": "8.2.5", + "babel-plugin-transform-class-properties": "6.24.1", + "cross-env": "7.0.3", + "css-loader": "6.7.1", + "dotenv": "16.0.1", + "esbuild": "0.19.10", + "esbuild-plugin-external-global": "1.0.1", + "esbuild-sass-plugin": "2.16.0", + "eslint-plugin-react": "7.32.2", + "fake-indexeddb": "3.1.7", + "import-meta-loader": "1.1.0", + "mini-css-extract-plugin": "2.6.1", + "postcss-loader": "7.0.1", + "react": "18.2.0", + "react-dom": "18.2.0", + "sass-loader": "13.0.2", + "size-limit": "9.0.0", + "style-loader": "3.3.3", + "@testing-library/jest-dom": "5.16.2", + "@testing-library/react": "12.1.5", + "ts-loader": "9.3.1", + "typescript": "4.9.4" + }, + "bugs": "https://github.com/excalidraw/excalidraw/issues", + "homepage": "https://github.com/excalidraw/excalidraw/tree/master/packages/excalidraw", + "scripts": { + "gen:types": "rm -rf types && tsc", + "build:esm": "rm -rf dist && node ../../scripts/buildPackage.js && yarn gen:types", + "pack": "yarn build:umd && yarn pack", + "start": "node ../../scripts/buildExample.mjs && vite", + "build:example": "node ../../scripts/buildExample.mjs", + "size": "yarn build:umd && size-limit" + } +} diff --git a/src/points.ts b/packages/excalidraw/points.ts similarity index 100% rename from src/points.ts rename to packages/excalidraw/points.ts diff --git a/src/polyfill.ts b/packages/excalidraw/polyfill.ts similarity index 100% rename from src/polyfill.ts rename to packages/excalidraw/polyfill.ts diff --git a/src/pwacompat.d.ts b/packages/excalidraw/pwacompat.d.ts similarity index 100% rename from src/pwacompat.d.ts rename to packages/excalidraw/pwacompat.d.ts diff --git a/src/random.ts b/packages/excalidraw/random.ts similarity index 100% rename from src/random.ts rename to packages/excalidraw/random.ts diff --git a/src/react-app-env.d.ts b/packages/excalidraw/react-app-env.d.ts similarity index 100% rename from src/react-app-env.d.ts rename to packages/excalidraw/react-app-env.d.ts diff --git a/packages/excalidraw/reactUtils.ts b/packages/excalidraw/reactUtils.ts new file mode 100644 index 000000000..535302d42 --- /dev/null +++ b/packages/excalidraw/reactUtils.ts @@ -0,0 +1,61 @@ +/** + * @param func handler taking at most single parameter (event). + */ + +import { unstable_batchedUpdates } from "react-dom"; +import { version as ReactVersion } from "react"; +import { throttleRAF } from "./utils"; + +export const withBatchedUpdates = < + TFunction extends ((event: any) => void) | (() => void), +>( + func: Parameters["length"] extends 0 | 1 ? TFunction : never, +) => + ((event) => { + unstable_batchedUpdates(func as TFunction, event); + }) as TFunction; + +/** + * barches React state updates and throttles the calls to a single call per + * animation frame + */ +export const withBatchedUpdatesThrottled = < + TFunction extends ((event: any) => void) | (() => void), +>( + func: Parameters["length"] extends 0 | 1 ? TFunction : never, +) => { + // @ts-ignore + return throttleRAF>(((event) => { + unstable_batchedUpdates(func, event); + }) as TFunction); +}; + +export const isRenderThrottlingEnabled = (() => { + // we don't want to throttle in react < 18 because of #5439 and it was + // getting more complex to maintain the fix + let IS_REACT_18_AND_UP: boolean; + try { + const version = ReactVersion.split("."); + IS_REACT_18_AND_UP = Number(version[0]) > 17; + } catch { + IS_REACT_18_AND_UP = false; + } + + let hasWarned = false; + + return () => { + if (window.EXCALIDRAW_THROTTLE_RENDER === true) { + if (!IS_REACT_18_AND_UP) { + if (!hasWarned) { + hasWarned = true; + console.warn( + "Excalidraw: render throttling is disabled on React versions < 18.", + ); + } + return false; + } + return true; + } + return false; + }; +})(); diff --git a/src/renderer/renderElement.ts b/packages/excalidraw/renderer/renderElement.ts similarity index 91% rename from src/renderer/renderElement.ts rename to packages/excalidraw/renderer/renderElement.ts index b3c6da32c..9f15e99c4 100644 --- a/src/renderer/renderElement.ts +++ b/packages/excalidraw/renderer/renderElement.ts @@ -5,6 +5,8 @@ import { ExcalidrawFreeDrawElement, ExcalidrawImageElement, ExcalidrawTextElementWithContainer, + ExcalidrawFrameLikeElement, + NonDeletedSceneElementsMap, } from "../element/types"; import { isTextElement, @@ -20,7 +22,11 @@ import type { RoughCanvas } from "roughjs/bin/canvas"; import type { Drawable } from "roughjs/bin/core"; import type { RoughSVG } from "roughjs/bin/svg"; -import { StaticCanvasRenderConfig } from "../scene/types"; +import { + SVGRenderConfig, + StaticCanvasRenderConfig, + RenderableElementsMap, +} from "../scene/types"; import { distance, getFontString, @@ -36,11 +42,13 @@ import { BinaryFiles, Zoom, InteractiveCanvasAppState, + ElementsPendingErasure, } from "../types"; import { getDefaultAppState } from "../appState"; import { getSubtypeMethods } from "../element/subtypes"; import { BOUND_TEXT_PADDING, + ELEMENT_READY_TO_ERASE_OPACITY, FRAME_STYLE, MAX_DECIMALS_FOR_SVG_EXPORT, MIME_TYPES, @@ -95,6 +103,27 @@ const shouldResetImageFilter = ( const getCanvasPadding = (element: ExcalidrawElement) => element.type === "freedraw" ? element.strokeWidth * 12 : 20; +export const getRenderOpacity = ( + element: ExcalidrawElement, + containingFrame: ExcalidrawFrameLikeElement | null, + elementsPendingErasure: ElementsPendingErasure, +) => { + // multiplying frame opacity with element opacity to combine them + // (e.g. frame 50% and element 50% opacity should result in 25% opacity) + let opacity = ((containingFrame?.opacity ?? 100) * element.opacity) / 10000; + + // if pending erasure, multiply again to combine further + // (so that erasing always results in lower opacity than original) + if ( + elementsPendingErasure.has(element.id) || + (containingFrame && elementsPendingErasure.has(containingFrame.id)) + ) { + opacity *= ELEMENT_READY_TO_ERASE_OPACITY / 100; + } + + return opacity; +}; + export interface ExcalidrawElementWithCanvas { element: ExcalidrawElement | ExcalidrawTextElement; canvas: HTMLCanvasElement; @@ -163,6 +192,7 @@ const cappedElementCanvasSize = ( const generateElementCanvas = ( element: NonDeletedExcalidrawElement, + elementsMap: RenderableElementsMap, zoom: Zoom, renderConfig: StaticCanvasRenderConfig, appState: StaticCanvasAppState, @@ -209,7 +239,14 @@ const generateElementCanvas = ( context.filter = IMAGE_INVERT_FILTER; } - drawElementOnCanvas(element, rc, context, renderConfig, appState); + drawElementOnCanvas( + element, + elementsMap, + rc, + context, + renderConfig, + appState, + ); context.restore(); return { @@ -220,7 +257,8 @@ const generateElementCanvas = ( zoomValue: zoom.value, canvasOffsetX, canvasOffsetY, - boundTextElementVersion: getBoundTextElement(element)?.version || null, + boundTextElementVersion: + getBoundTextElement(element, elementsMap)?.version || null, containingFrameOpacity: getContainingFrame(element)?.opacity || 100, }; }; @@ -265,6 +303,7 @@ const drawImagePlaceholder = ( const drawElementOnCanvas = ( element: NonDeletedExcalidrawElement, + elementsMap: RenderableElementsMap, rc: RoughCanvas, context: CanvasRenderingContext2D, renderConfig: StaticCanvasRenderConfig, @@ -274,7 +313,7 @@ const drawElementOnCanvas = ( ((getContainingFrame(element)?.opacity ?? 100) * element.opacity) / 10000; const map = getSubtypeMethods(element.subtype); if (map?.render) { - map.render(element, context); + map.render(element, elementsMap, context); context.globalAlpha = 1; return; } @@ -322,6 +361,17 @@ const drawElementOnCanvas = ( ? renderConfig.imageCache.get(element.fileId)?.image : undefined; if (img != null && !(img instanceof Promise)) { + if (element.roundness && context.roundRect) { + context.beginPath(); + context.roundRect( + 0, + 0, + element.width, + element.height, + getCornerRadius(Math.min(element.width, element.height), element), + ); + context.clip(); + } context.drawImage( img, 0 /* hardcoded for the selection box*/, @@ -379,7 +429,6 @@ const drawElementOnCanvas = ( } } } - context.globalAlpha = 1; }; export const elementWithCanvasCache = new WeakMap< @@ -389,6 +438,7 @@ export const elementWithCanvasCache = new WeakMap< const generateElementWithCanvas = ( element: NonDeletedExcalidrawElement, + elementsMap: RenderableElementsMap, renderConfig: StaticCanvasRenderConfig, appState: StaticCanvasAppState, ) => { @@ -398,7 +448,9 @@ const generateElementWithCanvas = ( prevElementWithCanvas && prevElementWithCanvas.zoomValue !== zoom.value && !appState?.shouldCacheIgnoreZoom; - const boundTextElementVersion = getBoundTextElement(element)?.version || null; + const boundTextElementVersion = + getBoundTextElement(element, elementsMap)?.version || null; + const containingFrameOpacity = getContainingFrame(element)?.opacity || 100; if ( @@ -410,6 +462,7 @@ const generateElementWithCanvas = ( ) { const elementWithCanvas = generateElementCanvas( element, + elementsMap, zoom, renderConfig, appState, @@ -427,6 +480,7 @@ const drawElementFromCanvas = ( context: CanvasRenderingContext2D, renderConfig: StaticCanvasRenderConfig, appState: StaticCanvasAppState, + allElementsMap: NonDeletedSceneElementsMap, ) => { const element = elementWithCanvas.element; const padding = getCanvasPadding(element); @@ -446,7 +500,8 @@ const drawElementFromCanvas = ( context.save(); context.scale(1 / window.devicePixelRatio, 1 / window.devicePixelRatio); - const boundTextElement = getBoundTextElement(element); + + const boundTextElement = getBoundTextElement(element, allElementsMap); if (isArrowElement(element) && boundTextElement) { const tempCanvas = document.createElement("canvas"); @@ -493,7 +548,6 @@ const drawElementFromCanvas = ( offsetY - padding * zoom; tempCanvasContext.translate(-shiftX, -shiftY); - // Clear the bound text area tempCanvasContext.clearRect( -(boundTextElement.width / 2 + BOUND_TEXT_PADDING) * @@ -555,6 +609,7 @@ const drawElementFromCanvas = ( ) { const textElement = getBoundTextElement( element, + allElementsMap, ) as ExcalidrawTextElementWithContainer; const coords = getContainerCoords(element); context.strokeStyle = "#c92a2a"; @@ -562,7 +617,7 @@ const drawElementFromCanvas = ( context.strokeRect( (coords.x + appState.scrollX) * window.devicePixelRatio, (coords.y + appState.scrollY) * window.devicePixelRatio, - getBoundTextMaxWidth(element) * window.devicePixelRatio, + getBoundTextMaxWidth(element, textElement) * window.devicePixelRatio, getBoundTextMaxHeight(element, textElement) * window.devicePixelRatio, ); } @@ -597,11 +652,19 @@ export const renderSelectionElement = ( export const renderElement = ( element: NonDeletedExcalidrawElement, + elementsMap: RenderableElementsMap, + allElementsMap: NonDeletedSceneElementsMap, rc: RoughCanvas, context: CanvasRenderingContext2D, renderConfig: StaticCanvasRenderConfig, appState: StaticCanvasAppState, ) => { + context.globalAlpha = getRenderOpacity( + element, + getContainingFrame(element), + renderConfig.elementsPendingErasure, + ); + switch (element.type) { case "magicframe": case "frame": { @@ -645,7 +708,7 @@ export const renderElement = ( // TODO investigate if we can do this in situ. Right now we need to call // beforehand because math helpers (such as getElementAbsoluteCoords) // rely on existing shapes - ShapeCache.generateElementShape(element); + ShapeCache.generateElementShape(element, null); if (renderConfig.isExporting) { const [x1, y1, x2, y2] = getElementAbsoluteCoords(element); @@ -657,11 +720,19 @@ export const renderElement = ( context.translate(cx, cy); context.rotate(element.angle); context.translate(-shiftX, -shiftY); - drawElementOnCanvas(element, rc, context, renderConfig, appState); + drawElementOnCanvas( + element, + elementsMap, + rc, + context, + renderConfig, + appState, + ); context.restore(); } else { const elementWithCanvas = generateElementWithCanvas( element, + elementsMap, renderConfig, appState, ); @@ -670,6 +741,7 @@ export const renderElement = ( context, renderConfig, appState, + allElementsMap, ); } @@ -687,7 +759,7 @@ export const renderElement = ( // TODO investigate if we can do this in situ. Right now we need to call // beforehand because math helpers (such as getElementAbsoluteCoords) // rely on existing shapes - ShapeCache.generateElementShape(element, renderConfig.isExporting); + ShapeCache.generateElementShape(element, renderConfig); if (renderConfig.isExporting) { const [x1, y1, x2, y2] = getElementAbsoluteCoords(element); const cx = (x1 + x2) / 2 + appState.scrollX; @@ -695,7 +767,7 @@ export const renderElement = ( let shiftX = (x2 - x1) / 2 - (element.x - x1); let shiftY = (y2 - y1) / 2 - (element.y - y1); if (isTextElement(element)) { - const container = getContainerElement(element); + const container = getContainerElement(element, elementsMap); if (isArrowElement(container)) { const boundTextCoords = LinearElementEditor.getBoundTextElementPosition( @@ -712,7 +784,7 @@ export const renderElement = ( if (shouldResetImageFilter(element, renderConfig, appState)) { context.filter = "none"; } - const boundTextElement = getBoundTextElement(element); + const boundTextElement = getBoundTextElement(element, elementsMap); if (isArrowElement(element) && boundTextElement) { const tempCanvas = document.createElement("canvas"); @@ -745,6 +817,7 @@ export const renderElement = ( drawElementOnCanvas( element, + elementsMap, tempRc, tempCanvasContext, renderConfig, @@ -786,7 +859,14 @@ export const renderElement = ( } context.translate(-shiftX, -shiftY); - drawElementOnCanvas(element, rc, context, renderConfig, appState); + drawElementOnCanvas( + element, + elementsMap, + rc, + context, + renderConfig, + appState, + ); } context.restore(); @@ -795,6 +875,7 @@ export const renderElement = ( } else { const elementWithCanvas = generateElementWithCanvas( element, + elementsMap, renderConfig, appState, ); @@ -826,6 +907,7 @@ export const renderElement = ( context, renderConfig, appState, + allElementsMap, ); // reset @@ -838,6 +920,8 @@ export const renderElement = ( throw new Error(`Unimplemented type ${element.type}`); } } + + context.globalAlpha = 1; }; const roughSVGDrawWithPrecision = ( @@ -878,23 +962,20 @@ const maybeWrapNodesInFrameClipPath = ( export const renderElementToSvg = ( element: NonDeletedExcalidrawElement, + elementsMap: RenderableElementsMap, rsvg: RoughSVG, svgRoot: SVGElement, files: BinaryFiles, offsetX: number, offsetY: number, - renderConfig: { - exportWithDarkMode: boolean; - renderEmbeddables: boolean; - frameRendering: AppState["frameRendering"]; - }, + renderConfig: SVGRenderConfig, ) => { const offset = { x: offsetX, y: offsetY }; const [x1, y1, x2, y2] = getElementAbsoluteCoords(element); let cx = (x2 - x1) / 2 - (element.x - x1); let cy = (y2 - y1) / 2 - (element.y - y1); if (isTextElement(element)) { - const container = getContainerElement(element); + const container = getContainerElement(element, elementsMap); if (isArrowElement(container)) { const [x1, y1, x2, y2] = getElementAbsoluteCoords(container); @@ -930,7 +1011,10 @@ export const renderElementToSvg = ( const map = getSubtypeMethods(element.subtype); if (map?.renderSvg) { - map.renderSvg(svgRoot, addToRoot, element, { offsetX, offsetY }); + map.renderSvg(svgRoot, addToRoot, element, elementsMap, { + offsetX, + offsetY, + }); return; } @@ -946,7 +1030,7 @@ export const renderElementToSvg = ( case "rectangle": case "diamond": case "ellipse": { - const shape = ShapeCache.generateElementShape(element); + const shape = ShapeCache.generateElementShape(element, null); const node = roughSVGDrawWithPrecision( rsvg, shape, @@ -977,7 +1061,7 @@ export const renderElementToSvg = ( case "iframe": case "embeddable": { // render placeholder rectangle - const shape = ShapeCache.generateElementShape(element, true); + const shape = ShapeCache.generateElementShape(element, renderConfig); const node = roughSVGDrawWithPrecision( rsvg, shape, @@ -1001,6 +1085,7 @@ export const renderElementToSvg = ( createPlaceholderEmbeddableLabel(element); renderElementToSvg( label, + elementsMap, rsvg, root, files, @@ -1077,7 +1162,7 @@ export const renderElementToSvg = ( } case "line": case "arrow": { - const boundText = getBoundTextElement(element); + const boundText = getBoundTextElement(element, elementsMap); const maskPath = svgRoot.ownerDocument!.createElementNS(SVG_NS, "mask"); if (boundText) { maskPath.setAttribute("id", `mask-${element.id}`); @@ -1126,7 +1211,7 @@ export const renderElementToSvg = ( } group.setAttribute("stroke-linecap", "round"); - const shapes = ShapeCache.generateElementShape(element); + const shapes = ShapeCache.generateElementShape(element, renderConfig); shapes.forEach((shape) => { const node = roughSVGDrawWithPrecision( rsvg, @@ -1169,7 +1254,10 @@ export const renderElementToSvg = ( break; } case "freedraw": { - const backgroundFillShape = ShapeCache.generateElementShape(element); + const backgroundFillShape = ShapeCache.generateElementShape( + element, + renderConfig, + ); const node = backgroundFillShape ? roughSVGDrawWithPrecision( rsvg, @@ -1265,6 +1353,31 @@ export const renderElementToSvg = ( }) rotate(${degree} ${cx} ${cy})`, ); + if (element.roundness) { + const clipPath = svgRoot.ownerDocument!.createElementNS( + SVG_NS, + "clipPath", + ); + clipPath.id = `image-clipPath-${element.id}`; + + const clipRect = svgRoot.ownerDocument!.createElementNS( + SVG_NS, + "rect", + ); + const radius = getCornerRadius( + Math.min(element.width, element.height), + element, + ); + clipRect.setAttribute("width", `${element.width}`); + clipRect.setAttribute("height", `${element.height}`); + clipRect.setAttribute("rx", `${radius}`); + clipRect.setAttribute("ry", `${radius}`); + clipPath.appendChild(clipRect); + addToRoot(clipPath, element); + + g.setAttributeNS(SVG_NS, "clip-path", `url(#${clipPath.id})`); + } + const clipG = maybeWrapNodesInFrameClipPath( element, root, diff --git a/src/renderer/renderScene.ts b/packages/excalidraw/renderer/renderScene.ts similarity index 89% rename from src/renderer/renderScene.ts rename to packages/excalidraw/renderer/renderScene.ts index 671e90d06..d31d69650 100644 --- a/src/renderer/renderScene.ts +++ b/packages/excalidraw/renderer/renderScene.ts @@ -30,8 +30,10 @@ import { roundRect } from "./roundRect"; import { InteractiveCanvasRenderConfig, InteractiveSceneRenderConfig, + SVGRenderConfig, StaticCanvasRenderConfig, StaticSceneRenderConfig, + RenderableElementsMap, } from "../scene/types"; import { getScrollBars, @@ -60,9 +62,13 @@ import { TransformHandles, TransformHandleType, } from "../element/transformHandles"; -import { throttleRAF } from "../utils"; +import { arrayToMap, throttleRAF } from "../utils"; import { UserIdleState } from "../types"; -import { FRAME_STYLE, THEME_FILTER } from "../constants"; +import { + DEFAULT_TRANSFORM_HANDLE_SPACING, + FRAME_STYLE, + THEME_FILTER, +} from "../constants"; import { EXTERNAL_LINK_IMG, getLinkHandleFromCoords, @@ -74,18 +80,12 @@ import { isIframeLikeElement, isLinearElement, } from "../element/typeChecks"; -import { - isIframeLikeOrItsLabel, - createPlaceholderEmbeddableLabel, -} from "../element/embeddable"; +import { createPlaceholderEmbeddableLabel } from "../element/embeddable"; import { elementOverlapsWithFrame, getTargetFrame, isElementInFrame, } from "../frame"; -import "canvas-roundrect-polyfill"; - -export const DEFAULT_SPACING = 2; const strokeRectWithRotation = ( context: CanvasRenderingContext2D, @@ -248,6 +248,7 @@ const renderLinearPointHandles = ( context: CanvasRenderingContext2D, appState: InteractiveCanvasAppState, element: NonDeleted, + elementsMap: RenderableElementsMap, ) => { if (!appState.selectedLinearElement) { return; @@ -271,6 +272,7 @@ const renderLinearPointHandles = ( //Rendering segment mid points const midPoints = LinearElementEditor.getEditorMidPoints( element, + elementsMap, appState, ).filter((midPoint) => midPoint !== null) as Point[]; @@ -445,7 +447,7 @@ const bootstrapCanvas = ({ const _renderInteractiveScene = ({ canvas, - elements, + elementsMap, visibleElements, selectedElements, scale, @@ -453,7 +455,7 @@ const _renderInteractiveScene = ({ renderConfig, }: InteractiveSceneRenderConfig) => { if (canvas === null) { - return { atLeastOneVisibleElement: false, elements }; + return { atLeastOneVisibleElement: false, elementsMap }; } const [normalizedWidth, normalizedHeight] = getNormalizedCanvasDimensions( @@ -487,7 +489,12 @@ const _renderInteractiveScene = ({ }); if (editingLinearElement) { - renderLinearPointHandles(context, appState, editingLinearElement); + renderLinearPointHandles( + context, + appState, + editingLinearElement, + elementsMap, + ); } // Paint selection element @@ -530,6 +537,7 @@ const _renderInteractiveScene = ({ context, appState, selectedElements[0] as NonDeleted, + elementsMap, ); } @@ -555,81 +563,71 @@ const _renderInteractiveScene = ({ context, appState, selectedElements[0] as ExcalidrawLinearElement, + elementsMap, ); } const selectionColor = renderConfig.selectionColor || oc.black; if (showBoundingBox) { // Optimisation for finding quickly relevant element ids - const locallySelectedIds = selectedElements.reduce( - (acc: Record, element) => { - acc[element.id] = true; - return acc; - }, - {}, - ); + const locallySelectedIds = arrayToMap(selectedElements); - const selections = elements.reduce( - ( - acc: { - angle: number; - elementX1: number; - elementY1: number; - elementX2: number; - elementY2: number; - selectionColors: string[]; - dashed?: boolean; - cx: number; - cy: number; - activeEmbeddable: boolean; - }[], - element, - ) => { - const selectionColors = []; - // local user - if ( - locallySelectedIds[element.id] && - !isSelectedViaGroup(appState, element) - ) { - selectionColors.push(selectionColor); - } - // remote users - if (renderConfig.remoteSelectedElementIds[element.id]) { - selectionColors.push( - ...renderConfig.remoteSelectedElementIds[element.id].map( - (socketId: string) => { - const background = getClientColor(socketId); - return background; - }, - ), - ); - } + const selections: { + angle: number; + elementX1: number; + elementY1: number; + elementX2: number; + elementY2: number; + selectionColors: string[]; + dashed?: boolean; + cx: number; + cy: number; + activeEmbeddable: boolean; + }[] = []; - if (selectionColors.length) { - const [elementX1, elementY1, elementX2, elementY2, cx, cy] = - getElementAbsoluteCoords(element, true); - acc.push({ - angle: element.angle, - elementX1, - elementY1, - elementX2, - elementY2, - selectionColors, - dashed: !!renderConfig.remoteSelectedElementIds[element.id], - cx, - cy, - activeEmbeddable: - appState.activeEmbeddable?.element === element && - appState.activeEmbeddable.state === "active", - }); - } - return acc; - }, - [], - ); + for (const element of elementsMap.values()) { + const selectionColors = []; + // local user + if ( + locallySelectedIds.has(element.id) && + !isSelectedViaGroup(appState, element) + ) { + selectionColors.push(selectionColor); + } + // remote users + if (renderConfig.remoteSelectedElementIds[element.id]) { + selectionColors.push( + ...renderConfig.remoteSelectedElementIds[element.id].map( + (socketId: string) => { + const background = getClientColor(socketId); + return background; + }, + ), + ); + } + + if (selectionColors.length) { + const [elementX1, elementY1, elementX2, elementY2, cx, cy] = + getElementAbsoluteCoords(element, true); + selections.push({ + angle: element.angle, + elementX1, + elementY1, + elementX2, + elementY2, + selectionColors, + dashed: !!renderConfig.remoteSelectedElementIds[element.id], + cx, + cy, + activeEmbeddable: + appState.activeEmbeddable?.element === element && + appState.activeEmbeddable.state === "active", + }); + } + } const addSelectionForGroupId = (groupId: GroupId) => { - const groupElements = getElementsInGroup(elements, groupId); + const groupElements = getElementsInGroup(elementsMap, groupId); const [elementX1, elementY1, elementX2, elementY2] = getCommonBounds(groupElements); selections.push({ @@ -680,7 +678,8 @@ const _renderInteractiveScene = ({ ); } } else if (selectedElements.length > 1 && !appState.isRotating) { - const dashedLinePadding = (DEFAULT_SPACING * 2) / appState.zoom.value; + const dashedLinePadding = + (DEFAULT_TRANSFORM_HANDLE_SPACING * 2) / appState.zoom.value; context.fillStyle = oc.white; const [x1, y1, x2, y2] = getCommonBounds(selectedElements); const initialLineDash = context.getLineDash(); @@ -869,7 +868,7 @@ const _renderInteractiveScene = ({ let scrollBars; if (renderConfig.renderScrollbars) { scrollBars = getScrollBars( - elements, + elementsMap, normalizedWidth, normalizedHeight, appState, @@ -896,14 +895,15 @@ const _renderInteractiveScene = ({ return { scrollBars, atLeastOneVisibleElement: visibleElements.length > 0, - elements, + elementsMap, }; }; const _renderStaticScene = ({ canvas, rc, - elements, + elementsMap, + allElementsMap, visibleElements, scale, appState, @@ -964,7 +964,7 @@ const _renderStaticScene = ({ // Paint visible elements visibleElements - .filter((el) => !isIframeLikeOrItsLabel(el)) + .filter((el) => !isIframeLikeElement(el)) .forEach((element) => { try { const frameId = element.frameId || appState.frameToHighlight?.id; @@ -976,16 +976,32 @@ const _renderStaticScene = ({ ) { context.save(); - const frame = getTargetFrame(element, appState); + const frame = getTargetFrame(element, elementsMap, appState); // TODO do we need to check isElementInFrame here? - if (frame && isElementInFrame(element, elements, appState)) { + if (frame && isElementInFrame(element, elementsMap, appState)) { frameClip(frame, context, renderConfig, appState); } - renderElement(element, rc, context, renderConfig, appState); + renderElement( + element, + elementsMap, + allElementsMap, + rc, + context, + renderConfig, + appState, + ); context.restore(); } else { - renderElement(element, rc, context, renderConfig, appState); + renderElement( + element, + elementsMap, + allElementsMap, + rc, + context, + renderConfig, + appState, + ); } if (!isExporting) { renderLinkIcon(element, context, appState); @@ -997,21 +1013,39 @@ const _renderStaticScene = ({ // render embeddables on top visibleElements - .filter((el) => isIframeLikeOrItsLabel(el)) + .filter((el) => isIframeLikeElement(el)) .forEach((element) => { try { const render = () => { - renderElement(element, rc, context, renderConfig, appState); + renderElement( + element, + elementsMap, + allElementsMap, + rc, + context, + renderConfig, + appState, + ); if ( isIframeLikeElement(element) && (isExporting || - (isEmbeddableElement(element) && !element.validated)) && + (isEmbeddableElement(element) && + renderConfig.embedsValidationStatus.get(element.id) !== + true)) && element.width && element.height ) { const label = createPlaceholderEmbeddableLabel(element); - renderElement(label, rc, context, renderConfig, appState); + renderElement( + label, + elementsMap, + allElementsMap, + rc, + context, + renderConfig, + appState, + ); } if (!isExporting) { renderLinkIcon(element, context, appState); @@ -1029,9 +1063,9 @@ const _renderStaticScene = ({ ) { context.save(); - const frame = getTargetFrame(element, appState); + const frame = getTargetFrame(element, elementsMap, appState); - if (frame && isElementInFrame(element, elements, appState)) { + if (frame && isElementInFrame(element, elementsMap, appState)) { frameClip(frame, context, renderConfig, appState); } render(); @@ -1160,7 +1194,7 @@ const renderSelectionBorder = ( cy: number; activeEmbeddable: boolean; }, - padding = DEFAULT_SPACING * 2, + padding = DEFAULT_TRANSFORM_HANDLE_SPACING * 2, ) => { const { angle, @@ -1445,45 +1479,30 @@ const renderLinkIcon = ( // This should be only called for exporting purposes export const renderSceneToSvg = ( elements: readonly NonDeletedExcalidrawElement[], + elementsMap: RenderableElementsMap, rsvg: RoughSVG, svgRoot: SVGElement, files: BinaryFiles, - { - offsetX = 0, - offsetY = 0, - exportWithDarkMode, - renderEmbeddables, - frameRendering, - }: { - offsetX?: number; - offsetY?: number; - exportWithDarkMode: boolean; - renderEmbeddables: boolean; - frameRendering: AppState["frameRendering"]; - }, + renderConfig: SVGRenderConfig, ) => { if (!svgRoot) { return; } - const renderConfig = { - exportWithDarkMode, - renderEmbeddables, - frameRendering, - }; // render elements elements - .filter((el) => !isIframeLikeOrItsLabel(el)) + .filter((el) => !isIframeLikeElement(el)) .forEach((element) => { if (!element.isDeleted) { try { renderElementToSvg( element, + elementsMap, rsvg, svgRoot, files, - element.x + offsetX, - element.y + offsetY, + element.x + renderConfig.offsetX, + element.y + renderConfig.offsetY, renderConfig, ); } catch (error: any) { @@ -1500,11 +1519,12 @@ export const renderSceneToSvg = ( try { renderElementToSvg( element, + elementsMap, rsvg, svgRoot, files, - element.x + offsetX, - element.y + offsetY, + element.x + renderConfig.offsetX, + element.y + renderConfig.offsetY, renderConfig, ); } catch (error: any) { diff --git a/src/renderer/renderSnaps.ts b/packages/excalidraw/renderer/renderSnaps.ts similarity index 100% rename from src/renderer/renderSnaps.ts rename to packages/excalidraw/renderer/renderSnaps.ts diff --git a/src/renderer/roundRect.ts b/packages/excalidraw/renderer/roundRect.ts similarity index 100% rename from src/renderer/roundRect.ts rename to packages/excalidraw/renderer/roundRect.ts diff --git a/src/scene/Fonts.ts b/packages/excalidraw/scene/Fonts.ts similarity index 91% rename from src/scene/Fonts.ts rename to packages/excalidraw/scene/Fonts.ts index 05dddadc4..1a97c06e0 100644 --- a/src/scene/Fonts.ts +++ b/packages/excalidraw/scene/Fonts.ts @@ -1,5 +1,6 @@ import { isTextElement, refreshTextDimensions } from "../element"; import { newElementWith } from "../element/mutateElement"; +import { getContainerElement } from "../element/textElement"; import { isBoundToContainer } from "../element/typeChecks"; import { ExcalidrawElement, ExcalidrawTextElement } from "../element/types"; import { getFontString } from "../utils"; @@ -57,7 +58,13 @@ export class Fonts { ShapeCache.delete(element); didUpdate = true; return newElementWith(element, { - ...refreshTextDimensions(element), + ...refreshTextDimensions( + element, + getContainerElement( + element, + this.scene.getElementsMapIncludingDeleted(), + ), + ), }); } return element; diff --git a/src/scene/Renderer.ts b/packages/excalidraw/scene/Renderer.ts similarity index 73% rename from src/scene/Renderer.ts rename to packages/excalidraw/scene/Renderer.ts index 152224951..1593d6d2e 100644 --- a/src/scene/Renderer.ts +++ b/packages/excalidraw/scene/Renderer.ts @@ -1,10 +1,14 @@ import { isElementInViewport } from "../element/sizeHelpers"; import { isImageElement } from "../element/typeChecks"; -import { NonDeletedExcalidrawElement } from "../element/types"; +import { + NonDeletedElementsMap, + NonDeletedExcalidrawElement, +} from "../element/types"; import { cancelRender } from "../renderer/renderScene"; import { AppState } from "../types"; -import { memoize } from "../utils"; +import { memoize, toBrandedType } from "../utils"; import Scene from "./Scene"; +import { RenderableElementsMap } from "./types"; export class Renderer { private scene: Scene; @@ -15,7 +19,7 @@ export class Renderer { public getRenderableElements = (() => { const getVisibleCanvasElements = ({ - elements, + elementsMap, zoom, offsetLeft, offsetTop, @@ -24,7 +28,7 @@ export class Renderer { height, width, }: { - elements: readonly NonDeletedExcalidrawElement[]; + elementsMap: NonDeletedElementsMap; zoom: AppState["zoom"]; offsetLeft: AppState["offsetLeft"]; offsetTop: AppState["offsetTop"]; @@ -33,43 +37,55 @@ export class Renderer { height: AppState["height"]; width: AppState["width"]; }): readonly NonDeletedExcalidrawElement[] => { - return elements.filter((element) => - isElementInViewport(element, width, height, { - zoom, - offsetLeft, - offsetTop, - scrollX, - scrollY, - }), - ); + const visibleElements: NonDeletedExcalidrawElement[] = []; + for (const element of elementsMap.values()) { + if ( + isElementInViewport(element, width, height, { + zoom, + offsetLeft, + offsetTop, + scrollX, + scrollY, + }) + ) { + visibleElements.push(element); + } + } + return visibleElements; }; - const getCanvasElements = ({ - editingElement, + const getRenderableElements = ({ elements, + editingElement, pendingImageElementId, }: { elements: readonly NonDeletedExcalidrawElement[]; editingElement: AppState["editingElement"]; pendingImageElementId: AppState["pendingImageElementId"]; }) => { - return elements.filter((element) => { + const elementsMap = toBrandedType(new Map()); + + for (const element of elements) { if (isImageElement(element)) { if ( // => not placed on canvas yet (but in elements array) pendingImageElementId === element.id ) { - return false; + continue; } } + // we don't want to render text element that's being currently edited // (it's rendered on remote only) - return ( + if ( !editingElement || editingElement.type !== "text" || element.id !== editingElement.id - ); - }); + ) { + elementsMap.set(element.id, element); + } + } + return elementsMap; }; return memoize( @@ -100,14 +116,14 @@ export class Renderer { }) => { const elements = this.scene.getNonDeletedElements(); - const canvasElements = getCanvasElements({ + const elementsMap = getRenderableElements({ elements, editingElement, pendingImageElementId, }); const visibleElements = getVisibleCanvasElements({ - elements: canvasElements, + elementsMap, zoom, offsetLeft, offsetTop, @@ -117,7 +133,7 @@ export class Renderer { width, }); - return { canvasElements, visibleElements }; + return { elementsMap, visibleElements }; }, ); })(); diff --git a/src/scene/Scene.ts b/packages/excalidraw/scene/Scene.ts similarity index 84% rename from src/scene/Scene.ts rename to packages/excalidraw/scene/Scene.ts index 814638e7e..88c3d8996 100644 --- a/src/scene/Scene.ts +++ b/packages/excalidraw/scene/Scene.ts @@ -3,14 +3,18 @@ import { NonDeletedExcalidrawElement, NonDeleted, ExcalidrawFrameLikeElement, + ElementsMapOrArray, + SceneElementsMap, + NonDeletedSceneElementsMap, } from "../element/types"; -import { getNonDeletedElements, isNonDeletedElement } from "../element"; +import { isNonDeletedElement } from "../element"; import { LinearElementEditor } from "../element/linearElementEditor"; import { isFrameLikeElement } from "../element/typeChecks"; import { getSelectedElements } from "./selection"; import { AppState } from "../types"; import { Assert, SameType } from "../utility-types"; import { randomInteger } from "../random"; +import { toBrandedType } from "../utils"; type ElementIdKey = InstanceType["elementId"]; type ElementKey = ExcalidrawElement | ElementIdKey; @@ -20,6 +24,20 @@ type SceneStateCallbackRemover = () => void; type SelectionHash = string & { __brand: "selectionHash" }; +const getNonDeletedElements = ( + allElements: readonly T[], +) => { + const elementsMap = new Map() as NonDeletedSceneElementsMap; + const elements: T[] = []; + for (const element of allElements) { + if (!element.isDeleted) { + elements.push(element as NonDeleted); + elementsMap.set(element.id, element as NonDeletedExcalidrawElement); + } + } + return { elementsMap, elements }; +}; + const hashSelectionOpts = ( opts: Parameters["getSelectedElements"]>[0], ) => { @@ -102,11 +120,14 @@ class Scene { private callbacks: Set = new Set(); private nonDeletedElements: readonly NonDeletedExcalidrawElement[] = []; + private nonDeletedElementsMap = toBrandedType( + new Map(), + ); private elements: readonly ExcalidrawElement[] = []; private nonDeletedFramesLikes: readonly NonDeleted[] = []; private frames: readonly ExcalidrawFrameLikeElement[] = []; - private elementsMap = new Map(); + private elementsMap = toBrandedType(new Map()); private selectedElementsCache: { selectedElementIds: AppState["selectedElementIds"] | null; elements: readonly NonDeletedExcalidrawElement[] | null; @@ -118,6 +139,14 @@ class Scene { }; private versionNonce: number | undefined; + getElementsMapIncludingDeleted() { + return this.elementsMap; + } + + getNonDeletedElementsMap() { + return this.nonDeletedElementsMap; + } + getElementsIncludingDeleted() { return this.elements; } @@ -138,7 +167,7 @@ class Scene { * scene state. This in effect will likely result in cache-miss, and * the cache won't be updated in this case. */ - elements?: readonly ExcalidrawElement[]; + elements?: ElementsMapOrArray; // selection-related options includeBoundTextElement?: boolean; includeElementsInFrames?: boolean; @@ -227,23 +256,27 @@ class Scene { return didChange; } - replaceAllElements( - nextElements: readonly ExcalidrawElement[], - mapElementIds = true, - ) { - this.elements = nextElements; + replaceAllElements(nextElements: ElementsMapOrArray, mapElementIds = true) { + this.elements = + // ts doesn't like `Array.isArray` of `instanceof Map` + nextElements instanceof Array + ? nextElements + : Array.from(nextElements.values()); const nextFrameLikes: ExcalidrawFrameLikeElement[] = []; this.elementsMap.clear(); - nextElements.forEach((element) => { + this.elements.forEach((element) => { if (isFrameLikeElement(element)) { nextFrameLikes.push(element); } this.elementsMap.set(element.id, element); - Scene.mapElementToScene(element, this); + Scene.mapElementToScene(element, this, mapElementIds); }); - this.nonDeletedElements = getNonDeletedElements(this.elements); + const nonDeletedElements = getNonDeletedElements(this.elements); + this.nonDeletedElements = nonDeletedElements.elements; + this.nonDeletedElementsMap = nonDeletedElements.elementsMap; + this.frames = nextFrameLikes; - this.nonDeletedFramesLikes = getNonDeletedElements(this.frames); + this.nonDeletedFramesLikes = getNonDeletedElements(this.frames).elements; this.informMutation(); } @@ -332,6 +365,22 @@ class Scene { getElementIndex(elementId: string) { return this.elements.findIndex((element) => element.id === elementId); } + + getContainerElement = ( + element: + | (ExcalidrawElement & { + containerId: ExcalidrawElement["id"] | null; + }) + | null, + ) => { + if (!element) { + return null; + } + if (element.containerId) { + return this.getElement(element.containerId) || null; + } + return null; + }; } export default Scene; diff --git a/src/scene/Shape.ts b/packages/excalidraw/scene/Shape.ts similarity index 72% rename from src/scene/Shape.ts rename to packages/excalidraw/scene/Shape.ts index 4d928e949..1d43aef71 100644 --- a/src/scene/Shape.ts +++ b/packages/excalidraw/scene/Shape.ts @@ -21,6 +21,7 @@ import { isLinearElement, } from "../element/typeChecks"; import { canChangeRoundness } from "./comparisons"; +import { EmbedsValidationStatus } from "../types"; const getDashArrayDashed = (strokeWidth: number) => [8, 8 + strokeWidth]; @@ -118,10 +119,13 @@ export const generateRoughOptions = ( const modifyIframeLikeForRoughOptions = ( element: NonDeletedExcalidrawElement, isExporting: boolean, + embedsValidationStatus: EmbedsValidationStatus | null, ) => { if ( isIframeLikeElement(element) && - (isExporting || (isEmbeddableElement(element) && !element.validated)) && + (isExporting || + (isEmbeddableElement(element) && + embedsValidationStatus?.get(element.id) !== true)) && isTransparent(element.backgroundColor) && isTransparent(element.strokeColor) ) { @@ -145,6 +149,126 @@ const modifyIframeLikeForRoughOptions = ( return element; }; +const getArrowheadShapes = ( + element: ExcalidrawLinearElement, + shape: Drawable[], + position: "start" | "end", + arrowhead: Arrowhead, + generator: RoughGenerator, + options: Options, + canvasBackgroundColor: string, +) => { + const arrowheadPoints = getArrowheadPoints( + element, + shape, + position, + arrowhead, + ); + + if (arrowheadPoints === null) { + return []; + } + + switch (arrowhead) { + case "dot": + case "circle": + case "circle_outline": { + const [x, y, diameter] = arrowheadPoints; + + // always use solid stroke for arrowhead + delete options.strokeLineDash; + + return [ + generator.circle(x, y, diameter, { + ...options, + fill: + arrowhead === "circle_outline" + ? canvasBackgroundColor + : element.strokeColor, + + fillStyle: "solid", + stroke: element.strokeColor, + roughness: Math.min(0.5, options.roughness || 0), + }), + ]; + } + case "triangle": + case "triangle_outline": { + const [x, y, x2, y2, x3, y3] = arrowheadPoints; + + // always use solid stroke for arrowhead + delete options.strokeLineDash; + + return [ + generator.polygon( + [ + [x, y], + [x2, y2], + [x3, y3], + [x, y], + ], + { + ...options, + fill: + arrowhead === "triangle_outline" + ? canvasBackgroundColor + : element.strokeColor, + fillStyle: "solid", + roughness: Math.min(1, options.roughness || 0), + }, + ), + ]; + } + case "diamond": + case "diamond_outline": { + const [x, y, x2, y2, x3, y3, x4, y4] = arrowheadPoints; + + // always use solid stroke for arrowhead + delete options.strokeLineDash; + + return [ + generator.polygon( + [ + [x, y], + [x2, y2], + [x3, y3], + [x4, y4], + [x, y], + ], + { + ...options, + fill: + arrowhead === "diamond_outline" + ? canvasBackgroundColor + : element.strokeColor, + fillStyle: "solid", + roughness: Math.min(1, options.roughness || 0), + }, + ), + ]; + } + case "bar": + case "arrow": + default: { + const [x2, y2, x3, y3, x4, y4] = arrowheadPoints; + + if (element.strokeStyle === "dotted") { + // for dotted arrows caps, reduce gap to make it more legible + const dash = getDashArrayDotted(element.strokeWidth - 1); + options.strokeLineDash = [dash[0], dash[1] - 1]; + } else { + // for solid/dashed, keep solid arrow cap + delete options.strokeLineDash; + } + options.roughness = Math.min(1, options.roughness || 0); + return [ + generator.line(x3, y3, x2, y2, options), + generator.line(x4, y4, x2, y2, options), + ]; + } + } +}; + /** * Generates the roughjs shape for given element. * @@ -155,7 +279,15 @@ const modifyIframeLikeForRoughOptions = ( export const _generateElementShape = ( element: Exclude, generator: RoughGenerator, - isExporting: boolean = false, + { + isExporting, + canvasBackgroundColor, + embedsValidationStatus, + }: { + isExporting: boolean; + canvasBackgroundColor: string; + embedsValidationStatus: EmbedsValidationStatus | null; + }, ): Drawable | Drawable[] | null => { switch (element.type) { case "rectangle": @@ -176,7 +308,11 @@ export const _generateElementShape = ( h - r } L 0 ${r} Q 0 0, ${r} 0`, generateRoughOptions( - modifyIframeLikeForRoughOptions(element, isExporting), + modifyIframeLikeForRoughOptions( + element, + isExporting, + embedsValidationStatus, + ), true, ), ); @@ -187,7 +323,11 @@ export const _generateElementShape = ( element.width, element.height, generateRoughOptions( - modifyIframeLikeForRoughOptions(element, isExporting), + modifyIframeLikeForRoughOptions( + element, + isExporting, + embedsValidationStatus, + ), false, ), ); @@ -276,83 +416,15 @@ export const _generateElementShape = ( if (element.type === "arrow") { const { startArrowhead = null, endArrowhead = "arrow" } = element; - const getArrowheadShapes = ( - element: ExcalidrawLinearElement, - shape: Drawable[], - position: "start" | "end", - arrowhead: Arrowhead, - ) => { - const arrowheadPoints = getArrowheadPoints( - element, - shape, - position, - arrowhead, - ); - - if (arrowheadPoints === null) { - return []; - } - - // Other arrowheads here... - if (arrowhead === "dot") { - const [x, y, r] = arrowheadPoints; - - return [ - generator.circle(x, y, r, { - ...options, - fill: element.strokeColor, - fillStyle: "solid", - stroke: "none", - }), - ]; - } - - if (arrowhead === "triangle") { - const [x, y, x2, y2, x3, y3] = arrowheadPoints; - - // always use solid stroke for triangle arrowhead - delete options.strokeLineDash; - - return [ - generator.polygon( - [ - [x, y], - [x2, y2], - [x3, y3], - [x, y], - ], - { - ...options, - fill: element.strokeColor, - fillStyle: "solid", - }, - ), - ]; - } - - // Arrow arrowheads - const [x2, y2, x3, y3, x4, y4] = arrowheadPoints; - - if (element.strokeStyle === "dotted") { - // for dotted arrows caps, reduce gap to make it more legible - const dash = getDashArrayDotted(element.strokeWidth - 1); - options.strokeLineDash = [dash[0], dash[1] - 1]; - } else { - // for solid/dashed, keep solid arrow cap - delete options.strokeLineDash; - } - return [ - generator.line(x3, y3, x2, y2, options), - generator.line(x4, y4, x2, y2, options), - ]; - }; - if (startArrowhead !== null) { const shapes = getArrowheadShapes( element, shape, "start", startArrowhead, + generator, + options, + canvasBackgroundColor, ); shape.push(...shapes); } @@ -367,6 +439,9 @@ export const _generateElementShape = ( shape, "end", endArrowhead, + generator, + options, + canvasBackgroundColor, ); shape.push(...shapes); } diff --git a/src/scene/ShapeCache.ts b/packages/excalidraw/scene/ShapeCache.ts similarity index 79% rename from src/scene/ShapeCache.ts rename to packages/excalidraw/scene/ShapeCache.ts index ded1b88fa..3bca88e85 100644 --- a/src/scene/ShapeCache.ts +++ b/packages/excalidraw/scene/ShapeCache.ts @@ -7,6 +7,8 @@ import { import { elementWithCanvasCache } from "../renderer/renderElement"; import { _generateElementShape } from "./Shape"; import { ElementShape, ElementShapes } from "./types"; +import { COLOR_PALETTE } from "../colors"; +import { AppState, EmbedsValidationStatus } from "../types"; export class ShapeCache { private static rg = new RoughGenerator(); @@ -46,10 +48,16 @@ export class ShapeCache { T extends Exclude, >( element: T, - isExporting = false, + renderConfig: { + isExporting: boolean; + canvasBackgroundColor: AppState["viewBackgroundColor"]; + embedsValidationStatus: EmbedsValidationStatus; + } | null, ) => { // when exporting, always regenerated to guarantee the latest shape - const cachedShape = isExporting ? undefined : ShapeCache.get(element); + const cachedShape = renderConfig?.isExporting + ? undefined + : ShapeCache.get(element); // `null` indicates no rc shape applicable for this element type, // but it's considered a valid cache value (= do not regenerate) @@ -62,7 +70,11 @@ export class ShapeCache { const shape = _generateElementShape( element, ShapeCache.rg, - isExporting, + renderConfig || { + isExporting: false, + canvasBackgroundColor: COLOR_PALETTE.white, + embedsValidationStatus: null, + }, ) as T["type"] extends keyof ElementShapes ? ElementShapes[T["type"]] : Drawable | null; diff --git a/src/scene/comparisons.ts b/packages/excalidraw/scene/comparisons.ts similarity index 98% rename from src/scene/comparisons.ts rename to packages/excalidraw/scene/comparisons.ts index 551aa2e6e..cb14d5810 100644 --- a/src/scene/comparisons.ts +++ b/packages/excalidraw/scene/comparisons.ts @@ -42,7 +42,8 @@ export const canChangeRoundness = (type: ElementOrToolType) => type === "embeddable" || type === "arrow" || type === "line" || - type === "diamond"; + type === "diamond" || + type === "image"; export const canHaveArrowheads = (type: ElementOrToolType) => type === "arrow"; diff --git a/src/scene/export.ts b/packages/excalidraw/scene/export.ts similarity index 90% rename from src/scene/export.ts rename to packages/excalidraw/scene/export.ts index 54ede380c..d463e2597 100644 --- a/src/scene/export.ts +++ b/packages/excalidraw/scene/export.ts @@ -4,6 +4,7 @@ import { ExcalidrawFrameLikeElement, ExcalidrawTextElement, NonDeletedExcalidrawElement, + NonDeletedSceneElementsMap, } from "../element/types"; import { Bounds, @@ -11,7 +12,13 @@ import { getElementAbsoluteCoords, } from "../element/bounds"; import { renderSceneToSvg, renderStaticScene } from "../renderer/renderScene"; -import { cloneJSON, distance, getFontString } from "../utils"; +import { + arrayToMap, + cloneJSON, + distance, + getFontString, + toBrandedType, +} from "../utils"; import { AppState, BinaryFiles } from "../types"; import { DEFAULT_EXPORT_PADDING, @@ -26,8 +33,8 @@ import { getInitializedImageElements, updateImageCache, } from "../element/image"; -import { elementsOverlappingBBox } from "../packages/withinBounds"; import { + getElementsOverlappingFrame, getFrameLikeElements, getFrameLikeTitle, getRootElements, @@ -37,6 +44,7 @@ import { Mutable } from "../utility-types"; import { newElementWith } from "../element/mutateElement"; import Scene from "./Scene"; import { isFrameElement, isFrameLikeElement } from "../element/typeChecks"; +import { RenderableElementsMap } from "./types"; const SVG_EXPORT_TAG = ``; @@ -168,11 +176,7 @@ const prepareElementsForRender = ({ let nextElements: readonly ExcalidrawElement[]; if (exportingFrame) { - nextElements = elementsOverlappingBBox({ - elements, - bounds: exportingFrame, - type: "overlap", - }); + nextElements = getElementsOverlappingFrame(elements, exportingFrame); } else if (frameRendering.enabled && frameRendering.name) { nextElements = addFrameLabelsAsTextElements(elements, { exportWithDarkMode, @@ -248,7 +252,12 @@ export const exportToCanvas = async ( renderStaticScene({ canvas, rc: rough.canvas(canvas), - elements: elementsForRender, + elementsMap: toBrandedType( + arrayToMap(elementsForRender), + ), + allElementsMap: toBrandedType( + arrayToMap(elements), + ), visibleElements: elementsForRender, scale, appState: { @@ -262,9 +271,13 @@ export const exportToCanvas = async ( theme: appState.exportWithDarkMode ? "dark" : "light", }, renderConfig: { + canvasBackgroundColor: viewBackgroundColor, imageCache, renderGrid: false, isExporting: true, + // empty disables embeddable rendering + embedsValidationStatus: new Map(), + elementsPendingErasure: new Set(), }, }); @@ -286,6 +299,9 @@ export const exportToSvg = async ( }, files: BinaryFiles | null, opts?: { + /** + * if true, all embeddables passed in will be rendered when possible. + */ renderEmbeddables?: boolean; exportingFrame?: ExcalidrawFrameLikeElement | null; }, @@ -326,7 +342,7 @@ export const exportToSvg = async ( if (exportEmbedScene) { try { metadata = await ( - await import(/* webpackChunkName: "image" */ "../../src/data/image") + await import("../data/image") ).encodeSvgMetadata({ // when embedding scene, we want to embed the origionally supplied // elements which don't contain the temp frame labels. @@ -426,13 +442,32 @@ export const exportToSvg = async ( } const rsvg = rough.svg(svgRoot); - renderSceneToSvg(elementsForRender, rsvg, svgRoot, files || {}, { - offsetX, - offsetY, - exportWithDarkMode, - renderEmbeddables: opts?.renderEmbeddables ?? false, - frameRendering, - }); + + const renderEmbeddables = opts?.renderEmbeddables ?? false; + + renderSceneToSvg( + elementsForRender, + toBrandedType(arrayToMap(elementsForRender)), + rsvg, + svgRoot, + files || {}, + { + offsetX, + offsetY, + isExporting: true, + exportWithDarkMode, + renderEmbeddables, + frameRendering, + canvasBackgroundColor: viewBackgroundColor, + embedsValidationStatus: renderEmbeddables + ? new Map( + elementsForRender + .filter((element) => isFrameLikeElement(element)) + .map((element) => [element.id, true]), + ) + : new Map(), + }, + ); tempScene.destroy(); diff --git a/src/scene/index.ts b/packages/excalidraw/scene/index.ts similarity index 90% rename from src/scene/index.ts rename to packages/excalidraw/scene/index.ts index 5a7b9028a..33399d79e 100644 --- a/src/scene/index.ts +++ b/packages/excalidraw/scene/index.ts @@ -1,4 +1,3 @@ -export { isOverScrollBars } from "./scrollbars"; export { isSomeElementSelected, getElementsWithinSelection, diff --git a/src/scene/scroll.ts b/packages/excalidraw/scene/scroll.ts similarity index 100% rename from src/scene/scroll.ts rename to packages/excalidraw/scene/scroll.ts diff --git a/src/scene/scrollbars.ts b/packages/excalidraw/scene/scrollbars.ts similarity index 96% rename from src/scene/scrollbars.ts rename to packages/excalidraw/scene/scrollbars.ts index 1d93f688f..14009588b 100644 --- a/src/scene/scrollbars.ts +++ b/packages/excalidraw/scene/scrollbars.ts @@ -1,7 +1,6 @@ -import { ExcalidrawElement } from "../element/types"; import { getCommonBounds } from "../element"; import { InteractiveCanvasAppState } from "../types"; -import { ScrollBars } from "./types"; +import { RenderableElementsMap, ScrollBars } from "./types"; import { getGlobalCSSVariable } from "../utils"; import { getLanguage } from "../i18n"; @@ -10,12 +9,12 @@ export const SCROLLBAR_WIDTH = 6; export const SCROLLBAR_COLOR = "rgba(0,0,0,0.3)"; export const getScrollBars = ( - elements: readonly ExcalidrawElement[], + elements: RenderableElementsMap, viewportWidth: number, viewportHeight: number, appState: InteractiveCanvasAppState, ): ScrollBars => { - if (elements.length === 0) { + if (!elements.size) { return { horizontal: null, vertical: null, diff --git a/src/scene/selection.test.ts b/packages/excalidraw/scene/selection.test.ts similarity index 100% rename from src/scene/selection.test.ts rename to packages/excalidraw/scene/selection.test.ts diff --git a/src/scene/selection.ts b/packages/excalidraw/scene/selection.ts similarity index 95% rename from src/scene/selection.ts rename to packages/excalidraw/scene/selection.ts index 7a620155f..ae021f6aa 100644 --- a/src/scene/selection.ts +++ b/packages/excalidraw/scene/selection.ts @@ -1,4 +1,5 @@ import { + ElementsMapOrArray, ExcalidrawElement, NonDeletedExcalidrawElement, } from "../element/types"; @@ -166,26 +167,28 @@ export const getCommonAttributeOfSelectedElements = ( }; export const getSelectedElements = ( - elements: readonly NonDeletedExcalidrawElement[], + elements: ElementsMapOrArray, appState: Pick, opts?: { includeBoundTextElement?: boolean; includeElementsInFrames?: boolean; }, ) => { - const selectedElements = elements.filter((element) => { + const selectedElements: ExcalidrawElement[] = []; + for (const element of elements.values()) { if (appState.selectedElementIds[element.id]) { - return element; + selectedElements.push(element); + continue; } if ( opts?.includeBoundTextElement && isBoundToContainer(element) && appState.selectedElementIds[element?.containerId] ) { - return element; + selectedElements.push(element); + continue; } - return null; - }); + } if (opts?.includeElementsInFrames) { const elementsToInclude: ExcalidrawElement[] = []; @@ -205,7 +208,7 @@ export const getSelectedElements = ( }; export const getTargetElements = ( - elements: readonly NonDeletedExcalidrawElement[], + elements: ElementsMapOrArray, appState: Pick, ) => appState.editingElement diff --git a/src/scene/types.ts b/packages/excalidraw/scene/types.ts similarity index 76% rename from src/scene/types.ts rename to packages/excalidraw/scene/types.ts index dc709a22a..02aa3b7bf 100644 --- a/src/scene/types.ts +++ b/packages/excalidraw/scene/types.ts @@ -2,15 +2,25 @@ import type { RoughCanvas } from "roughjs/bin/canvas"; import { Drawable } from "roughjs/bin/core"; import { ExcalidrawTextElement, + NonDeletedElementsMap, NonDeletedExcalidrawElement, + NonDeletedSceneElementsMap, } from "../element/types"; import { AppClassProperties, + AppState, + EmbedsValidationStatus, + ElementsPendingErasure, InteractiveCanvasAppState, StaticCanvasAppState, } from "../types"; +import { MakeBrand } from "../utility-types"; + +export type RenderableElementsMap = NonDeletedElementsMap & + MakeBrand<"RenderableElementsMap">; export type StaticCanvasRenderConfig = { + canvasBackgroundColor: AppState["viewBackgroundColor"]; // extra options passed to the renderer // --------------------------------------------------------------------------- imageCache: AppClassProperties["imageCache"]; @@ -18,6 +28,19 @@ export type StaticCanvasRenderConfig = { /** when exporting the behavior is slightly different (e.g. we can't use CSS filters), and we disable render optimizations for best output */ isExporting: boolean; + embedsValidationStatus: EmbedsValidationStatus; + elementsPendingErasure: ElementsPendingErasure; +}; + +export type SVGRenderConfig = { + offsetX: number; + offsetY: number; + isExporting: boolean; + exportWithDarkMode: boolean; + renderEmbeddables: boolean; + frameRendering: AppState["frameRendering"]; + canvasBackgroundColor: AppState["viewBackgroundColor"]; + embedsValidationStatus: EmbedsValidationStatus; }; export type InteractiveCanvasRenderConfig = { @@ -36,14 +59,15 @@ export type InteractiveCanvasRenderConfig = { export type RenderInteractiveSceneCallback = { atLeastOneVisibleElement: boolean; - elements: readonly NonDeletedExcalidrawElement[]; + elementsMap: RenderableElementsMap; scrollBars?: ScrollBars; }; export type StaticSceneRenderConfig = { canvas: HTMLCanvasElement; rc: RoughCanvas; - elements: readonly NonDeletedExcalidrawElement[]; + elementsMap: RenderableElementsMap; + allElementsMap: NonDeletedSceneElementsMap; visibleElements: readonly NonDeletedExcalidrawElement[]; scale: number; appState: StaticCanvasAppState; @@ -52,7 +76,7 @@ export type StaticSceneRenderConfig = { export type InteractiveSceneRenderConfig = { canvas: HTMLCanvasElement | null; - elements: readonly NonDeletedExcalidrawElement[]; + elementsMap: RenderableElementsMap; visibleElements: readonly NonDeletedExcalidrawElement[]; selectedElements: readonly NonDeletedExcalidrawElement[]; scale: number; diff --git a/src/scene/zoom.ts b/packages/excalidraw/scene/zoom.ts similarity index 100% rename from src/scene/zoom.ts rename to packages/excalidraw/scene/zoom.ts diff --git a/src/shapes.tsx b/packages/excalidraw/shapes.tsx similarity index 100% rename from src/shapes.tsx rename to packages/excalidraw/shapes.tsx diff --git a/src/snapping.ts b/packages/excalidraw/snapping.ts similarity index 99% rename from src/snapping.ts rename to packages/excalidraw/snapping.ts index e7ff9b787..7557145ae 100644 --- a/src/snapping.ts +++ b/packages/excalidraw/snapping.ts @@ -16,6 +16,7 @@ import { KEYS } from "./keys"; import { rangeIntersection, rangesOverlap, rotatePoint } from "./math"; import { getVisibleAndNonSelectedElements } from "./scene/selection"; import { AppState, KeyboardModifiersObject, Point } from "./types"; +import { arrayToMap } from "./utils"; const SNAP_DISTANCE = 8; @@ -286,7 +287,10 @@ export const getVisibleGaps = ( appState, ); - const referenceBounds = getMaximumGroups(referenceElements) + const referenceBounds = getMaximumGroups( + referenceElements, + arrayToMap(elements), + ) .filter( (elementsGroup) => !(elementsGroup.length === 1 && isBoundToContainer(elementsGroup[0])), @@ -572,7 +576,7 @@ export const getReferenceSnapPoints = ( appState, ); - return getMaximumGroups(referenceElements) + return getMaximumGroups(referenceElements, arrayToMap(elements)) .filter( (elementsGroup) => !(elementsGroup.length === 1 && isBoundToContainer(elementsGroup[0])), diff --git a/src/tests/App.test.tsx b/packages/excalidraw/tests/App.test.tsx similarity index 95% rename from src/tests/App.test.tsx rename to packages/excalidraw/tests/App.test.tsx index af576dcb0..316d274ef 100644 --- a/src/tests/App.test.tsx +++ b/packages/excalidraw/tests/App.test.tsx @@ -3,7 +3,7 @@ import * as Renderer from "../renderer/renderScene"; import { reseed } from "../random"; import { render, queryByTestId } from "../tests/test-utils"; -import { Excalidraw } from "../packages/excalidraw/index"; +import { Excalidraw } from "../index"; import { vi } from "vitest"; const renderStaticScene = vi.spyOn(Renderer, "renderStaticScene"); diff --git a/src/tests/MermaidToExcalidraw.test.tsx b/packages/excalidraw/tests/MermaidToExcalidraw.test.tsx similarity index 98% rename from src/tests/MermaidToExcalidraw.test.tsx rename to packages/excalidraw/tests/MermaidToExcalidraw.test.tsx index e12df4b24..21946bab1 100644 --- a/src/tests/MermaidToExcalidraw.test.tsx +++ b/packages/excalidraw/tests/MermaidToExcalidraw.test.tsx @@ -1,5 +1,5 @@ import { act, fireEvent, render, waitFor } from "./test-utils"; -import { Excalidraw } from "../packages/excalidraw/index"; +import { Excalidraw } from "../index"; import React from "react"; import { expect, vi } from "vitest"; import * as MermaidToExcalidraw from "@excalidraw/mermaid-to-excalidraw"; diff --git a/src/tests/__snapshots__/App.test.tsx.snap b/packages/excalidraw/tests/__snapshots__/App.test.tsx.snap similarity index 100% rename from src/tests/__snapshots__/App.test.tsx.snap rename to packages/excalidraw/tests/__snapshots__/App.test.tsx.snap diff --git a/packages/excalidraw/tests/__snapshots__/MermaidToExcalidraw.test.tsx.snap b/packages/excalidraw/tests/__snapshots__/MermaidToExcalidraw.test.tsx.snap new file mode 100644 index 000000000..0e25dc33d --- /dev/null +++ b/packages/excalidraw/tests/__snapshots__/MermaidToExcalidraw.test.tsx.snap @@ -0,0 +1,10 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`Test > should open mermaid popup when active tool is mermaid 1`] = ` +"" +`; diff --git a/src/tests/__snapshots__/MobileMenu.test.tsx.snap b/packages/excalidraw/tests/__snapshots__/MobileMenu.test.tsx.snap similarity index 100% rename from src/tests/__snapshots__/MobileMenu.test.tsx.snap rename to packages/excalidraw/tests/__snapshots__/MobileMenu.test.tsx.snap diff --git a/src/tests/__snapshots__/charts.test.tsx.snap b/packages/excalidraw/tests/__snapshots__/charts.test.tsx.snap similarity index 100% rename from src/tests/__snapshots__/charts.test.tsx.snap rename to packages/excalidraw/tests/__snapshots__/charts.test.tsx.snap diff --git a/src/tests/__snapshots__/contextmenu.test.tsx.snap b/packages/excalidraw/tests/__snapshots__/contextmenu.test.tsx.snap similarity index 95% rename from src/tests/__snapshots__/contextmenu.test.tsx.snap rename to packages/excalidraw/tests/__snapshots__/contextmenu.test.tsx.snap index d8ef5572a..682af4bfe 100644 --- a/src/tests/__snapshots__/contextmenu.test.tsx.snap +++ b/packages/excalidraw/tests/__snapshots__/contextmenu.test.tsx.snap @@ -312,6 +312,7 @@ exports[`contextMenu element > right-clicking on a group should select whole gro "exportScale": 1, "exportWithDarkMode": false, "fileHandle": null, + "followedBy": Set {}, "frameRendering": { "clip": true, "enabled": true, @@ -370,6 +371,7 @@ exports[`contextMenu element > right-clicking on a group should select whole gro "suggestedBindings": [], "theme": "light", "toast": null, + "userToFollow": null, "viewBackgroundColor": "#ffffff", "viewModeEnabled": false, "width": 200, @@ -385,6 +387,7 @@ exports[`contextMenu element > right-clicking on a group should select whole gro "angle": 0, "backgroundColor": "red", "boundElements": null, + "customData": undefined, "fillStyle": "solid", "frameId": null, "groupIds": [ @@ -419,6 +422,7 @@ exports[`contextMenu element > right-clicking on a group should select whole gro "angle": 0, "backgroundColor": "red", "boundElements": null, + "customData": undefined, "fillStyle": "solid", "frameId": null, "groupIds": [ @@ -511,6 +515,7 @@ exports[`contextMenu element > selecting 'Add to library' in context menu adds e "exportScale": 1, "exportWithDarkMode": false, "fileHandle": null, + "followedBy": Set {}, "frameRendering": { "clip": true, "enabled": true, @@ -565,6 +570,7 @@ exports[`contextMenu element > selecting 'Add to library' in context menu adds e "toast": { "message": "Added to library", }, + "userToFollow": null, "viewBackgroundColor": "#ffffff", "viewModeEnabled": false, "width": 200, @@ -580,6 +586,7 @@ exports[`contextMenu element > selecting 'Add to library' in context menu adds e "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "customData": undefined, "fillStyle": "solid", "frameId": null, "groupIds": [], @@ -600,7 +607,7 @@ exports[`contextMenu element > selecting 'Add to library' in context menu adds e "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 401146281, + "versionNonce": 2019559783, "width": 20, "x": -10, "y": 0, @@ -639,6 +646,7 @@ exports[`contextMenu element > selecting 'Add to library' in context menu adds e "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "customData": undefined, "fillStyle": "solid", "frameId": null, "groupIds": [], @@ -659,7 +667,7 @@ exports[`contextMenu element > selecting 'Add to library' in context menu adds e "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 401146281, + "versionNonce": 2019559783, "width": 20, "x": -10, "y": 0, @@ -713,6 +721,7 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings "exportScale": 1, "exportWithDarkMode": false, "fileHandle": null, + "followedBy": Set {}, "frameRendering": { "clip": true, "enabled": true, @@ -765,6 +774,7 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings "suggestedBindings": [], "theme": "light", "toast": null, + "userToFollow": null, "viewBackgroundColor": "#ffffff", "viewModeEnabled": false, "width": 200, @@ -780,6 +790,7 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "customData": undefined, "fillStyle": "solid", "frameId": null, "groupIds": [], @@ -793,14 +804,14 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings "roundness": { "type": 3, }, - "seed": 1150084233, + "seed": 1116226695, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 1014066025, + "versionNonce": 238820263, "width": 20, "x": 20, "y": 30, @@ -812,6 +823,7 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "customData": undefined, "fillStyle": "solid", "frameId": null, "groupIds": [], @@ -832,7 +844,7 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings "type": "rectangle", "updated": 1, "version": 3, - "versionNonce": 400692809, + "versionNonce": 1604849351, "width": 20, "x": -10, "y": 0, @@ -871,6 +883,7 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "customData": undefined, "fillStyle": "solid", "frameId": null, "groupIds": [], @@ -891,7 +904,7 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 401146281, + "versionNonce": 2019559783, "width": 20, "x": -10, "y": 0, @@ -914,6 +927,7 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "customData": undefined, "fillStyle": "solid", "frameId": null, "groupIds": [], @@ -934,7 +948,7 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 401146281, + "versionNonce": 2019559783, "width": 20, "x": -10, "y": 0, @@ -943,6 +957,7 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "customData": undefined, "fillStyle": "solid", "frameId": null, "groupIds": [], @@ -956,14 +971,14 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings "roundness": { "type": 3, }, - "seed": 1150084233, + "seed": 1116226695, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 1014066025, + "versionNonce": 238820263, "width": 20, "x": 20, "y": 30, @@ -986,6 +1001,7 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "customData": undefined, "fillStyle": "solid", "frameId": null, "groupIds": [], @@ -999,14 +1015,14 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings "roundness": { "type": 3, }, - "seed": 1150084233, + "seed": 1116226695, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 1014066025, + "versionNonce": 238820263, "width": 20, "x": 20, "y": 30, @@ -1015,6 +1031,7 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "customData": undefined, "fillStyle": "solid", "frameId": null, "groupIds": [], @@ -1035,7 +1052,7 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings "type": "rectangle", "updated": 1, "version": 3, - "versionNonce": 400692809, + "versionNonce": 1604849351, "width": 20, "x": -10, "y": 0, @@ -1089,6 +1106,7 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings "exportScale": 1, "exportWithDarkMode": false, "fileHandle": null, + "followedBy": Set {}, "frameRendering": { "clip": true, "enabled": true, @@ -1141,6 +1159,7 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings "suggestedBindings": [], "theme": "light", "toast": null, + "userToFollow": null, "viewBackgroundColor": "#ffffff", "viewModeEnabled": false, "width": 200, @@ -1156,6 +1175,7 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "customData": undefined, "fillStyle": "solid", "frameId": null, "groupIds": [], @@ -1169,14 +1189,14 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings "roundness": { "type": 3, }, - "seed": 1150084233, + "seed": 1116226695, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 1014066025, + "versionNonce": 238820263, "width": 20, "x": 20, "y": 30, @@ -1188,6 +1208,7 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "customData": undefined, "fillStyle": "solid", "frameId": null, "groupIds": [], @@ -1208,7 +1229,7 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings "type": "rectangle", "updated": 1, "version": 3, - "versionNonce": 400692809, + "versionNonce": 1604849351, "width": 20, "x": -10, "y": 0, @@ -1247,6 +1268,7 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "customData": undefined, "fillStyle": "solid", "frameId": null, "groupIds": [], @@ -1267,7 +1289,7 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 401146281, + "versionNonce": 2019559783, "width": 20, "x": -10, "y": 0, @@ -1290,6 +1312,7 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "customData": undefined, "fillStyle": "solid", "frameId": null, "groupIds": [], @@ -1310,7 +1333,7 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 401146281, + "versionNonce": 2019559783, "width": 20, "x": -10, "y": 0, @@ -1319,6 +1342,7 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "customData": undefined, "fillStyle": "solid", "frameId": null, "groupIds": [], @@ -1332,14 +1356,14 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings "roundness": { "type": 3, }, - "seed": 1150084233, + "seed": 1116226695, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 1014066025, + "versionNonce": 238820263, "width": 20, "x": 20, "y": 30, @@ -1362,6 +1386,7 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "customData": undefined, "fillStyle": "solid", "frameId": null, "groupIds": [], @@ -1375,14 +1400,14 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings "roundness": { "type": 3, }, - "seed": 1150084233, + "seed": 1116226695, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 1014066025, + "versionNonce": 238820263, "width": 20, "x": 20, "y": 30, @@ -1391,6 +1416,7 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "customData": undefined, "fillStyle": "solid", "frameId": null, "groupIds": [], @@ -1411,7 +1437,7 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings "type": "rectangle", "updated": 1, "version": 3, - "versionNonce": 400692809, + "versionNonce": 1604849351, "width": 20, "x": -10, "y": 0, @@ -1465,6 +1491,7 @@ exports[`contextMenu element > selecting 'Copy styles' in context menu copies st "exportScale": 1, "exportWithDarkMode": false, "fileHandle": null, + "followedBy": Set {}, "frameRendering": { "clip": true, "enabled": true, @@ -1519,6 +1546,7 @@ exports[`contextMenu element > selecting 'Copy styles' in context menu copies st "toast": { "message": "Copied styles.", }, + "userToFollow": null, "viewBackgroundColor": "#ffffff", "viewModeEnabled": false, "width": 200, @@ -1534,6 +1562,7 @@ exports[`contextMenu element > selecting 'Copy styles' in context menu copies st "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "customData": undefined, "fillStyle": "solid", "frameId": null, "groupIds": [], @@ -1547,7 +1576,7 @@ exports[`contextMenu element > selecting 'Copy styles' in context menu copies st "roundness": { "type": 3, }, - "seed": 453191, + "seed": 449462985, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -1593,6 +1622,7 @@ exports[`contextMenu element > selecting 'Copy styles' in context menu copies st "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "customData": undefined, "fillStyle": "solid", "frameId": null, "groupIds": [], @@ -1606,7 +1636,7 @@ exports[`contextMenu element > selecting 'Copy styles' in context menu copies st "roundness": { "type": 3, }, - "seed": 453191, + "seed": 449462985, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -1667,6 +1697,7 @@ exports[`contextMenu element > selecting 'Delete' in context menu deletes elemen "exportScale": 1, "exportWithDarkMode": false, "fileHandle": null, + "followedBy": Set {}, "frameRendering": { "clip": true, "enabled": true, @@ -1717,6 +1748,7 @@ exports[`contextMenu element > selecting 'Delete' in context menu deletes elemen "suggestedBindings": [], "theme": "light", "toast": null, + "userToFollow": null, "viewBackgroundColor": "#ffffff", "viewModeEnabled": false, "width": 200, @@ -1732,6 +1764,7 @@ exports[`contextMenu element > selecting 'Delete' in context menu deletes elemen "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "customData": undefined, "fillStyle": "solid", "frameId": null, "groupIds": [], @@ -1752,7 +1785,7 @@ exports[`contextMenu element > selecting 'Delete' in context menu deletes elemen "type": "rectangle", "updated": 1, "version": 3, - "versionNonce": 1150084233, + "versionNonce": 1116226695, "width": 20, "x": -10, "y": 0, @@ -1791,6 +1824,7 @@ exports[`contextMenu element > selecting 'Delete' in context menu deletes elemen "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "customData": undefined, "fillStyle": "solid", "frameId": null, "groupIds": [], @@ -1811,7 +1845,7 @@ exports[`contextMenu element > selecting 'Delete' in context menu deletes elemen "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 401146281, + "versionNonce": 2019559783, "width": 20, "x": -10, "y": 0, @@ -1832,6 +1866,7 @@ exports[`contextMenu element > selecting 'Delete' in context menu deletes elemen "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "customData": undefined, "fillStyle": "solid", "frameId": null, "groupIds": [], @@ -1852,7 +1887,7 @@ exports[`contextMenu element > selecting 'Delete' in context menu deletes elemen "type": "rectangle", "updated": 1, "version": 3, - "versionNonce": 1150084233, + "versionNonce": 1116226695, "width": 20, "x": -10, "y": 0, @@ -1906,6 +1941,7 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates "exportScale": 1, "exportWithDarkMode": false, "fileHandle": null, + "followedBy": Set {}, "frameRendering": { "clip": true, "enabled": true, @@ -1958,6 +1994,7 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates "suggestedBindings": [], "theme": "light", "toast": null, + "userToFollow": null, "viewBackgroundColor": "#ffffff", "viewModeEnabled": false, "width": 200, @@ -1973,6 +2010,7 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "customData": undefined, "fillStyle": "solid", "frameId": null, "groupIds": [], @@ -1993,7 +2031,7 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 401146281, + "versionNonce": 2019559783, "width": 20, "x": -10, "y": 0, @@ -2005,6 +2043,7 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "customData": undefined, "fillStyle": "solid", "frameId": null, "groupIds": [], @@ -2018,14 +2057,14 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates "roundness": { "type": 3, }, - "seed": 1150084233, + "seed": 1116226695, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 401146281, + "versionNonce": 2019559783, "width": 20, "x": 0, "y": 10, @@ -2064,6 +2103,7 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "customData": undefined, "fillStyle": "solid", "frameId": null, "groupIds": [], @@ -2084,7 +2124,7 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 401146281, + "versionNonce": 2019559783, "width": 20, "x": -10, "y": 0, @@ -2107,6 +2147,7 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "customData": undefined, "fillStyle": "solid", "frameId": null, "groupIds": [], @@ -2127,7 +2168,7 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 401146281, + "versionNonce": 2019559783, "width": 20, "x": -10, "y": 0, @@ -2136,6 +2177,7 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "customData": undefined, "fillStyle": "solid", "frameId": null, "groupIds": [], @@ -2149,14 +2191,14 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates "roundness": { "type": 3, }, - "seed": 1150084233, + "seed": 1116226695, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 401146281, + "versionNonce": 2019559783, "width": 20, "x": 0, "y": 10, @@ -2210,6 +2252,7 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group "exportScale": 1, "exportWithDarkMode": false, "fileHandle": null, + "followedBy": Set {}, "frameRendering": { "clip": true, "enabled": true, @@ -2267,6 +2310,7 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group "suggestedBindings": [], "theme": "light", "toast": null, + "userToFollow": null, "viewBackgroundColor": "#ffffff", "viewModeEnabled": false, "width": 200, @@ -2282,6 +2326,7 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "customData": undefined, "fillStyle": "solid", "frameId": null, "groupIds": [ @@ -2304,7 +2349,7 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group "type": "rectangle", "updated": 1, "version": 3, - "versionNonce": 1604849351, + "versionNonce": 1505387817, "width": 20, "x": -10, "y": 0, @@ -2316,6 +2361,7 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "customData": undefined, "fillStyle": "solid", "frameId": null, "groupIds": [ @@ -2331,14 +2377,14 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group "roundness": { "type": 3, }, - "seed": 1150084233, + "seed": 1116226695, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, "version": 3, - "versionNonce": 1505387817, + "versionNonce": 23633383, "width": 20, "x": 20, "y": 30, @@ -2377,6 +2423,7 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "customData": undefined, "fillStyle": "solid", "frameId": null, "groupIds": [], @@ -2397,7 +2444,7 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 401146281, + "versionNonce": 2019559783, "width": 20, "x": -10, "y": 0, @@ -2420,6 +2467,7 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "customData": undefined, "fillStyle": "solid", "frameId": null, "groupIds": [], @@ -2440,7 +2488,7 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 401146281, + "versionNonce": 2019559783, "width": 20, "x": -10, "y": 0, @@ -2449,6 +2497,7 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "customData": undefined, "fillStyle": "solid", "frameId": null, "groupIds": [], @@ -2462,14 +2511,14 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group "roundness": { "type": 3, }, - "seed": 1150084233, + "seed": 1116226695, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 1014066025, + "versionNonce": 238820263, "width": 20, "x": 20, "y": 30, @@ -2495,6 +2544,7 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "customData": undefined, "fillStyle": "solid", "frameId": null, "groupIds": [ @@ -2517,7 +2567,7 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group "type": "rectangle", "updated": 1, "version": 3, - "versionNonce": 1604849351, + "versionNonce": 1505387817, "width": 20, "x": -10, "y": 0, @@ -2526,6 +2576,7 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "customData": undefined, "fillStyle": "solid", "frameId": null, "groupIds": [ @@ -2541,14 +2592,14 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group "roundness": { "type": 3, }, - "seed": 1150084233, + "seed": 1116226695, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, "version": 3, - "versionNonce": 1505387817, + "versionNonce": 23633383, "width": 20, "x": 20, "y": 30, @@ -2602,6 +2653,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "exportScale": 1, "exportWithDarkMode": false, "fileHandle": null, + "followedBy": Set {}, "frameRendering": { "clip": true, "enabled": true, @@ -2656,6 +2708,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "toast": { "message": "Copied styles.", }, + "userToFollow": null, "viewBackgroundColor": "#ffffff", "viewModeEnabled": false, "width": 200, @@ -2671,6 +2724,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "angle": 0, "backgroundColor": "#a5d8ff", "boundElements": null, + "customData": undefined, "fillStyle": "cross-hatch", "frameId": null, "groupIds": [], @@ -2691,7 +2745,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "type": "rectangle", "updated": 1, "version": 3, - "versionNonce": 1898319239, + "versionNonce": 640725609, "width": 20, "x": -10, "y": 0, @@ -2703,6 +2757,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "angle": 0, "backgroundColor": "#a5d8ff", "boundElements": null, + "customData": undefined, "fillStyle": "cross-hatch", "frameId": null, "groupIds": [], @@ -2716,14 +2771,14 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "roundness": { "type": 3, }, - "seed": 1723083209, + "seed": 760410951, "strokeColor": "#e03131", "strokeStyle": "dotted", "strokeWidth": 2, "type": "rectangle", "updated": 1, "version": 8, - "versionNonce": 289600103, + "versionNonce": 1315507081, "width": 20, "x": 20, "y": 30, @@ -2762,6 +2817,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "customData": undefined, "fillStyle": "solid", "frameId": null, "groupIds": [], @@ -2782,7 +2838,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 401146281, + "versionNonce": 2019559783, "width": 20, "x": -10, "y": 0, @@ -2805,6 +2861,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "customData": undefined, "fillStyle": "solid", "frameId": null, "groupIds": [], @@ -2825,7 +2882,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 401146281, + "versionNonce": 2019559783, "width": 20, "x": -10, "y": 0, @@ -2834,6 +2891,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "customData": undefined, "fillStyle": "solid", "frameId": null, "groupIds": [], @@ -2847,14 +2905,14 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "roundness": { "type": 3, }, - "seed": 1150084233, + "seed": 1116226695, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 1014066025, + "versionNonce": 238820263, "width": 20, "x": 20, "y": 30, @@ -2877,6 +2935,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "customData": undefined, "fillStyle": "solid", "frameId": null, "groupIds": [], @@ -2897,7 +2956,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 401146281, + "versionNonce": 2019559783, "width": 20, "x": -10, "y": 0, @@ -2906,6 +2965,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "customData": undefined, "fillStyle": "solid", "frameId": null, "groupIds": [], @@ -2919,14 +2979,14 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "roundness": { "type": 3, }, - "seed": 1150084233, + "seed": 1116226695, "strokeColor": "#e03131", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, "version": 3, - "versionNonce": 400692809, + "versionNonce": 1604849351, "width": 20, "x": 20, "y": 30, @@ -2949,6 +3009,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "customData": undefined, "fillStyle": "solid", "frameId": null, "groupIds": [], @@ -2969,7 +3030,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 401146281, + "versionNonce": 2019559783, "width": 20, "x": -10, "y": 0, @@ -2978,6 +3039,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "angle": 0, "backgroundColor": "#a5d8ff", "boundElements": null, + "customData": undefined, "fillStyle": "solid", "frameId": null, "groupIds": [], @@ -2991,14 +3053,14 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "roundness": { "type": 3, }, - "seed": 1150084233, + "seed": 1116226695, "strokeColor": "#e03131", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, "version": 4, - "versionNonce": 1505387817, + "versionNonce": 23633383, "width": 20, "x": 20, "y": 30, @@ -3021,6 +3083,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "customData": undefined, "fillStyle": "solid", "frameId": null, "groupIds": [], @@ -3041,7 +3104,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 401146281, + "versionNonce": 2019559783, "width": 20, "x": -10, "y": 0, @@ -3050,6 +3113,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "angle": 0, "backgroundColor": "#a5d8ff", "boundElements": null, + "customData": undefined, "fillStyle": "cross-hatch", "frameId": null, "groupIds": [], @@ -3063,14 +3127,14 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "roundness": { "type": 3, }, - "seed": 1150084233, + "seed": 1116226695, "strokeColor": "#e03131", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, "version": 5, - "versionNonce": 493213705, + "versionNonce": 915032327, "width": 20, "x": 20, "y": 30, @@ -3093,6 +3157,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "customData": undefined, "fillStyle": "solid", "frameId": null, "groupIds": [], @@ -3113,7 +3178,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 401146281, + "versionNonce": 2019559783, "width": 20, "x": -10, "y": 0, @@ -3122,6 +3187,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "angle": 0, "backgroundColor": "#a5d8ff", "boundElements": null, + "customData": undefined, "fillStyle": "cross-hatch", "frameId": null, "groupIds": [], @@ -3135,14 +3201,14 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "roundness": { "type": 3, }, - "seed": 1150084233, + "seed": 1116226695, "strokeColor": "#e03131", "strokeStyle": "dotted", "strokeWidth": 2, "type": "rectangle", "updated": 1, "version": 6, - "versionNonce": 81784553, + "versionNonce": 747212839, "width": 20, "x": 20, "y": 30, @@ -3165,6 +3231,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "customData": undefined, "fillStyle": "solid", "frameId": null, "groupIds": [], @@ -3185,7 +3252,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 401146281, + "versionNonce": 2019559783, "width": 20, "x": -10, "y": 0, @@ -3194,6 +3261,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "angle": 0, "backgroundColor": "#a5d8ff", "boundElements": null, + "customData": undefined, "fillStyle": "cross-hatch", "frameId": null, "groupIds": [], @@ -3207,14 +3275,14 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "roundness": { "type": 3, }, - "seed": 1723083209, + "seed": 760410951, "strokeColor": "#e03131", "strokeStyle": "dotted", "strokeWidth": 2, "type": "rectangle", "updated": 1, "version": 7, - "versionNonce": 760410951, + "versionNonce": 1006504105, "width": 20, "x": 20, "y": 30, @@ -3237,6 +3305,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "customData": undefined, "fillStyle": "solid", "frameId": null, "groupIds": [], @@ -3257,7 +3326,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 401146281, + "versionNonce": 2019559783, "width": 20, "x": -10, "y": 0, @@ -3266,6 +3335,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "angle": 0, "backgroundColor": "#a5d8ff", "boundElements": null, + "customData": undefined, "fillStyle": "cross-hatch", "frameId": null, "groupIds": [], @@ -3279,14 +3349,14 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "roundness": { "type": 3, }, - "seed": 1723083209, + "seed": 760410951, "strokeColor": "#e03131", "strokeStyle": "dotted", "strokeWidth": 2, "type": "rectangle", "updated": 1, "version": 8, - "versionNonce": 289600103, + "versionNonce": 1315507081, "width": 20, "x": 20, "y": 30, @@ -3309,6 +3379,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "angle": 0, "backgroundColor": "#a5d8ff", "boundElements": null, + "customData": undefined, "fillStyle": "cross-hatch", "frameId": null, "groupIds": [], @@ -3329,7 +3400,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "type": "rectangle", "updated": 1, "version": 3, - "versionNonce": 1898319239, + "versionNonce": 640725609, "width": 20, "x": -10, "y": 0, @@ -3338,6 +3409,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "angle": 0, "backgroundColor": "#a5d8ff", "boundElements": null, + "customData": undefined, "fillStyle": "cross-hatch", "frameId": null, "groupIds": [], @@ -3351,14 +3423,14 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "roundness": { "type": 3, }, - "seed": 1723083209, + "seed": 760410951, "strokeColor": "#e03131", "strokeStyle": "dotted", "strokeWidth": 2, "type": "rectangle", "updated": 1, "version": 8, - "versionNonce": 289600103, + "versionNonce": 1315507081, "width": 20, "x": 20, "y": 30, @@ -3412,6 +3484,7 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e "exportScale": 1, "exportWithDarkMode": false, "fileHandle": null, + "followedBy": Set {}, "frameRendering": { "clip": true, "enabled": true, @@ -3464,6 +3537,7 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e "suggestedBindings": [], "theme": "light", "toast": null, + "userToFollow": null, "viewBackgroundColor": "#ffffff", "viewModeEnabled": false, "width": 200, @@ -3479,6 +3553,7 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "customData": undefined, "fillStyle": "solid", "frameId": null, "groupIds": [], @@ -3492,14 +3567,14 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e "roundness": { "type": 3, }, - "seed": 1150084233, + "seed": 1116226695, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, "version": 3, - "versionNonce": 400692809, + "versionNonce": 1604849351, "width": 20, "x": 20, "y": 30, @@ -3511,6 +3586,7 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "customData": undefined, "fillStyle": "solid", "frameId": null, "groupIds": [], @@ -3531,7 +3607,7 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 401146281, + "versionNonce": 2019559783, "width": 20, "x": -10, "y": 0, @@ -3570,6 +3646,7 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "customData": undefined, "fillStyle": "solid", "frameId": null, "groupIds": [], @@ -3590,7 +3667,7 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 401146281, + "versionNonce": 2019559783, "width": 20, "x": -10, "y": 0, @@ -3613,6 +3690,7 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "customData": undefined, "fillStyle": "solid", "frameId": null, "groupIds": [], @@ -3633,7 +3711,7 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 401146281, + "versionNonce": 2019559783, "width": 20, "x": -10, "y": 0, @@ -3642,6 +3720,7 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "customData": undefined, "fillStyle": "solid", "frameId": null, "groupIds": [], @@ -3655,14 +3734,14 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e "roundness": { "type": 3, }, - "seed": 1150084233, + "seed": 1116226695, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 1014066025, + "versionNonce": 238820263, "width": 20, "x": 20, "y": 30, @@ -3685,6 +3764,7 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "customData": undefined, "fillStyle": "solid", "frameId": null, "groupIds": [], @@ -3698,14 +3778,14 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e "roundness": { "type": 3, }, - "seed": 1150084233, + "seed": 1116226695, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, "version": 3, - "versionNonce": 400692809, + "versionNonce": 1604849351, "width": 20, "x": 20, "y": 30, @@ -3714,6 +3794,7 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "customData": undefined, "fillStyle": "solid", "frameId": null, "groupIds": [], @@ -3734,7 +3815,7 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 401146281, + "versionNonce": 2019559783, "width": 20, "x": -10, "y": 0, @@ -3788,6 +3869,7 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el "exportScale": 1, "exportWithDarkMode": false, "fileHandle": null, + "followedBy": Set {}, "frameRendering": { "clip": true, "enabled": true, @@ -3840,6 +3922,7 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el "suggestedBindings": [], "theme": "light", "toast": null, + "userToFollow": null, "viewBackgroundColor": "#ffffff", "viewModeEnabled": false, "width": 200, @@ -3855,6 +3938,7 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "customData": undefined, "fillStyle": "solid", "frameId": null, "groupIds": [], @@ -3868,14 +3952,14 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el "roundness": { "type": 3, }, - "seed": 1150084233, + "seed": 1116226695, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, "version": 3, - "versionNonce": 400692809, + "versionNonce": 1604849351, "width": 20, "x": 20, "y": 30, @@ -3887,6 +3971,7 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "customData": undefined, "fillStyle": "solid", "frameId": null, "groupIds": [], @@ -3907,7 +3992,7 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 401146281, + "versionNonce": 2019559783, "width": 20, "x": -10, "y": 0, @@ -3946,6 +4031,7 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "customData": undefined, "fillStyle": "solid", "frameId": null, "groupIds": [], @@ -3966,7 +4052,7 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 401146281, + "versionNonce": 2019559783, "width": 20, "x": -10, "y": 0, @@ -3989,6 +4075,7 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "customData": undefined, "fillStyle": "solid", "frameId": null, "groupIds": [], @@ -4009,7 +4096,7 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 401146281, + "versionNonce": 2019559783, "width": 20, "x": -10, "y": 0, @@ -4018,6 +4105,7 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "customData": undefined, "fillStyle": "solid", "frameId": null, "groupIds": [], @@ -4031,14 +4119,14 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el "roundness": { "type": 3, }, - "seed": 1150084233, + "seed": 1116226695, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 1014066025, + "versionNonce": 238820263, "width": 20, "x": 20, "y": 30, @@ -4061,6 +4149,7 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "customData": undefined, "fillStyle": "solid", "frameId": null, "groupIds": [], @@ -4074,14 +4163,14 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el "roundness": { "type": 3, }, - "seed": 1150084233, + "seed": 1116226695, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, "version": 3, - "versionNonce": 400692809, + "versionNonce": 1604849351, "width": 20, "x": 20, "y": 30, @@ -4090,6 +4179,7 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "customData": undefined, "fillStyle": "solid", "frameId": null, "groupIds": [], @@ -4110,7 +4200,7 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 401146281, + "versionNonce": 2019559783, "width": 20, "x": -10, "y": 0, @@ -4164,6 +4254,7 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung "exportScale": 1, "exportWithDarkMode": false, "fileHandle": null, + "followedBy": Set {}, "frameRendering": { "clip": true, "enabled": true, @@ -4219,6 +4310,7 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung "suggestedBindings": [], "theme": "light", "toast": null, + "userToFollow": null, "viewBackgroundColor": "#ffffff", "viewModeEnabled": false, "width": 200, @@ -4234,6 +4326,7 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "customData": undefined, "fillStyle": "solid", "frameId": null, "groupIds": [], @@ -4247,14 +4340,14 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung "roundness": { "type": 3, }, - "seed": 453191, + "seed": 449462985, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, "version": 4, - "versionNonce": 915032327, + "versionNonce": 81784553, "width": 20, "x": -10, "y": 0, @@ -4266,6 +4359,7 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "customData": undefined, "fillStyle": "solid", "frameId": null, "groupIds": [], @@ -4279,14 +4373,14 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung "roundness": { "type": 3, }, - "seed": 1116226695, + "seed": 1014066025, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, "version": 4, - "versionNonce": 81784553, + "versionNonce": 747212839, "width": 20, "x": 20, "y": 30, @@ -4325,6 +4419,7 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "customData": undefined, "fillStyle": "solid", "frameId": null, "groupIds": [], @@ -4338,7 +4433,7 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung "roundness": { "type": 3, }, - "seed": 453191, + "seed": 449462985, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -4368,6 +4463,7 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "customData": undefined, "fillStyle": "solid", "frameId": null, "groupIds": [], @@ -4381,7 +4477,7 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung "roundness": { "type": 3, }, - "seed": 453191, + "seed": 449462985, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -4397,6 +4493,7 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "customData": undefined, "fillStyle": "solid", "frameId": null, "groupIds": [], @@ -4410,14 +4507,14 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung "roundness": { "type": 3, }, - "seed": 1116226695, + "seed": 1014066025, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 238820263, + "versionNonce": 400692809, "width": 20, "x": 20, "y": 30, @@ -4443,6 +4540,7 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "customData": undefined, "fillStyle": "solid", "frameId": null, "groupIds": [ @@ -4458,14 +4556,14 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung "roundness": { "type": 3, }, - "seed": 453191, + "seed": 449462985, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, "version": 3, - "versionNonce": 1505387817, + "versionNonce": 23633383, "width": 20, "x": -10, "y": 0, @@ -4474,6 +4572,7 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "customData": undefined, "fillStyle": "solid", "frameId": null, "groupIds": [ @@ -4489,14 +4588,14 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung "roundness": { "type": 3, }, - "seed": 1116226695, + "seed": 1014066025, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, "version": 3, - "versionNonce": 23633383, + "versionNonce": 493213705, "width": 20, "x": 20, "y": 30, @@ -4520,6 +4619,7 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "customData": undefined, "fillStyle": "solid", "frameId": null, "groupIds": [], @@ -4533,14 +4633,14 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung "roundness": { "type": 3, }, - "seed": 453191, + "seed": 449462985, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, "version": 4, - "versionNonce": 915032327, + "versionNonce": 81784553, "width": 20, "x": -10, "y": 0, @@ -4549,6 +4649,7 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "customData": undefined, "fillStyle": "solid", "frameId": null, "groupIds": [], @@ -4562,14 +4663,14 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung "roundness": { "type": 3, }, - "seed": 1116226695, + "seed": 1014066025, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, "version": 4, - "versionNonce": 81784553, + "versionNonce": 747212839, "width": 20, "x": 20, "y": 30, @@ -4896,6 +4997,7 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi "exportScale": 1, "exportWithDarkMode": false, "fileHandle": null, + "followedBy": Set {}, "frameRendering": { "clip": true, "enabled": true, @@ -4951,6 +5053,7 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi "suggestedBindings": [], "theme": "light", "toast": null, + "userToFollow": null, "viewBackgroundColor": "#ffffff", "viewModeEnabled": false, "width": 200, @@ -4966,6 +5069,7 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "customData": undefined, "fillStyle": "solid", "frameId": null, "groupIds": [], @@ -4979,14 +5083,14 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi "roundness": { "type": 3, }, - "seed": 449462985, + "seed": 453191, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 401146281, + "versionNonce": 1116226695, "width": 10, "x": -10, "y": 0, @@ -4998,6 +5102,7 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "customData": undefined, "fillStyle": "solid", "frameId": null, "groupIds": [], @@ -5011,14 +5116,14 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi "roundness": { "type": 3, }, - "seed": 1150084233, + "seed": 238820263, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 1014066025, + "versionNonce": 1604849351, "width": 10, "x": 10, "y": 0, @@ -5057,6 +5162,7 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "customData": undefined, "fillStyle": "solid", "frameId": null, "groupIds": [], @@ -5070,14 +5176,14 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi "roundness": { "type": 3, }, - "seed": 449462985, + "seed": 453191, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 401146281, + "versionNonce": 1116226695, "width": 10, "x": -10, "y": 0, @@ -5100,6 +5206,7 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "customData": undefined, "fillStyle": "solid", "frameId": null, "groupIds": [], @@ -5113,14 +5220,14 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi "roundness": { "type": 3, }, - "seed": 449462985, + "seed": 453191, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 401146281, + "versionNonce": 1116226695, "width": 10, "x": -10, "y": 0, @@ -5129,6 +5236,7 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "customData": undefined, "fillStyle": "solid", "frameId": null, "groupIds": [], @@ -5142,14 +5250,14 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi "roundness": { "type": 3, }, - "seed": 1150084233, + "seed": 238820263, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 1014066025, + "versionNonce": 1604849351, "width": 10, "x": 10, "y": 0, @@ -5476,6 +5584,7 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro "exportScale": 1, "exportWithDarkMode": false, "fileHandle": null, + "followedBy": Set {}, "frameRendering": { "clip": true, "enabled": true, @@ -5533,6 +5642,7 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro "suggestedBindings": [], "theme": "light", "toast": null, + "userToFollow": null, "viewBackgroundColor": "#ffffff", "viewModeEnabled": false, "width": 200, @@ -5548,6 +5658,7 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "customData": undefined, "fillStyle": "solid", "frameId": null, "groupIds": [ @@ -5563,14 +5674,14 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro "roundness": { "type": 3, }, - "seed": 453191, + "seed": 449462985, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, "version": 3, - "versionNonce": 23633383, + "versionNonce": 493213705, "width": 10, "x": -10, "y": 0, @@ -5582,6 +5693,7 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "customData": undefined, "fillStyle": "solid", "frameId": null, "groupIds": [ @@ -5597,14 +5709,14 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro "roundness": { "type": 3, }, - "seed": 1116226695, + "seed": 1014066025, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, "version": 3, - "versionNonce": 493213705, + "versionNonce": 915032327, "width": 10, "x": 10, "y": 0, @@ -5643,6 +5755,7 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "customData": undefined, "fillStyle": "solid", "frameId": null, "groupIds": [], @@ -5656,7 +5769,7 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro "roundness": { "type": 3, }, - "seed": 453191, + "seed": 449462985, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -5686,6 +5799,7 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "customData": undefined, "fillStyle": "solid", "frameId": null, "groupIds": [], @@ -5699,7 +5813,7 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro "roundness": { "type": 3, }, - "seed": 453191, + "seed": 449462985, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -5715,6 +5829,7 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "customData": undefined, "fillStyle": "solid", "frameId": null, "groupIds": [], @@ -5728,14 +5843,14 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro "roundness": { "type": 3, }, - "seed": 1116226695, + "seed": 1014066025, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 238820263, + "versionNonce": 400692809, "width": 10, "x": 10, "y": 0, @@ -5761,6 +5876,7 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "customData": undefined, "fillStyle": "solid", "frameId": null, "groupIds": [ @@ -5776,14 +5892,14 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro "roundness": { "type": 3, }, - "seed": 453191, + "seed": 449462985, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, "version": 3, - "versionNonce": 23633383, + "versionNonce": 493213705, "width": 10, "x": -10, "y": 0, @@ -5792,6 +5908,7 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "customData": undefined, "fillStyle": "solid", "frameId": null, "groupIds": [ @@ -5807,14 +5924,14 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro "roundness": { "type": 3, }, - "seed": 1116226695, + "seed": 1014066025, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, "version": 3, - "versionNonce": 493213705, + "versionNonce": 915032327, "width": 10, "x": 10, "y": 0, @@ -5995,6 +6112,7 @@ exports[`contextMenu element > shows context menu for canvas > [end of test] app "exportScale": 1, "exportWithDarkMode": false, "fileHandle": null, + "followedBy": Set {}, "frameRendering": { "clip": true, "enabled": true, @@ -6048,6 +6166,7 @@ exports[`contextMenu element > shows context menu for canvas > [end of test] app "suggestedBindings": [], "theme": "light", "toast": null, + "userToFollow": null, "viewBackgroundColor": "#ffffff", "viewModeEnabled": false, "width": 200, @@ -6394,6 +6513,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap "exportScale": 1, "exportWithDarkMode": false, "fileHandle": null, + "followedBy": Set {}, "frameRendering": { "clip": true, "enabled": true, @@ -6446,6 +6566,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap "suggestedBindings": [], "theme": "light", "toast": null, + "userToFollow": null, "viewBackgroundColor": "#ffffff", "viewModeEnabled": false, "width": 200, @@ -6768,6 +6889,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap "exportScale": 1, "exportWithDarkMode": false, "fileHandle": null, + "followedBy": Set {}, "frameRendering": { "clip": true, "enabled": true, @@ -6823,6 +6945,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap "suggestedBindings": [], "theme": "light", "toast": null, + "userToFollow": null, "viewBackgroundColor": "#ffffff", "viewModeEnabled": false, "width": 200, @@ -6838,6 +6961,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] el "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "customData": undefined, "fillStyle": "solid", "frameId": null, "groupIds": [], @@ -6858,7 +6982,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] el "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 401146281, + "versionNonce": 2019559783, "width": 20, "x": -10, "y": 0, @@ -6870,6 +6994,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] el "angle": 0, "backgroundColor": "red", "boundElements": null, + "customData": undefined, "fillStyle": "solid", "frameId": null, "groupIds": [], @@ -6902,6 +7027,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] el "angle": 0, "backgroundColor": "red", "boundElements": null, + "customData": undefined, "fillStyle": "solid", "frameId": null, "groupIds": [], @@ -6961,6 +7087,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] hi "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "customData": undefined, "fillStyle": "solid", "frameId": null, "groupIds": [], @@ -6981,7 +7108,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] hi "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 401146281, + "versionNonce": 2019559783, "width": 20, "x": -10, "y": 0, diff --git a/src/tests/__snapshots__/dragCreate.test.tsx.snap b/packages/excalidraw/tests/__snapshots__/dragCreate.test.tsx.snap similarity index 96% rename from src/tests/__snapshots__/dragCreate.test.tsx.snap rename to packages/excalidraw/tests/__snapshots__/dragCreate.test.tsx.snap index 5c986f44b..91203eefb 100644 --- a/src/tests/__snapshots__/dragCreate.test.tsx.snap +++ b/packages/excalidraw/tests/__snapshots__/dragCreate.test.tsx.snap @@ -7,6 +7,7 @@ exports[`Test dragCreate > add element to the scene when pointer dragging long e "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "customData": undefined, "endArrowhead": "arrow", "endBinding": null, "fillStyle": "solid", @@ -56,6 +57,7 @@ exports[`Test dragCreate > add element to the scene when pointer dragging long e "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "customData": undefined, "fillStyle": "solid", "frameId": null, "groupIds": [], @@ -90,6 +92,7 @@ exports[`Test dragCreate > add element to the scene when pointer dragging long e "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "customData": undefined, "fillStyle": "solid", "frameId": null, "groupIds": [], @@ -122,6 +125,7 @@ exports[`Test dragCreate > add element to the scene when pointer dragging long e "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "customData": undefined, "endArrowhead": null, "endBinding": null, "fillStyle": "solid", @@ -171,6 +175,7 @@ exports[`Test dragCreate > add element to the scene when pointer dragging long e "angle": 0, "backgroundColor": "transparent", "boundElements": null, + "customData": undefined, "fillStyle": "solid", "frameId": null, "groupIds": [], diff --git a/packages/excalidraw/tests/__snapshots__/excalidraw.test.tsx.snap b/packages/excalidraw/tests/__snapshots__/excalidraw.test.tsx.snap new file mode 100644 index 000000000..c06fff7e4 --- /dev/null +++ b/packages/excalidraw/tests/__snapshots__/excalidraw.test.tsx.snap @@ -0,0 +1,619 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[` > > should render main menu with host menu items if passed from host 1`] = ` +