merge with master
|
@ -4,8 +4,16 @@
|
|||
!.eslintrc.json
|
||||
!.npmrc
|
||||
!.prettierrc
|
||||
!excalidraw-app/
|
||||
!package.json
|
||||
!public/
|
||||
!packages/
|
||||
!scripts/
|
||||
!tsconfig.json
|
||||
!yarn.lock
|
||||
|
||||
# keep (sub)sub directories at the end to exclude from explicit included
|
||||
# e.g. ./packages/excalidraw/{dist,node_modules}
|
||||
**/build
|
||||
**/dist
|
||||
**/node_modules
|
||||
|
|
|
@ -8,7 +8,7 @@ VITE_APP_LIBRARY_BACKEND=https://us-central1-excalidraw-room-persistence.cloudfu
|
|||
VITE_APP_WS_SERVER_URL=http://localhost:3002
|
||||
|
||||
VITE_APP_PLUS_LP=https://plus.excalidraw.com
|
||||
VITE_APP_PLUS_APP=https://app.excalidraw.com
|
||||
VITE_APP_PLUS_APP=http://localhost:3000
|
||||
|
||||
VITE_APP_AI_BACKEND=http://localhost:3015
|
||||
|
||||
|
@ -17,12 +17,10 @@ VITE_APP_FIREBASE_CONFIG='{"apiKey":"AIzaSyCMkxA60XIW8KbqMYL7edC4qT5l4qHX2h8","a
|
|||
# put these in your .env.local, or make sure you don't commit!
|
||||
# must be lowercase `true` when turned on
|
||||
#
|
||||
# whether to enable Service Workers in development
|
||||
VITE_APP_DEV_ENABLE_SW=
|
||||
# whether to disable live reload / HMR. Usuaully what you want to do when
|
||||
# debugging Service Workers.
|
||||
VITE_APP_DEV_DISABLE_LIVE_RELOAD=
|
||||
VITE_APP_DISABLE_TRACKING=true
|
||||
VITE_APP_ENABLE_TRACKING=true
|
||||
|
||||
FAST_REFRESH=false
|
||||
|
||||
|
@ -39,3 +37,17 @@ VITE_APP_COLLAPSE_OVERLAY=true
|
|||
|
||||
# Set this flag to false to disable eslint
|
||||
VITE_APP_ENABLE_ESLINT=true
|
||||
|
||||
# Enable PWA in dev server
|
||||
VITE_APP_ENABLE_PWA=false
|
||||
|
||||
VITE_APP_PLUS_EXPORT_PUBLIC_KEY='MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAm2g5T+Rub6Kbf1Mf57t0
|
||||
7r2zeHuVg4dla3r5ryXMswtzz6x767octl6oLThn33mQsPSy3GKglFZoCTXJR4ij
|
||||
ba8SxB04sL/N8eRrKja7TFWjCVtRwTTfyy771NYYNFVJclkxHyE5qw4m27crHF1y
|
||||
UNWEjuqNMi/lwAErS9fFa2oJlWyT8U7zzv/5kQREkxZI6y9v0AF3qcbsy2731FnD
|
||||
s9ChJvOUW9toIab2gsIdrKW8ZNpu084ZFVKb6LNjvIXI1Se4oMTHeszXzNptzlot
|
||||
kdxxjOoaQMAyfljFSot1F1FlU6MQlag7UnFGvFjRHN1JI5q4K+n3a67DX+TMyRqS
|
||||
HQIDAQAB'
|
||||
|
||||
# set to true in .env.development.local to disable the prevent unload dialog
|
||||
VITE_APP_DISABLE_PREVENT_UNLOAD=
|
||||
|
|
|
@ -14,4 +14,19 @@ 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"}'
|
||||
|
||||
VITE_APP_DISABLE_TRACKING=
|
||||
VITE_APP_ENABLE_TRACKING=false
|
||||
|
||||
VITE_APP_PLUS_EXPORT_PUBLIC_KEY='MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApQ0jM9Qz8TdFLzcuAZZX
|
||||
/WvuKSOJxiw6AR/ZcE3eFQWM/mbFdhQgyK8eHGkKQifKzH1xUZjCxyXcxW6ZO02t
|
||||
kPOPxhz+nxUrIoWCD/V4NGmUA1lxwHuO21HN1gzKrN3xHg5EGjyouR9vibT9VDGF
|
||||
gq6+4Ic/kJX+AD2MM7Yre2+FsOdysrmuW2Fu3ahuC1uQE7pOe1j0k7auNP0y1q53
|
||||
PrB8Ts2LUpepWC1l7zIXFm4ViDULuyWXTEpUcHSsEH8vpd1tckjypxCwkipfZsXx
|
||||
iPszy0o0Dx2iArPfWMXlFAI9mvyFCyFC3+nSvfyAUb2C4uZgCwAuyFh/ydPF4DEE
|
||||
PQIDAQAB'
|
||||
|
||||
# Set the below flags explicitly to false in production mode since vite loads and merges .env.local vars when running the build command
|
||||
VITE_APP_DEBUG_ENABLE_TEXT_CONTAINER_BOUNDING_BOX=false
|
||||
VITE_APP_COLLAPSE_OVERLAY=false
|
||||
# Enable eslint in dev server
|
||||
VITE_APP_ENABLE_ESLINT=false
|
||||
|
||||
|
|
|
@ -6,3 +6,6 @@ firebase/
|
|||
dist/
|
||||
public/workbox
|
||||
packages/excalidraw/types
|
||||
examples/**/public
|
||||
dev-dist
|
||||
coverage
|
||||
|
|
|
@ -1,7 +1,37 @@
|
|||
{
|
||||
"extends": ["@excalidraw/eslint-config", "react-app"],
|
||||
"rules": {
|
||||
"import/order": [
|
||||
"warn",
|
||||
{
|
||||
"groups": ["builtin", "external", "internal", "parent", "sibling", "index", "object", "type"],
|
||||
"pathGroups": [
|
||||
{
|
||||
"pattern": "@excalidraw/**",
|
||||
"group": "external",
|
||||
"position": "after"
|
||||
}
|
||||
],
|
||||
"newlines-between": "always-and-inside-groups",
|
||||
"warnOnUnassignedImports": true
|
||||
}
|
||||
],
|
||||
"import/no-anonymous-default-export": "off",
|
||||
"no-restricted-globals": "off"
|
||||
"no-restricted-globals": "off",
|
||||
"@typescript-eslint/consistent-type-imports": [
|
||||
"error",
|
||||
{
|
||||
"prefer": "type-imports",
|
||||
"disallowTypeAnnotations": false,
|
||||
"fixStyle": "separate-type-imports"
|
||||
}
|
||||
],
|
||||
"no-restricted-imports": [
|
||||
"error",
|
||||
{
|
||||
"name": "jotai",
|
||||
"message": "Do not import from \"jotai\" directly. Use our app-specific modules (\"editor-jotai\" or \"app-jotai\")."
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
2
.github/workflows/locales-coverage.yml
vendored
|
@ -10,7 +10,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ secrets.PUSH_TRANSLATIONS_COVERAGE_PAT }}
|
||||
|
||||
|
|
2
.github/workflows/semantic-pr-title.yml
vendored
|
@ -11,6 +11,6 @@ jobs:
|
|||
semantic:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: amannn/action-semantic-pull-request@v3.0.0
|
||||
- uses: amannn/action-semantic-pull-request@v5
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
|
8
.github/workflows/test.yml
vendored
|
@ -1,14 +1,16 @@
|
|||
name: Tests
|
||||
|
||||
on: pull_request
|
||||
on:
|
||||
push:
|
||||
branches: master
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup Node.js 18.x
|
||||
uses: actions/setup-node@v2
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18.x
|
||||
- name: Install and test
|
||||
|
|
2
.gitignore
vendored
|
@ -25,4 +25,4 @@ packages/excalidraw/types
|
|||
coverage
|
||||
dev-dist
|
||||
html
|
||||
examples/**/bundle.*
|
||||
meta*.json
|
12
Dockerfile
|
@ -2,16 +2,18 @@ FROM node:18 AS build
|
|||
|
||||
WORKDIR /opt/node_app
|
||||
|
||||
COPY package.json yarn.lock ./
|
||||
RUN yarn --ignore-optional --network-timeout 600000
|
||||
COPY . .
|
||||
|
||||
# do not ignore optional dependencies:
|
||||
# Error: Cannot find module @rollup/rollup-linux-x64-gnu
|
||||
RUN yarn --network-timeout 600000
|
||||
|
||||
ARG NODE_ENV=production
|
||||
|
||||
COPY . .
|
||||
RUN yarn build:app:docker
|
||||
|
||||
FROM nginx:1.21-alpine
|
||||
FROM nginx:1.27-alpine
|
||||
|
||||
COPY --from=build /opt/node_app/build /usr/share/nginx/html
|
||||
COPY --from=build /opt/node_app/excalidraw-app/build /usr/share/nginx/html
|
||||
|
||||
HEALTHCHECK CMD wget -q -O /dev/null http://localhost || exit 1
|
||||
|
|
12
README.md
|
@ -7,7 +7,7 @@
|
|||
|
||||
<h4 align="center">
|
||||
<a href="https://excalidraw.com">Excalidraw Editor</a> |
|
||||
<a href="https://blog.excalidraw.com">Blog</a> |
|
||||
<a href="https://plus.excalidraw.com/blog">Blog</a> |
|
||||
<a href="https://docs.excalidraw.com">Documentation</a> |
|
||||
<a href="https://plus.excalidraw.com">Excalidraw+</a>
|
||||
</h4>
|
||||
|
@ -87,13 +87,11 @@ We'll be adding these features as drop-in plugins for the npm package in the fut
|
|||
|
||||
**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).
|
||||
|
||||
```
|
||||
Use `npm` or `yarn` to install the package.
|
||||
|
||||
```bash
|
||||
npm install react react-dom @excalidraw/excalidraw
|
||||
```
|
||||
|
||||
or via yarn
|
||||
|
||||
```
|
||||
# or
|
||||
yarn add react react-dom @excalidraw/excalidraw
|
||||
```
|
||||
|
||||
|
|
|
@ -133,7 +133,7 @@ function App() {
|
|||
}
|
||||
```
|
||||
|
||||
Here is a [complete list](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/components/mainMenu/DefaultItems.tsx) of the default items.
|
||||
Here is a [complete list](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/components/main-menu/DefaultItems.tsx) of the default items.
|
||||
|
||||
### MainMenu.Group
|
||||
|
||||
|
|
|
@ -31,7 +31,7 @@ The welcome screen consists of two main groups of subcomponents:
|
|||
|
||||
<img
|
||||
src={require("@site/static/img/welcome-screen-overview.png").default}
|
||||
alt="Excalidraw logo: Sketch handrawn like diagrams."
|
||||
alt="Excalidraw logo: Sketch hand-drawn like diagrams."
|
||||
/>
|
||||
|
||||
### Center
|
||||
|
|
|
@ -8,15 +8,15 @@
|
|||
import { FONT_FAMILY } from "@excalidraw/excalidraw";
|
||||
```
|
||||
|
||||
`FONT_FAMILY` contains all the font families used in `Excalidraw` as explained below
|
||||
`FONT_FAMILY` contains all the font families used in `Excalidraw`. The default families are the following:
|
||||
|
||||
| Font Family | Description |
|
||||
| ----------- | ---------------------- |
|
||||
| `Virgil` | The `handwritten` font |
|
||||
| `Helvetica` | The `Normal` Font |
|
||||
| `Cascadia` | The `Code` Font |
|
||||
| `Excalifont` | The `Hand-drawn` font |
|
||||
| `Nunito` | The `Normal` Font |
|
||||
| `Comic Shanns` | The `Code` Font |
|
||||
|
||||
Defaults to `FONT_FAMILY.Virgil` unless passed in `initialData.appState.currentItemFontFamily`.
|
||||
Pre-selected family is `FONT_FAMILY.Excalifont`, unless it's overriden with `initialData.appState.currentItemFontFamily`.
|
||||
|
||||
### THEME
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@ Once the callback is triggered, you will need to store the api in state to acces
|
|||
```jsx showLineNumbers
|
||||
export default function App() {
|
||||
const [excalidrawAPI, setExcalidrawAPI] = useState(null);
|
||||
return <Excalidraw excalidrawAPI={{(api)=> setExcalidrawAPI(api)}} />;
|
||||
return <Excalidraw excalidrawAPI={(api)=> setExcalidrawAPI(api)} />;
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -22,7 +22,7 @@ You can use this prop when you want to access some [Excalidraw APIs](https://git
|
|||
| API | Signature | Usage |
|
||||
| --- | --- | --- |
|
||||
| [updateScene](#updatescene) | `function` | updates the scene with the sceneData |
|
||||
| [updateLibrary](#updatelibrary) | `function` | updates the scene with the sceneData |
|
||||
| [updateLibrary](#updatelibrary) | `function` | updates the library |
|
||||
| [addFiles](#addfiles) | `function` | add files data to the appState |
|
||||
| [resetScene](#resetscene) | `function` | Resets the scene. If `resetLoadingState` is passed as true then it will also force set the loading state to false. |
|
||||
| [getSceneElementsIncludingDeleted](#getsceneelementsincludingdeleted) | `function` | Returns all the elements including the deleted in the scene |
|
||||
|
@ -65,7 +65,7 @@ You can use this function to update the scene with the sceneData. It accepts the
|
|||
| `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` | <code>Map<string, <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L37">Collaborator></a></code> | The list of collaborators to be updated in the scene. |
|
||||
| `commitToHistory` | `boolean` | Implies if the `history (undo/redo)` should be recorded. Defaults to `false`. |
|
||||
| `captureUpdate` | [`CaptureUpdateAction`](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/store.ts#L40) | Controls which updates should be captured by the `Store`. Captured updates are emmitted and listened to by other components, such as `History` for undo / redo purposes. |
|
||||
|
||||
```jsx live
|
||||
function App() {
|
||||
|
@ -105,6 +105,7 @@ function App() {
|
|||
appState: {
|
||||
viewBackgroundColor: "#edf2ff",
|
||||
},
|
||||
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
|
||||
};
|
||||
excalidrawAPI.updateScene(sceneData);
|
||||
};
|
||||
|
@ -115,12 +116,24 @@ function App() {
|
|||
<button className="custom-button" onClick={updateScene}>
|
||||
Update Scene
|
||||
</button>
|
||||
<Excalidraw ref={(api) => setExcalidrawAPI(api)} />
|
||||
<Excalidraw excalidrawAPI={(api) => setExcalidrawAPI(api)} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
#### captureUpdate
|
||||
|
||||
You can use the `captureUpdate` to influence undo / redo behaviour.
|
||||
|
||||
> **NOTE**: Some updates are not observed by the store / history - i.e. updates to `collaborators` object or parts of `AppState` which are not observed (not `ObservedAppState`). Such updates will never make it to the undo / redo stacks, regardless of the passed `captureUpdate` value.
|
||||
|
||||
| | `captureUpdate` value | Notes |
|
||||
| --- | --- | --- |
|
||||
| _Immediately undoable_ | `CaptureUpdateAction.IMMEDIATELY` | Use for updates which should be captured. Should be used for most of the local updates. These updates will _immediately_ make it to the local undo / redo stacks. |
|
||||
| _Eventually undoable_ | `CaptureUpdateAction.EVENTUALLY` | Use for updates which should not be captured immediately - likely exceptions which are part of some async multi-step process. Otherwise, all such updates would end up being captured with the next `CaptureUpdateAction.IMMEDIATELY` - triggered either by the next `updateScene` or internally by the editor. These updates will _eventually_ make it to the local undo / redo stacks. |
|
||||
| _Never undoable_ | `CaptureUpdateAction.NEVER` | Use for updates which should never be recorded, such as remote updates or scene initialization. These updates will _never_ make it to the local undo / redo stacks. |
|
||||
|
||||
### updateLibrary
|
||||
|
||||
<pre>
|
||||
|
@ -188,7 +201,7 @@ function App() {
|
|||
Update Library
|
||||
</button>
|
||||
<Excalidraw
|
||||
ref={(api) => setExcalidrawAPI(api)}
|
||||
excalidrawAPI={(api) => setExcalidrawAPI(api)}
|
||||
// initial data retrieved from https://github.com/excalidraw/excalidraw/blob/master/dev-docs/packages/excalidraw/initialData.js
|
||||
initialData={{
|
||||
libraryItems: initialData.libraryItems,
|
||||
|
|
|
@ -5,30 +5,31 @@ All `props` are _optional_.
|
|||
| Name | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| [`initialData`](/docs/@excalidraw/excalidraw/api/props/initialdata) | `object` | `null` | <code>Promise<object | null></code> | `null` | The initial data with which app loads. |
|
||||
| [`excalidrawAPI`](/docs/@excalidraw/excalidraw/api/props/excalidraw-api) | `function` | _ | Callback triggered with the excalidraw api once rendered |
|
||||
| [`isCollaborating`](#iscollaborating) | `boolean` | _ | This indicates if the app is in `collaboration` mode |
|
||||
| [`onChange`](#onchange) | `function` | _ | This callback is triggered whenever the component updates due to any change. This callback will receive the excalidraw `elements` and the current `app state`. |
|
||||
| [`onPointerUpdate`](#onpointerupdate) | `function` | _ | Callback triggered when mouse pointer is updated. |
|
||||
| [`onPointerDown`](#onpointerdown) | `function` | _ | This prop if passed gets triggered on pointer down evenets |
|
||||
| [`onScrollChange`](#onscrollchange) | `function` | _ | This prop if passed gets triggered when scrolling the canvas. |
|
||||
| [`onPaste`](#onpaste) | `function` | _ | Callback to be triggered if passed when the something is pasted in to the scene |
|
||||
| [`onLibraryChange`](#onlibrarychange) | `function` | _ | The callback if supplied is triggered when the library is updated and receives the library items. |
|
||||
| [`onLinkOpen`](#onlinkopen) | `function` | _ | The callback if supplied is triggered when any link is opened. |
|
||||
| [`excalidrawAPI`](/docs/@excalidraw/excalidraw/api/props/excalidraw-api) | `function` | \_ | Callback triggered with the excalidraw api once rendered |
|
||||
| [`isCollaborating`](#iscollaborating) | `boolean` | \_ | This indicates if the app is in `collaboration` mode |
|
||||
| [`onChange`](#onchange) | `function` | \_ | This callback is triggered whenever the component updates due to any change. This callback will receive the excalidraw `elements` and the current `app state`. |
|
||||
| [`onPointerUpdate`](#onpointerupdate) | `function` | \_ | Callback triggered when mouse pointer is updated. |
|
||||
| [`onPointerDown`](#onpointerdown) | `function` | \_ | This prop if passed gets triggered on pointer down events |
|
||||
| [`onScrollChange`](#onscrollchange) | `function` | \_ | This prop if passed gets triggered when scrolling the canvas. |
|
||||
| [`onPaste`](#onpaste) | `function` | \_ | Callback to be triggered if passed when something is pasted into the scene |
|
||||
| [`onLibraryChange`](#onlibrarychange) | `function` | \_ | The callback if supplied is triggered when the library is updated and receives the library items. |
|
||||
| [`generateLinkForSelection`](#generatelinkforselection) | `function` | \_ | Allows you to override `url` generation when linking to Excalidraw elements. |
|
||||
| [`onLinkOpen`](#onlinkopen) | `function` | \_ | The callback if supplied is triggered when any link is opened. |
|
||||
| [`langCode`](#langcode) | `string` | `en` | Language code string to be used in Excalidraw |
|
||||
| [`renderTopRightUI`](/docs/@excalidraw/excalidraw/api/props/render-props#rendertoprightui) | `function` | _ | Render function that renders custom UI in top right corner |
|
||||
| [`renderCustomStats`](/docs/@excalidraw/excalidraw/api/props/render-props#rendercustomstats) | `function` | _ | Render function that can be used to render custom stats on the stats dialog. |
|
||||
| [`viewModeEnabled`](#viewmodeenabled) | `boolean` | _ | This indicates if the app is in `view` mode. |
|
||||
| [`zenModeEnabled`](#zenmodeenabled) | `boolean` | _ | This indicates if the `zen` mode is enabled |
|
||||
| [`gridModeEnabled`](#gridmodeenabled) | `boolean` | _ | This indicates if the `grid` mode is enabled |
|
||||
| [`libraryReturnUrl`](#libraryreturnurl) | `string` | _ | What URL should [libraries.excalidraw.com](https://libraries.excalidraw.com) be installed to |
|
||||
| [`renderTopRightUI`](/docs/@excalidraw/excalidraw/api/props/render-props#rendertoprightui) | `function` | \_ | Render function that renders custom UI in top right corner |
|
||||
| [`renderCustomStats`](/docs/@excalidraw/excalidraw/api/props/render-props#rendercustomstats) | `function` | \_ | Render function that can be used to render custom stats on the stats dialog. |
|
||||
| [`viewModeEnabled`](#viewmodeenabled) | `boolean` | \_ | This indicates if the app is in `view` mode. |
|
||||
| [`zenModeEnabled`](#zenmodeenabled) | `boolean` | \_ | This indicates if the `zen` mode is enabled |
|
||||
| [`gridModeEnabled`](#gridmodeenabled) | `boolean` | \_ | This indicates if the `grid` mode is enabled |
|
||||
| [`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/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 |
|
||||
| [`generateIdForFile`](#generateidforfile) | `function` | _ | Allows you to override `id` generation for files added on canvas |
|
||||
| [`validateEmbeddable`](#validateEmbeddable) | string[] | `boolean | RegExp | RegExp[] | ((link: string) => boolean | undefined)` | \_ | use for custom src url validation |
|
||||
| [`autoFocus`](#autofocus) | `boolean` | `false` | Indicates whether to focus the Excalidraw component on page load |
|
||||
| [`generateIdForFile`](#generateidforfile) | `function` | \_ | Allows you to override `id` generation for files added on canvas |
|
||||
| [`validateEmbeddable`](#validateembeddable) | `string[]` \| `boolean` \| `RegExp` \| `RegExp[]` \| <code>((link: string) => boolean | undefined)</code> | \_ | use for custom src url validation |
|
||||
| [`renderEmbeddable`](/docs/@excalidraw/excalidraw/api/props/render-props#renderEmbeddable) | `function` | \_ | Render function that can override the built-in `<iframe>` |
|
||||
|
||||
### Storing custom data on Excalidraw elements
|
||||
|
@ -93,7 +94,6 @@ This callback is triggered when mouse pointer is updated.
|
|||
|
||||
This prop if passed will be triggered on pointer down events and has the below signature.
|
||||
|
||||
|
||||
<pre>
|
||||
(activeTool:{" "}
|
||||
<a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L115">
|
||||
|
@ -143,6 +143,14 @@ This callback if supplied will get triggered when the library is updated and has
|
|||
|
||||
It is invoked with empty items when user clears the library. You can use this callback when you want to do something additional when library is updated for example persisting it to local storage.
|
||||
|
||||
### generateLinkForSelection
|
||||
|
||||
This prop if passed will be used to replace the default link generation function. The idea is that the host app can take over the creation of element links, which can be used to navigate to a particular element or a group. If the host app chooses a different key for element link id, then the host app should also take care of the handling and the navigation in `onLinkOpen`.
|
||||
|
||||
```tsx
|
||||
(id: string, type: "element" | "group") => string;
|
||||
```
|
||||
|
||||
### onLinkOpen
|
||||
|
||||
This prop if passed will be triggered when clicked on `link`. To handle the redirect yourself (such as when using your own router for internal links), you must call `event.preventDefault()`.
|
||||
|
@ -207,8 +215,7 @@ This prop indicates whether the shows the grid. When supplied, the value takes p
|
|||
|
||||
### libraryReturnUrl
|
||||
|
||||
If supplied, this URL will be used when user tries to install a library from [libraries.excalidraw.com](https://libraries.excalidraw.com).
|
||||
Defaults to _window.location.origin + window.location.pathname_. To install the libraries in the same tab from which it was opened, you need to set `window.name` (to any alphanumeric string) — if it's not set it will open in a new tab.
|
||||
If supplied, this URL will be used when user tries to install a library from [libraries.excalidraw.com](https://libraries.excalidraw.com). Defaults to _window.location.origin + window.location.pathname_. To install the libraries in the same tab from which it was opened, you need to set `window.name` (to any alphanumeric string) — if it's not set it will open in a new tab.
|
||||
|
||||
### theme
|
||||
|
||||
|
@ -220,7 +227,6 @@ You can use [`THEME`](/docs/@excalidraw/excalidraw/api/utils#theme) to specify t
|
|||
|
||||
This prop sets the `name` of the drawing which will be used when exporting the drawing. When supplied, the value takes precedence over _intialData.appState.name_, the `name` will be fully controlled by host app and the users won't be able to edit from within Excalidraw.
|
||||
|
||||
|
||||
### detectScroll
|
||||
|
||||
Indicates whether Excalidraw should listen for `scroll` event on the nearest scrollable container in the DOM tree and recompute the coordinates (e.g. to correctly handle the cursor) when the component's position changes. You can disable this when you either know this doesn't affect your app or you want to take care of it yourself (calling the [`refresh()`](#ref) method).
|
||||
|
|
|
@ -20,7 +20,7 @@ exportToCanvas({<br/>
|
|||
getDimensions,<br/>
|
||||
files,<br/>
|
||||
exportPadding?: number;<br/>
|
||||
}: <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/packages/utils.ts#L21">ExportOpts</a>
|
||||
}: <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/utils/export.ts#L24">ExportOpts</a>
|
||||
</pre>
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
|
@ -90,7 +90,7 @@ function App() {
|
|||
<img src={canvasUrl} alt="" />
|
||||
</div>
|
||||
<div style={{ height: "400px" }}>
|
||||
<Excalidraw ref={(api) => setExcalidrawAPI(api)}
|
||||
<Excalidraw excalidrawAPI={(api) => setExcalidrawAPI(api)}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
|
|
|
@ -31,7 +31,7 @@ You can pass `null` / `undefined` if not applicable.
|
|||
restoreElements(
|
||||
elements: <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L114">ImportedDataState["elements"]</a>,<br/>
|
||||
localElements: <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L114">ExcalidrawElement[]</a> | null | undefined): <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L114">ExcalidrawElement[]</a>,<br/>
|
||||
opts: { refreshDimensions?: boolean, repairBindings?: boolean }<br/>
|
||||
opts: { refreshDimensions?: boolean, repairBindings?: boolean, normalizeIndices?: boolean }<br/>
|
||||
)
|
||||
</pre>
|
||||
|
||||
|
@ -51,8 +51,9 @@ The extra optional parameter to configure restored elements. It has the followin
|
|||
|
||||
| Prop | Type | Description|
|
||||
| --- | --- | ------|
|
||||
| `refreshDimensions` | `boolean` | Indicates whether we should also `recalculate` text element dimensions. Since this is a potentially costly operation, you may want to disable it if you restore elements in tight loops, such as during collaboration. |
|
||||
| `repairBindings` |`boolean` | Indicates whether the `bindings` for the elements should be repaired. This is to make sure there are no containers with non existent bound text element id and no bound text elements with non existent container id. |
|
||||
| `refreshDimensions` | `boolean` | Indicates whether we should also _recalculate_ text element dimensions. Since this is a potentially costly operation, you may want to disable it if you restore elements in tight loops, such as during collaboration. |
|
||||
| `repairBindings` |`boolean` | Indicates whether the _bindings_ for the elements should be repaired. This is to make sure there are no containers with non existent bound text element id and no bound text elements with non existent container id. |
|
||||
| `normalizeIndices` |`boolean` | Indicates whether _fractional indices_ for the elements should be normalized. This is to prevent possible issues caused by using stale (too old, too long) indices. |
|
||||
|
||||
**_How to use_**
|
||||
|
||||
|
@ -73,7 +74,7 @@ restore(
|
|||
data: <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/data/types.ts#L34">ImportedDataState</a>,<br/>
|
||||
localAppState: Partial<<a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L95">AppState</a>> | null | undefined,<br/>
|
||||
localElements: <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L114">ExcalidrawElement[]</a> | null | undefined<br/>): <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/data/types.ts#L4">DataState</a><br/>
|
||||
opts: { refreshDimensions?: boolean, repairBindings?: boolean }<br/>
|
||||
opts: { refreshDimensions?: boolean, repairBindings?: boolean, normalizeIndices?: boolean }<br/>
|
||||
|
||||
)
|
||||
</pre>
|
||||
|
|
|
@ -13,18 +13,18 @@ To start the example app using the `@excalidraw/excalidraw` package, follow the
|
|||
1. Install the dependencies
|
||||
|
||||
```bash
|
||||
cd packages/excalidraw && yarn
|
||||
yarn
|
||||
```
|
||||
|
||||
2. Start the example app
|
||||
|
||||
```bash
|
||||
yarn start
|
||||
yarn start:example
|
||||
```
|
||||
|
||||
[http://localhost:3001](http://localhost:3001) will open in your default browser.
|
||||
|
||||
The example is same as the [codesandbox example](https://ehlz3.csb.app/)
|
||||
This is the same example as the [CodeSandbox](https://codesandbox.io/p/sandbox/github/excalidraw/excalidraw/tree/master/examples/with-script-in-browser) example.
|
||||
|
||||
## Releasing
|
||||
|
||||
|
|
|
@ -17,11 +17,9 @@ We strongly recommend turning it off. You can follow the steps below on how to d
|
|||
<div style={{width:'30rem'}}>
|
||||
|
||||
2. Once opened, look for **Aggressively Block Fingerprinting**
|
||||
|
||||

|
||||
|
||||
3. Switch to **Block Fingerprinting**
|
||||
|
||||

|
||||
|
||||
4. Thats all. All text elements should be fixed now 🎉
|
||||
|
|
|
@ -1,16 +1,12 @@
|
|||
# Installation
|
||||
|
||||
**Excalidraw** is published to npm as a component you can directly embed in your projects.
|
||||
**Excalidraw** is exported as a component to be directly embedded in your project.
|
||||
|
||||
Using `npm`:
|
||||
Use `npm` or `yarn` to install the package.
|
||||
|
||||
```bash
|
||||
npm install react react-dom @excalidraw/excalidraw
|
||||
```
|
||||
|
||||
or `yarn`:
|
||||
|
||||
```bash
|
||||
# or
|
||||
yarn add react react-dom @excalidraw/excalidraw
|
||||
```
|
||||
|
||||
|
@ -20,24 +16,40 @@ yarn add react react-dom @excalidraw/excalidraw
|
|||
|
||||
:::
|
||||
|
||||
### Static assets
|
||||
### Self-hosting fonts
|
||||
|
||||
Excalidraw depends on assets such as localization files (if you opt to use them), fonts, and others.
|
||||
By default, Excalidraw will try to download all the used fonts from the [CDN](https://esm.run/@excalidraw/excalidraw/dist/prod).
|
||||
|
||||
By default these assets are loaded from a public CDN [`https://unpkg.com/@excalidraw/excalidraw/dist/`](https://unpkg.com/@excalidraw/excalidraw/dist), so you don't need to do anything on your end.
|
||||
|
||||
However, if you want to host these files yourself, you can find them in your `node_modules/@excalidraw/excalidraw/dist` directory, in folders `excalidraw-assets` (for production) and `excalidraw-assets-dev` (for development).
|
||||
|
||||
Copy these folders to your static assets directory, and add a `window.EXCALIDRAW_ASSET_PATH` variable in your `index.html` or `index.js` entry file pointing to your public assets path (relative). For example, if you serve your assets from the root of your hostname, you would do:
|
||||
For self-hosting purposes, you'll have to copy the content of the folder `node_modules/@excalidraw/excalidraw/dist/prod/fonts` to the path where your assets should be served from (i.e. `public/` directory in your project). In that case, you should also set `window.EXCALIDRAW_ASSET_PATH` to the very same path, i.e. `/` in case it's in the root:
|
||||
|
||||
```js
|
||||
window.EXCALIDRAW_ASSET_PATH = "/";
|
||||
```
|
||||
|
||||
or, if you serve your assets from the root of your CDN, you would do:
|
||||
|
||||
```js
|
||||
// Vanilla
|
||||
<head>
|
||||
<script>
|
||||
window.EXCALIDRAW_ASSET_PATH = "https://my.cdn.com/assets/";
|
||||
</script>
|
||||
</head>
|
||||
```
|
||||
|
||||
or, if you prefer the path to be dynamicly set based on the `location.origin`, you could do the following:
|
||||
|
||||
```jsx
|
||||
// Next.js
|
||||
<Script id="load-env-variables" strategy="beforeInteractive" >
|
||||
{ `window["EXCALIDRAW_ASSET_PATH"] = location.origin;` } // or use just "/"!
|
||||
</Script>
|
||||
```
|
||||
|
||||
### Dimensions of Excalidraw
|
||||
|
||||
Excalidraw takes _100%_ of `width` and `height` of the containing block so make sure the container in which you render Excalidraw has non zero dimensions.
|
||||
|
||||
### Demo
|
||||
## Demo
|
||||
|
||||
[Try here](https://codesandbox.io/s/excalidraw-ehlz3).
|
||||
Go to [CodeSandbox](https://codesandbox.io/p/sandbox/github/excalidraw/excalidraw/tree/master/examples/with-script-in-browser) example.
|
||||
|
|
|
@ -58,7 +58,7 @@ If you are using `pages router` then importing the wrapper dynamically would wor
|
|||
|
||||
```jsx showLineNumbers
|
||||
"use client";
|
||||
import { Excalidraw. convertToExcalidrawElements } from "@excalidraw/excalidraw";
|
||||
import { Excalidraw, convertToExcalidrawElements } from "@excalidraw/excalidraw";
|
||||
|
||||
import "@excalidraw/excalidraw/index.css";
|
||||
|
||||
|
@ -70,7 +70,7 @@ If you are using `pages router` then importing the wrapper dynamically would wor
|
|||
height: 141.9765625,
|
||||
},]));
|
||||
return (
|
||||
<div style={{height:"500px", width:"500px"}}
|
||||
<div style={{height:"500px", width:"500px"}}>
|
||||
<Excalidraw />
|
||||
</div>
|
||||
);
|
||||
|
@ -128,11 +128,10 @@ If you are using `pages router` then importing the wrapper dynamically would wor
|
|||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
{/* Link should be updated to point to the latest! */}
|
||||
Here is a [source code](https://github.com/excalidraw/excalidraw/tree/master/examples/with-nextjs) for the example with app and pages router. You you can try it out [here](https://excalidraw-package-example-with-nextjs.vercel.app/).
|
||||
|
||||
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/).
|
||||
|
||||
|
||||
The `types` are available at `@excalidraw/excalidraw/types`, you can view [example for typescript](https://codesandbox.io/s/excalidraw-types-9h2dm)
|
||||
The `types` are available at `@excalidraw/excalidraw/types`, check [CodeSandbox](https://codesandbox.io/p/sandbox/github/excalidraw/excalidraw/tree/master/examples/with-script-in-browser) example for details.
|
||||
|
||||
### Preact
|
||||
|
||||
|
@ -157,27 +156,9 @@ Since Vite removes env variables by default, you can update the vite config to e
|
|||
|
||||
## Browser
|
||||
|
||||
To use it in a browser directly:
|
||||
To use it Excalidraw in a browser directly, use the following setup :point_down:
|
||||
|
||||
For development use :point_down:
|
||||
|
||||
```js
|
||||
<script
|
||||
type="text/javascript"
|
||||
src="https://unpkg.com/@excalidraw/excalidraw/dist/excalidraw.development.js"
|
||||
></script>
|
||||
```
|
||||
|
||||
For production use :point_down:
|
||||
|
||||
```js
|
||||
<script
|
||||
type="text/javascript"
|
||||
src="https://unpkg.com/@excalidraw/excalidraw/dist/excalidraw.production.min.js"
|
||||
></script>
|
||||
```
|
||||
|
||||
You will need to make sure `react`, `react-dom` is available as shown in the below example. For prod please use the production versions of `react`, `react-dom`.
|
||||
> **Note**: We rely on import maps to de-duplicate `react`, `react-dom` and `react/jsx-runtime` versions.
|
||||
|
||||
import Tabs from "@theme/Tabs";
|
||||
import TabItem from "@theme/TabItem";
|
||||
|
@ -191,13 +172,23 @@ import TabItem from "@theme/TabItem";
|
|||
<head>
|
||||
<title>Excalidraw in browser</title>
|
||||
<meta charset="UTF-8" />
|
||||
<script src="https://unpkg.com/react@18.2.0/umd/react.development.js"></script>
|
||||
<script src="https://unpkg.com/react-dom@18.2.0/umd/react-dom.development.js"></script>
|
||||
|
||||
<script
|
||||
type="text/javascript"
|
||||
src="https://unpkg.com/@excalidraw/excalidraw/dist/excalidraw.development.js"
|
||||
></script>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://esm.sh/@excalidraw/excalidraw@0.18.0/dist/dev/index.css"
|
||||
/>
|
||||
<link rel="stylesheet" href="./index.css" />
|
||||
<script>
|
||||
window.EXCALIDRAW_ASSET_PATH = "https://esm.sh/@excalidraw/excalidraw@0.18.0/dist/prod/";
|
||||
</script>
|
||||
<script type="importmap">
|
||||
{
|
||||
"imports": {
|
||||
"react": "https://esm.sh/react@19.0.0",
|
||||
"react/jsx-runtime": "https://esm.sh/react@19.0.0/jsx-runtime",
|
||||
"react-dom": "https://esm.sh/react-dom@19.0.0"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
@ -214,6 +205,14 @@ import TabItem from "@theme/TabItem";
|
|||
<TabItem value="js" label="Javascript">
|
||||
|
||||
```js showLineNumbers
|
||||
// See https://www.npmjs.com/package/@excalidraw/excalidraw documentation.
|
||||
import * as ExcalidrawLib from 'https://esm.sh/@excalidraw/excalidraw@0.18.0/dist/dev/index.js?external=react,react-dom';
|
||||
import React from "https://esm.sh/react@19.0.0";
|
||||
import ReactDOM from "https://esm.sh/react-dom@19.0.0"
|
||||
|
||||
window.ExcalidrawLib = ExcalidrawLib;
|
||||
console.log("Excalidraw library", ExcalidrawLib);
|
||||
|
||||
const App = () => {
|
||||
return React.createElement(
|
||||
React.Fragment,
|
||||
|
@ -235,3 +234,5 @@ root.render(React.createElement(App));
|
|||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
You can try it out [here](https://jsfiddle.net/vfn6dm14/3/).
|
||||
|
|
|
@ -14,7 +14,7 @@ This API receives the mermaid syntax as the input, and resolves to skeleton Exca
|
|||
import { parseMermaidToExcalidraw } from "@excalidraw/mermaid-to-excalidraw";
|
||||
import { convertToExcalidrawElements} from "@excalidraw/excalidraw"
|
||||
try {
|
||||
const { elements, files } = await parseMermaid(mermaidSyntax: string, {
|
||||
const { elements, files } = await parseMermaidToExcalidraw(mermaidSyntax: string, {
|
||||
fontSize: number,
|
||||
});
|
||||
const excalidrawElements = convertToExcalidrawElements(elements);
|
||||
|
|
|
@ -43,7 +43,7 @@ When saving an Excalidraw scene locally to a file, the JSON file (`.excalidraw`)
|
|||
|
||||
// editor state (canvas config, preferences, ...)
|
||||
"appState": {
|
||||
"gridSize": null,
|
||||
"gridSize": 20,
|
||||
"viewBackgroundColor": "#ffffff"
|
||||
},
|
||||
|
||||
|
|
|
@ -66,7 +66,7 @@ const config = {
|
|||
label: "Docs",
|
||||
},
|
||||
{
|
||||
to: "https://blog.excalidraw.com",
|
||||
to: "https://plus.excalidraw.com/blog",
|
||||
label: "Blog",
|
||||
position: "left",
|
||||
},
|
||||
|
@ -111,7 +111,7 @@ const config = {
|
|||
items: [
|
||||
{
|
||||
label: "Blog",
|
||||
to: "https://blog.excalidraw.com",
|
||||
to: "https://plus.excalidraw.com/blog",
|
||||
},
|
||||
{
|
||||
label: "GitHub",
|
||||
|
@ -149,6 +149,29 @@ const config = {
|
|||
systemvars: true,
|
||||
},
|
||||
],
|
||||
function () {
|
||||
return {
|
||||
name: "disable-fully-specified-error",
|
||||
configureWebpack() {
|
||||
return {
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.m?js$/,
|
||||
resolve: {
|
||||
fullySpecified: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
optimization: {
|
||||
// disable terser minification
|
||||
minimize: false,
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
|
|
|
@ -18,13 +18,13 @@
|
|||
"@docusaurus/core": "2.2.0",
|
||||
"@docusaurus/preset-classic": "2.2.0",
|
||||
"@docusaurus/theme-live-codeblock": "2.2.0",
|
||||
"@excalidraw/excalidraw": "0.17.0",
|
||||
"@excalidraw/excalidraw": "0.18.0",
|
||||
"@mdx-js/react": "^1.6.22",
|
||||
"clsx": "^1.2.1",
|
||||
"docusaurus-plugin-sass": "0.2.3",
|
||||
"prism-react-renderer": "^1.3.5",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"sass": "1.57.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import React from "react";
|
||||
import clsx from "clsx";
|
||||
import React from "react";
|
||||
|
||||
import styles from "./styles.module.css";
|
||||
|
||||
const FeatureList = [
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import React from "react";
|
||||
import clsx from "clsx";
|
||||
import React from "react";
|
||||
|
||||
import styles from "./styles.module.css";
|
||||
|
||||
type FeatureItem = {
|
||||
|
|
|
@ -59,7 +59,7 @@ pre a {
|
|||
padding: 5px;
|
||||
background: #70b1ec;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
font-weight: 700;
|
||||
border: none;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
import React from "react";
|
||||
import clsx from "clsx";
|
||||
import Layout from "@theme/Layout";
|
||||
import Link from "@docusaurus/Link";
|
||||
import useDocusaurusContext from "@docusaurus/useDocusaurusContext";
|
||||
import styles from "./index.module.css";
|
||||
import HomepageFeatures from "@site/src/components/Homepage";
|
||||
import Layout from "@theme/Layout";
|
||||
import clsx from "clsx";
|
||||
import React from "react";
|
||||
|
||||
import styles from "./index.module.css";
|
||||
|
||||
function HomepageHeader() {
|
||||
const { siteConfig } = useDocusaurusContext();
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
// Import the original mapper
|
||||
import MDXComponents from "@theme-original/MDXComponents";
|
||||
import Highlight from "@site/src/components/Highlight";
|
||||
import MDXComponents from "@theme-original/MDXComponents";
|
||||
|
||||
export default {
|
||||
// Re-use the default mapping
|
||||
|
|
|
@ -3,11 +3,18 @@ import ExecutionEnvironment from "@docusaurus/ExecutionEnvironment";
|
|||
import initialData from "@site/src/initialData";
|
||||
import { useColorMode } from "@docusaurus/theme-common";
|
||||
|
||||
import "@excalidraw/excalidraw/index.css";
|
||||
|
||||
let ExcalidrawComp = {};
|
||||
if (ExecutionEnvironment.canUseDOM) {
|
||||
ExcalidrawComp = require("@excalidraw/excalidraw");
|
||||
}
|
||||
const Excalidraw = React.forwardRef((props, ref) => {
|
||||
if (!window.EXCALIDRAW_ASSET_PATH) {
|
||||
window.EXCALIDRAW_ASSET_PATH =
|
||||
"https://esm.sh/@excalidraw/excalidraw@0.18.0/dist/prod/";
|
||||
}
|
||||
|
||||
const { colorMode } = useColorMode();
|
||||
return <ExcalidrawComp.Excalidraw theme={colorMode} {...props} ref={ref} />;
|
||||
});
|
||||
|
|
1501
dev-docs/yarn.lock
|
@ -1,13 +0,0 @@
|
|||
{
|
||||
"name": "examples",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"@excalidraw/excalidraw": "*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
{
|
||||
"extends": "../../tsconfig"
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
{
|
||||
"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"
|
||||
}
|
||||
}
|
|
@ -1,4 +0,0 @@
|
|||
{
|
||||
"outputDirectory": "dist",
|
||||
"installCommand": "yarn install"
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
import { defineConfig } from "vite";
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
server: {
|
||||
port: 3001,
|
||||
// open the browser
|
||||
open: true,
|
||||
},
|
||||
publicDir: "public",
|
||||
});
|
|
@ -1,313 +0,0 @@
|
|||
# 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"
|
|
@ -34,3 +34,6 @@ yarn-error.log*
|
|||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
|
||||
# copied assets
|
||||
public/**/*.woff2
|
|
@ -3,22 +3,22 @@
|
|||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build:workspace": "yarn workspace @excalidraw/excalidraw run build:esm",
|
||||
"build:workspace": "yarn workspace @excalidraw/excalidraw run build:esm && yarn copy:assets",
|
||||
"copy:assets": "cp -r ../../packages/excalidraw/dist/prod/fonts ./public",
|
||||
"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"
|
||||
"react": "19.0.0",
|
||||
"react-dom": "19.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
"@types/react": "19.0.10",
|
||||
"@types/react-dom": "19.0.4",
|
||||
"path2d-polyfill": "2.0.1",
|
||||
"typescript": "^5"
|
||||
}
|
Before Width: | Height: | Size: 197 KiB After Width: | Height: | Size: 197 KiB |
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
Before Width: | Height: | Size: 6.1 KiB After Width: | Height: | Size: 6.1 KiB |
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 39 KiB |
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
|
@ -1,4 +1,6 @@
|
|||
import dynamic from "next/dynamic";
|
||||
import Script from "next/script";
|
||||
|
||||
import "../common.scss";
|
||||
|
||||
// Since client components get prerenderd on server as well hence importing the excalidraw stuff dynamically
|
||||
|
@ -15,7 +17,9 @@ export default function Page() {
|
|||
<>
|
||||
<a href="/excalidraw-in-pages">Switch to Pages router</a>
|
||||
<h1 className="page-title">App Router</h1>
|
||||
|
||||
<Script id="load-env-variables" strategy="beforeInteractive">
|
||||
{`window["EXCALIDRAW_ASSET_PATH"] = window.origin;`}
|
||||
</Script>
|
||||
{/* @ts-expect-error - https://github.com/vercel/next.js/issues/42292 */}
|
||||
<ExcalidrawWithClientOnly />
|
||||
</>
|
|
@ -7,7 +7,7 @@ a {
|
|||
color: #1c7ed6;
|
||||
font-size: 20px;
|
||||
text-decoration: none;
|
||||
font-weight: 550;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.page-title {
|
|
@ -1,10 +1,11 @@
|
|||
"use client";
|
||||
import * as excalidrawLib from "@excalidraw/excalidraw";
|
||||
import { Excalidraw } from "@excalidraw/excalidraw";
|
||||
import App from "../../components/App";
|
||||
|
||||
import "@excalidraw/excalidraw/index.css";
|
||||
|
||||
import App from "../../with-script-in-browser/components/ExampleApp";
|
||||
|
||||
const ExcalidrawWrapper: React.FC = () => {
|
||||
return (
|
||||
<>
|
|
@ -1,4 +1,5 @@
|
|||
import dynamic from "next/dynamic";
|
||||
|
||||
import "../common.scss";
|
||||
|
||||
// Since client components get prerenderd on server as well hence importing the excalidraw stuff dynamically
|
5
examples/with-script-in-browser/.codesandbox/Dockerfile
Normal file
|
@ -0,0 +1,5 @@
|
|||
FROM node:18-bullseye
|
||||
|
||||
# Vite wants to open the browser using `open`, so we
|
||||
# need to install those utils.
|
||||
RUN apt update -y && apt install -y xdg-utils
|
35
examples/with-script-in-browser/.codesandbox/tasks.json
Normal file
|
@ -0,0 +1,35 @@
|
|||
{
|
||||
// These tasks will run in order when initializing your CodeSandbox project.
|
||||
"setupTasks": [
|
||||
{
|
||||
"name": "Install Dependencies",
|
||||
"command": "yarn install"
|
||||
}
|
||||
],
|
||||
|
||||
// These tasks can be run from CodeSandbox. Running one will open a log in the app.
|
||||
"tasks": {
|
||||
"build": {
|
||||
"name": "Build",
|
||||
"command": "yarn build",
|
||||
"runAtStart": false
|
||||
},
|
||||
"start": {
|
||||
"name": "Start Example",
|
||||
"command": "yarn start",
|
||||
"runAtStart": true,
|
||||
"preview": {
|
||||
"port": 3001
|
||||
}
|
||||
},
|
||||
"install-deps": {
|
||||
"name": "Install Dependencies",
|
||||
"command": "yarn install",
|
||||
"restartOn": {
|
||||
"files": ["yarn.lock"],
|
||||
"branch": false,
|
||||
"resume": false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
2
examples/with-script-in-browser/.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
# copied assets
|
||||
public/**/*.woff2
|
|
@ -1,5 +1,7 @@
|
|||
import React from "react";
|
||||
|
||||
import type * as TExcalidraw from "@excalidraw/excalidraw";
|
||||
import type { ExcalidrawImperativeAPI } from "@excalidraw/excalidraw/dist/excalidraw/types";
|
||||
import type { ExcalidrawImperativeAPI } from "@excalidraw/excalidraw/types";
|
||||
|
||||
const COMMENT_SVG = (
|
||||
<svg
|
|
@ -1,3 +1,4 @@
|
|||
import { nanoid } from "nanoid";
|
||||
import React, {
|
||||
useEffect,
|
||||
useState,
|
||||
|
@ -6,25 +7,13 @@ import React, {
|
|||
Children,
|
||||
cloneElement,
|
||||
} from "react";
|
||||
import ExampleSidebar from "./sidebar/ExampleSidebar";
|
||||
|
||||
import type * as TExcalidraw from "@excalidraw/excalidraw";
|
||||
|
||||
import { nanoid } from "nanoid";
|
||||
|
||||
import {
|
||||
resolvablePromise,
|
||||
ResolvablePromise,
|
||||
distance2d,
|
||||
fileOpen,
|
||||
withBatchedUpdates,
|
||||
withBatchedUpdatesThrottled,
|
||||
} from "../utils";
|
||||
|
||||
import CustomFooter from "./CustomFooter";
|
||||
import MobileFooter from "./MobileFooter";
|
||||
import initialData from "../initialData";
|
||||
|
||||
import type { ImportedLibraryData } from "@excalidraw/excalidraw/data/types";
|
||||
import type {
|
||||
NonDeletedExcalidrawElement,
|
||||
Theme,
|
||||
} from "@excalidraw/excalidraw/element/types";
|
||||
import type {
|
||||
AppState,
|
||||
BinaryFileData,
|
||||
|
@ -33,14 +22,24 @@ import type {
|
|||
Gesture,
|
||||
LibraryItems,
|
||||
PointerDownState as ExcalidrawPointerDownState,
|
||||
} 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";
|
||||
} from "@excalidraw/excalidraw/types";
|
||||
|
||||
import "./App.scss";
|
||||
import initialData from "../initialData";
|
||||
import {
|
||||
resolvablePromise,
|
||||
distance2d,
|
||||
fileOpen,
|
||||
withBatchedUpdates,
|
||||
withBatchedUpdatesThrottled,
|
||||
} from "../utils";
|
||||
|
||||
import CustomFooter from "./CustomFooter";
|
||||
import MobileFooter from "./MobileFooter";
|
||||
import ExampleSidebar from "./sidebar/ExampleSidebar";
|
||||
|
||||
import "./ExampleApp.scss";
|
||||
|
||||
import type { ResolvablePromise } from "../utils";
|
||||
|
||||
type Comment = {
|
||||
x: number;
|
||||
|
@ -73,7 +72,7 @@ export interface AppProps {
|
|||
excalidrawLib: typeof TExcalidraw;
|
||||
}
|
||||
|
||||
export default function App({
|
||||
export default function ExampleApp({
|
||||
appTitle,
|
||||
useCustom,
|
||||
customArgs,
|
||||
|
@ -872,7 +871,7 @@ export default function App({
|
|||
files: excalidrawAPI.getFiles(),
|
||||
});
|
||||
const ctx = canvas.getContext("2d")!;
|
||||
ctx.font = "30px Virgil";
|
||||
ctx.font = "30px Excalifont";
|
||||
ctx.strokeText("My custom text", 50, 60);
|
||||
setCanvasUrl(canvas.toDataURL());
|
||||
}}
|
||||
|
@ -893,7 +892,7 @@ export default function App({
|
|||
files: excalidrawAPI.getFiles(),
|
||||
});
|
||||
const ctx = canvas.getContext("2d")!;
|
||||
ctx.font = "30px Virgil";
|
||||
ctx.font = "30px Excalifont";
|
||||
ctx.strokeText("My custom text", 50, 60);
|
||||
setCanvasUrl(canvas.toDataURL());
|
||||
}}
|
|
@ -1,6 +1,9 @@
|
|||
import { ExcalidrawImperativeAPI } from "@excalidraw/excalidraw/dist/excalidraw/types";
|
||||
import CustomFooter from "./CustomFooter";
|
||||
import React from "react";
|
||||
|
||||
import type * as TExcalidraw from "@excalidraw/excalidraw";
|
||||
import type { ExcalidrawImperativeAPI } from "@excalidraw/excalidraw/types";
|
||||
|
||||
import CustomFooter from "./CustomFooter";
|
||||
|
||||
const MobileFooter = ({
|
||||
excalidrawAPI,
|
|
@ -1,4 +1,5 @@
|
|||
import { useState } from "react";
|
||||
import React, { useState } from "react";
|
||||
|
||||
import "./ExampleSidebar.scss";
|
||||
|
||||
export default function Sidebar({ children }: { children: React.ReactNode }) {
|
|
@ -11,16 +11,15 @@
|
|||
<title>React App</title>
|
||||
<script>
|
||||
window.name = "codesandbox";
|
||||
window.EXCALIDRAW_ASSET_PATH =
|
||||
"https://esm.sh/@excalidraw/excalidraw@0.18.0/dist/prod/";
|
||||
</script>
|
||||
<link rel="stylesheet" href="/dist/browser/dev/index.css" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<noscript> You need to enable JavaScript to run this app. </noscript>
|
||||
<div id="root"></div>
|
||||
|
||||
<!-- This is so that we use the bundled excalidraw.development.js file instead
|
||||
of the actual source code -->
|
||||
<script type="module">
|
||||
import * as ExcalidrawLib from "@excalidraw/excalidraw";
|
||||
|
|
@ -1,10 +1,11 @@
|
|||
import App from "../components/App";
|
||||
import React, { StrictMode } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
|
||||
import "@excalidraw/excalidraw/index.css";
|
||||
|
||||
import type * as TExcalidraw from "@excalidraw/excalidraw";
|
||||
|
||||
import "@excalidraw/excalidraw/index.css";
|
||||
import App from "./components/ExampleApp";
|
||||
|
||||
declare global {
|
||||
interface Window {
|
|
@ -46,7 +46,7 @@ const elements: ExcalidrawElementSkeleton[] = [
|
|||
];
|
||||
export default {
|
||||
elements,
|
||||
appState: { viewBackgroundColor: "#AFEEEE", currentItemFontFamily: 1 },
|
||||
appState: { viewBackgroundColor: "#AFEEEE", currentItemFontFamily: 5 },
|
||||
scrollToContent: true,
|
||||
libraryItems: [
|
||||
[
|
22
examples/with-script-in-browser/package.json
Normal file
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"name": "with-script-in-browser",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"react": "19.0.0",
|
||||
"react-dom": "19.0.0",
|
||||
"@excalidraw/excalidraw": "*",
|
||||
"browser-fs-access": "0.29.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vite": "5.0.12",
|
||||
"typescript": "^5"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview --port 5002",
|
||||
"build:preview": "yarn build && yarn preview",
|
||||
"build:package": "yarn workspace @excalidraw/excalidraw run build:esm"
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 197 KiB After Width: | Height: | Size: 197 KiB |
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
Before Width: | Height: | Size: 6.1 KiB After Width: | Height: | Size: 6.1 KiB |
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 39 KiB |
9
examples/with-script-in-browser/tsconfig.json
Normal file
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"module": "ES2022",
|
||||
"moduleResolution": "Bundler",
|
||||
"lib": ["ESNext", "DOM", "DOM.Iterable"],
|
||||
"jsx": "react-jsx",
|
||||
"skipLibCheck": true
|
||||
}
|
||||
}
|
|
@ -1,7 +1,6 @@
|
|||
import { unstable_batchedUpdates } from "react-dom";
|
||||
import { MIME_TYPES } from "@excalidraw/excalidraw";
|
||||
import { fileOpen as _fileOpen } from "browser-fs-access";
|
||||
import type { MIME_TYPES } from "@excalidraw/excalidraw";
|
||||
import { AbortError } from "../../packages/excalidraw/errors";
|
||||
import { unstable_batchedUpdates } from "react-dom";
|
||||
|
||||
type FILE_EXTENSION = Exclude<keyof typeof MIME_TYPES, "binary">;
|
||||
|
||||
|
@ -85,7 +84,7 @@ export const fileOpen = <M extends boolean | undefined = false>(opts: {
|
|||
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());
|
||||
rejectPromise(new Error("Request Aborted"));
|
||||
}
|
||||
};
|
||||
},
|
5
examples/with-script-in-browser/vercel.json
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"outputDirectory": "dist",
|
||||
"installCommand": "yarn install",
|
||||
"buildCommand": "yarn build:package && yarn build"
|
||||
}
|
19
examples/with-script-in-browser/vite.config.mts
Normal file
|
@ -0,0 +1,19 @@
|
|||
import { defineConfig } from "vite";
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
server: {
|
||||
port: 3001,
|
||||
// open the browser
|
||||
open: true,
|
||||
},
|
||||
publicDir: "public",
|
||||
optimizeDeps: {
|
||||
esbuildOptions: {
|
||||
// Bumping to 2022 due to "Arbitrary module namespace identifier names" not being
|
||||
// supported in Vite's default browser target https://github.com/vitejs/vite/issues/13556
|
||||
target: "es2022",
|
||||
treeShaking: true,
|
||||
},
|
||||
},
|
||||
});
|
|
@ -1,112 +1,147 @@
|
|||
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 {
|
||||
Excalidraw,
|
||||
LiveCollaborationTrigger,
|
||||
TTDDialogTrigger,
|
||||
CaptureUpdateAction,
|
||||
reconcileElements,
|
||||
getCommonBounds,
|
||||
} from "@excalidraw/excalidraw";
|
||||
import { trackEvent } from "@excalidraw/excalidraw/analytics";
|
||||
import { getDefaultAppState } from "@excalidraw/excalidraw/appState";
|
||||
import {
|
||||
CommandPalette,
|
||||
DEFAULT_CATEGORIES,
|
||||
} from "@excalidraw/excalidraw/components/CommandPalette/CommandPalette";
|
||||
import { ErrorDialog } from "@excalidraw/excalidraw/components/ErrorDialog";
|
||||
import { OverwriteConfirmDialog } from "@excalidraw/excalidraw/components/OverwriteConfirm/OverwriteConfirm";
|
||||
import { openConfirmModal } from "@excalidraw/excalidraw/components/OverwriteConfirm/OverwriteConfirmState";
|
||||
import { ShareableLinkDialog } from "@excalidraw/excalidraw/components/ShareableLinkDialog";
|
||||
import Trans from "@excalidraw/excalidraw/components/Trans";
|
||||
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,
|
||||
getCommonBounds,
|
||||
} from "../packages/excalidraw/index";
|
||||
import {
|
||||
AppState,
|
||||
LibraryItems,
|
||||
ExcalidrawImperativeAPI,
|
||||
BinaryFiles,
|
||||
ExcalidrawInitialDataState,
|
||||
UIAppState,
|
||||
ScrollConstraints,
|
||||
} from "../packages/excalidraw/types";
|
||||
import {
|
||||
debounce,
|
||||
getVersion,
|
||||
getFrame,
|
||||
isTestEnv,
|
||||
preventUnload,
|
||||
ResolvablePromise,
|
||||
resolvablePromise,
|
||||
isRunningInIframe,
|
||||
} from "../packages/excalidraw/utils";
|
||||
isDevEnv,
|
||||
} from "@excalidraw/common";
|
||||
import polyfill from "@excalidraw/excalidraw/polyfill";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { loadFromBlob } from "@excalidraw/excalidraw/data/blob";
|
||||
import { useCallbackRefState } from "@excalidraw/excalidraw/hooks/useCallbackRefState";
|
||||
import { t } from "@excalidraw/excalidraw/i18n";
|
||||
|
||||
import {
|
||||
GithubIcon,
|
||||
XBrandIcon,
|
||||
DiscordIcon,
|
||||
ExcalLogo,
|
||||
usersIcon,
|
||||
exportToPlus,
|
||||
share,
|
||||
youtubeIcon,
|
||||
} from "@excalidraw/excalidraw/components/icons";
|
||||
import { isElementLink } from "@excalidraw/element/elementLink";
|
||||
import { restore, restoreAppState } from "@excalidraw/excalidraw/data/restore";
|
||||
import { newElementWith } from "@excalidraw/element/mutateElement";
|
||||
import { isInitializedImageElement } from "@excalidraw/element/typeChecks";
|
||||
import clsx from "clsx";
|
||||
import {
|
||||
parseLibraryTokensFromUrl,
|
||||
useHandleLibrary,
|
||||
} from "@excalidraw/excalidraw/data/library";
|
||||
|
||||
import type { RemoteExcalidrawElement } from "@excalidraw/excalidraw/data/reconcile";
|
||||
import type { RestoredDataState } from "@excalidraw/excalidraw/data/restore";
|
||||
import type {
|
||||
ExcalidrawElement,
|
||||
FileId,
|
||||
NonDeletedExcalidrawElement,
|
||||
OrderedExcalidrawElement,
|
||||
} from "@excalidraw/element/types";
|
||||
import type {
|
||||
AppState,
|
||||
ExcalidrawImperativeAPI,
|
||||
BinaryFiles,
|
||||
ExcalidrawInitialDataState,
|
||||
UIAppState,
|
||||
ScrollConstraints,
|
||||
} from "@excalidraw/excalidraw/types";
|
||||
import type { Merge, ResolutionType } from "@excalidraw/common/utility-types";
|
||||
import type { ResolvablePromise } from "@excalidraw/common/utils";
|
||||
|
||||
import CustomStats from "./CustomStats";
|
||||
import {
|
||||
Provider,
|
||||
useAtom,
|
||||
useAtomValue,
|
||||
useAtomWithInitialValue,
|
||||
appJotaiStore,
|
||||
} from "./app-jotai";
|
||||
import {
|
||||
FIREBASE_STORAGE_PREFIXES,
|
||||
isExcalidrawPlusSignedUser,
|
||||
STORAGE_KEYS,
|
||||
SYNC_BROWSER_TABS_TIMEOUT,
|
||||
} from "./app_constants";
|
||||
import Collab, {
|
||||
CollabAPI,
|
||||
collabAPIAtom,
|
||||
isCollaboratingAtom,
|
||||
isOfflineAtom,
|
||||
} from "./collab/Collab";
|
||||
import { AppFooter } from "./components/AppFooter";
|
||||
import { AppMainMenu } from "./components/AppMainMenu";
|
||||
import { AppWelcomeScreen } from "./components/AppWelcomeScreen";
|
||||
import {
|
||||
ExportToExcalidrawPlus,
|
||||
exportToExcalidrawPlus,
|
||||
} from "./components/ExportToExcalidrawPlus";
|
||||
import { TopErrorBoundary } from "./components/TopErrorBoundary";
|
||||
|
||||
import {
|
||||
exportToBackend,
|
||||
getCollaborationLinkData,
|
||||
isCollaborationLink,
|
||||
loadScene,
|
||||
} from "./data";
|
||||
|
||||
import { updateStaleImageStatuses } from "./data/FileManager";
|
||||
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";
|
||||
LibraryIndexedDBAdapter,
|
||||
LibraryLocalStorageMigrationAdapter,
|
||||
LocalData,
|
||||
} from "./data/LocalData";
|
||||
import { isBrowserStorageStateNewer } from "./data/tabSync";
|
||||
import { ShareDialog, shareDialogStateAtom } from "./share/ShareDialog";
|
||||
import CollabError, { collabErrorIndicatorAtom } from "./collab/CollabError";
|
||||
import { useHandleAppTheme } from "./useHandleAppTheme";
|
||||
import { getPreferredLanguage } from "./app-language/language-detector";
|
||||
import { useAppLangCode } from "./app-language/language-state";
|
||||
import DebugCanvas, {
|
||||
debugRenderer,
|
||||
isVisualDebuggerEnabled,
|
||||
loadSavedDebugState,
|
||||
} from "./components/DebugCanvas";
|
||||
import { AIComponents } from "./components/AI";
|
||||
import { ExcalidrawPlusIframeExport } from "./ExcalidrawPlusIframeExport";
|
||||
|
||||
import "./index.scss";
|
||||
import { Merge, 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";
|
||||
import { getSelectedElements } from "../packages/excalidraw/scene";
|
||||
|
||||
import type { CollabAPI } from "./collab/Collab";
|
||||
import { getSelectedElements } from "@excalidraw/element/selection";
|
||||
|
||||
polyfill();
|
||||
|
||||
|
@ -257,6 +292,38 @@ const ConstraintsSettings = ({
|
|||
|
||||
window.EXCALIDRAW_THROTTLE_RENDER = true;
|
||||
|
||||
declare global {
|
||||
interface BeforeInstallPromptEventChoiceResult {
|
||||
outcome: "accepted" | "dismissed";
|
||||
}
|
||||
|
||||
interface BeforeInstallPromptEvent extends Event {
|
||||
prompt(): Promise<void>;
|
||||
userChoice: Promise<BeforeInstallPromptEventChoiceResult>;
|
||||
}
|
||||
|
||||
interface WindowEventMap {
|
||||
beforeinstallprompt: BeforeInstallPromptEvent;
|
||||
}
|
||||
}
|
||||
|
||||
let pwaEvent: BeforeInstallPromptEvent | null = null;
|
||||
|
||||
// Adding a listener outside of the component as it may (?) need to be
|
||||
// subscribed early to catch the event.
|
||||
//
|
||||
// Also note that it will fire only if certain heuristics are met (user has
|
||||
// used the app for some time, etc.)
|
||||
window.addEventListener(
|
||||
"beforeinstallprompt",
|
||||
(event: BeforeInstallPromptEvent) => {
|
||||
// prevent Chrome <= 67 from automatically showing the prompt
|
||||
event.preventDefault();
|
||||
// cache for later use
|
||||
pwaEvent = event;
|
||||
},
|
||||
);
|
||||
|
||||
let isSelfEmbedding = false;
|
||||
|
||||
if (window.self !== window.top) {
|
||||
|
@ -271,11 +338,6 @@ if (window.self !== window.top) {
|
|||
}
|
||||
}
|
||||
|
||||
const languageDetector = new LanguageDetector();
|
||||
languageDetector.init({
|
||||
languageUtils: {},
|
||||
});
|
||||
|
||||
const shareableLinkConfirmDialog = {
|
||||
title: t("overwriteConfirm.modal.shareableLink.title"),
|
||||
description: (
|
||||
|
@ -413,7 +475,7 @@ const initializeScene = async (opts: {
|
|||
},
|
||||
elements: reconcileElements(
|
||||
scene?.elements || [],
|
||||
excalidrawAPI.getSceneElementsIncludingDeleted(),
|
||||
excalidrawAPI.getSceneElementsIncludingDeleted() as RemoteExcalidrawElement[],
|
||||
excalidrawAPI.getAppState(),
|
||||
),
|
||||
},
|
||||
|
@ -434,16 +496,14 @@ const initializeScene = async (opts: {
|
|||
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();
|
||||
|
||||
const { editorTheme, appTheme, setAppTheme } = useHandleAppTheme();
|
||||
|
||||
const [langCode, setLangCode] = useAppLangCode();
|
||||
|
||||
// initial state
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
|
@ -455,6 +515,8 @@ const ExcalidrawWrapper = () => {
|
|||
resolvablePromise<ExcalidrawInitialDataState | null>();
|
||||
}
|
||||
|
||||
const debugCanvasRef = useRef<HTMLCanvasElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
trackEvent("load", "frame", getFrame());
|
||||
// Delayed so that the app has a time to load the latest SW
|
||||
|
@ -471,12 +533,32 @@ const ExcalidrawWrapper = () => {
|
|||
const [isCollaborating] = useAtomWithInitialValue(isCollaboratingAtom, () => {
|
||||
return isCollaborationLink(window.location.href);
|
||||
});
|
||||
const collabError = useAtomValue(collabErrorIndicatorAtom);
|
||||
|
||||
useHandleLibrary({
|
||||
excalidrawAPI,
|
||||
getInitialLibraryItems: getLibraryItemsFromStorage,
|
||||
adapter: LibraryIndexedDBAdapter,
|
||||
// TODO maybe remove this in several months (shipped: 24-03-11)
|
||||
migrationAdapter: LibraryLocalStorageMigrationAdapter,
|
||||
});
|
||||
|
||||
const [, forceRefresh] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (isDevEnv()) {
|
||||
const debugState = loadSavedDebugState();
|
||||
|
||||
if (debugState.enabled && !window.visualDebug) {
|
||||
window.visualDebug = {
|
||||
data: [],
|
||||
};
|
||||
} else {
|
||||
delete window.visualDebug;
|
||||
}
|
||||
forceRefresh((prev) => !prev);
|
||||
}
|
||||
}, [excalidrawAPI]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!excalidrawAPI || (!isCollabDisabled && !collabAPI)) {
|
||||
return;
|
||||
|
@ -572,7 +654,7 @@ const ExcalidrawWrapper = () => {
|
|||
excalidrawAPI.updateScene({
|
||||
...data.scene,
|
||||
...restore(data.scene, null, null, { repairBindings: true }),
|
||||
commitToHistory: true,
|
||||
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
@ -596,16 +678,17 @@ const ExcalidrawWrapper = () => {
|
|||
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);
|
||||
setLangCode(getPreferredLanguage());
|
||||
excalidrawAPI.updateScene({
|
||||
...localDataState,
|
||||
captureUpdate: CaptureUpdateAction.NEVER,
|
||||
});
|
||||
LibraryIndexedDBAdapter.load().then((data) => {
|
||||
if (data) {
|
||||
excalidrawAPI.updateLibrary({
|
||||
libraryItems: getLibraryItemsFromStorage(),
|
||||
libraryItems: data.libraryItems,
|
||||
});
|
||||
}
|
||||
});
|
||||
collabAPI?.setUsername(username || "");
|
||||
}
|
||||
|
@ -687,7 +770,13 @@ const ExcalidrawWrapper = () => {
|
|||
excalidrawAPI.getSceneElements(),
|
||||
)
|
||||
) {
|
||||
if (import.meta.env.VITE_APP_DISABLE_PREVENT_UNLOAD !== "true") {
|
||||
preventUnload(event);
|
||||
} else {
|
||||
console.warn(
|
||||
"preventing unload disabled (VITE_APP_DISABLE_PREVENT_UNLOAD)",
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
window.addEventListener(EVENT.BEFORE_UNLOAD, unloadHandler);
|
||||
|
@ -696,29 +785,8 @@ const ExcalidrawWrapper = () => {
|
|||
};
|
||||
}, [excalidrawAPI]);
|
||||
|
||||
useEffect(() => {
|
||||
languageDetector.cacheUserLanguage(langCode);
|
||||
}, [langCode]);
|
||||
|
||||
const [theme, setTheme] = useState<Theme>(
|
||||
() =>
|
||||
(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[],
|
||||
elements: readonly OrderedExcalidrawElement[],
|
||||
appState: AppState,
|
||||
files: BinaryFiles,
|
||||
) => {
|
||||
|
@ -726,8 +794,6 @@ const ExcalidrawWrapper = () => {
|
|||
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()) {
|
||||
|
@ -753,11 +819,22 @@ const ExcalidrawWrapper = () => {
|
|||
if (didChange) {
|
||||
excalidrawAPI.updateScene({
|
||||
elements,
|
||||
captureUpdate: CaptureUpdateAction.NEVER,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Render the debug scene if the debug canvas is available
|
||||
if (debugCanvasRef.current && excalidrawAPI) {
|
||||
debugRenderer(
|
||||
debugCanvasRef.current,
|
||||
appState,
|
||||
window.devicePixelRatio,
|
||||
() => forceRefresh((prev) => !prev),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const [latestShareableLink, setLatestShareableLink] = useState<string | null>(
|
||||
|
@ -817,15 +894,6 @@ const ExcalidrawWrapper = () => {
|
|||
);
|
||||
};
|
||||
|
||||
const onLibraryChange = async (items: LibraryItems) => {
|
||||
if (!items.length) {
|
||||
localStorage.removeItem(STORAGE_KEYS.LOCAL_STORAGE_LIBRARY);
|
||||
return;
|
||||
}
|
||||
const serializedItems = JSON.stringify(items);
|
||||
localStorage.setItem(STORAGE_KEYS.LOCAL_STORAGE_LIBRARY, serializedItems);
|
||||
};
|
||||
|
||||
const isOffline = useAtomValue(isOfflineAtom);
|
||||
|
||||
const onCollabDialogOpen = useCallback(
|
||||
|
@ -877,6 +945,45 @@ const ExcalidrawWrapper = () => {
|
|||
);
|
||||
}
|
||||
|
||||
const ExcalidrawPlusCommand = {
|
||||
label: "Excalidraw+",
|
||||
category: DEFAULT_CATEGORIES.links,
|
||||
predicate: true,
|
||||
icon: <div style={{ width: 14 }}>{ExcalLogo}</div>,
|
||||
keywords: ["plus", "cloud", "server"],
|
||||
perform: () => {
|
||||
window.open(
|
||||
`${
|
||||
import.meta.env.VITE_APP_PLUS_LP
|
||||
}/plus?utm_source=excalidraw&utm_medium=app&utm_content=command_palette`,
|
||||
"_blank",
|
||||
);
|
||||
},
|
||||
};
|
||||
const ExcalidrawPlusAppCommand = {
|
||||
label: "Sign up",
|
||||
category: DEFAULT_CATEGORIES.links,
|
||||
predicate: true,
|
||||
icon: <div style={{ width: 14 }}>{ExcalLogo}</div>,
|
||||
keywords: [
|
||||
"excalidraw",
|
||||
"plus",
|
||||
"cloud",
|
||||
"server",
|
||||
"signin",
|
||||
"login",
|
||||
"signup",
|
||||
],
|
||||
perform: () => {
|
||||
window.open(
|
||||
`${
|
||||
import.meta.env.VITE_APP_PLUS_APP
|
||||
}?utm_source=excalidraw&utm_medium=app&utm_content=command_palette`,
|
||||
"_blank",
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{ height: "100%" }}
|
||||
|
@ -895,12 +1002,14 @@ const ExcalidrawWrapper = () => {
|
|||
toggleTheme: true,
|
||||
export: {
|
||||
onExportToBackend,
|
||||
renderCustomUI: (elements, appState, files) => {
|
||||
renderCustomUI: excalidrawAPI
|
||||
? (elements, appState, files) => {
|
||||
return (
|
||||
<ExportToExcalidrawPlus
|
||||
elements={elements}
|
||||
appState={appState}
|
||||
files={files}
|
||||
name={excalidrawAPI.getName()}
|
||||
onError={(error) => {
|
||||
excalidrawAPI?.updateScene({
|
||||
appState: {
|
||||
|
@ -909,13 +1018,14 @@ const ExcalidrawWrapper = () => {
|
|||
});
|
||||
}}
|
||||
onSuccess={() => {
|
||||
excalidrawAPI?.updateScene({
|
||||
excalidrawAPI.updateScene({
|
||||
appState: { openDialog: null },
|
||||
});
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
},
|
||||
}}
|
||||
|
@ -923,23 +1033,31 @@ const ExcalidrawWrapper = () => {
|
|||
renderCustomStats={renderCustomStats}
|
||||
detectScroll={false}
|
||||
handleKeyboardGlobally={true}
|
||||
onLibraryChange={onLibraryChange}
|
||||
autoFocus={true}
|
||||
theme={theme}
|
||||
theme={editorTheme}
|
||||
renderTopRightUI={(isMobile) => {
|
||||
if (isMobile || !collabAPI || isCollabDisabled) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div className="top-right-ui">
|
||||
{collabError.message && <CollabError collabError={collabError} />}
|
||||
<LiveCollaborationTrigger
|
||||
isCollaborating={isCollaborating}
|
||||
onSelect={() =>
|
||||
setShareDialogState({ isOpen: true, type: "share" })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
scrollConstraints={constraints.enabled ? constraints : undefined}
|
||||
onLinkOpen={(element, event) => {
|
||||
if (element.link && isElementLink(element.link)) {
|
||||
event.preventDefault();
|
||||
excalidrawAPI?.scrollToContent(element.link, { animate: true });
|
||||
}
|
||||
}}
|
||||
>
|
||||
{excalidrawAPI && (
|
||||
<ConstraintsSettings
|
||||
|
@ -951,6 +1069,9 @@ const ExcalidrawWrapper = () => {
|
|||
onCollabDialogOpen={onCollabDialogOpen}
|
||||
isCollaborating={isCollaborating}
|
||||
isCollabEnabled={!isCollabDisabled}
|
||||
theme={appTheme}
|
||||
setTheme={(theme) => setAppTheme(theme)}
|
||||
refresh={() => forceRefresh((prev) => !prev)}
|
||||
/>
|
||||
<AppWelcomeScreen
|
||||
onCollabDialogOpen={onCollabDialogOpen}
|
||||
|
@ -968,6 +1089,7 @@ const ExcalidrawWrapper = () => {
|
|||
excalidrawAPI.getSceneElements(),
|
||||
excalidrawAPI.getAppState(),
|
||||
excalidrawAPI.getFiles(),
|
||||
excalidrawAPI.getName(),
|
||||
);
|
||||
}}
|
||||
>
|
||||
|
@ -975,64 +1097,9 @@ const ExcalidrawWrapper = () => {
|
|||
</OverwriteConfirmDialog.Action>
|
||||
)}
|
||||
</OverwriteConfirmDialog>
|
||||
<AppFooter />
|
||||
<TTDDialog
|
||||
onTextSubmit={async (input) => {
|
||||
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 }),
|
||||
},
|
||||
);
|
||||
<AppFooter onChange={() => excalidrawAPI?.refresh()} />
|
||||
{excalidrawAPI && <AIComponents excalidrawAPI={excalidrawAPI} />}
|
||||
|
||||
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");
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<TTDDialogTrigger />
|
||||
{isCollaborating && isOffline && (
|
||||
<div className="collab-offline-warning">
|
||||
|
@ -1072,15 +1139,218 @@ const ExcalidrawWrapper = () => {
|
|||
{errorMessage}
|
||||
</ErrorDialog>
|
||||
)}
|
||||
|
||||
<CommandPalette
|
||||
customCommandPaletteItems={[
|
||||
{
|
||||
label: t("labels.liveCollaboration"),
|
||||
category: DEFAULT_CATEGORIES.app,
|
||||
keywords: [
|
||||
"team",
|
||||
"multiplayer",
|
||||
"share",
|
||||
"public",
|
||||
"session",
|
||||
"invite",
|
||||
],
|
||||
icon: usersIcon,
|
||||
perform: () => {
|
||||
setShareDialogState({
|
||||
isOpen: true,
|
||||
type: "collaborationOnly",
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
label: t("roomDialog.button_stopSession"),
|
||||
category: DEFAULT_CATEGORIES.app,
|
||||
predicate: () => !!collabAPI?.isCollaborating(),
|
||||
keywords: [
|
||||
"stop",
|
||||
"session",
|
||||
"end",
|
||||
"leave",
|
||||
"close",
|
||||
"exit",
|
||||
"collaboration",
|
||||
],
|
||||
perform: () => {
|
||||
if (collabAPI) {
|
||||
collabAPI.stopCollaboration();
|
||||
if (!collabAPI.isCollaborating()) {
|
||||
setShareDialogState({ isOpen: false });
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
label: t("labels.share"),
|
||||
category: DEFAULT_CATEGORIES.app,
|
||||
predicate: true,
|
||||
icon: share,
|
||||
keywords: [
|
||||
"link",
|
||||
"shareable",
|
||||
"readonly",
|
||||
"export",
|
||||
"publish",
|
||||
"snapshot",
|
||||
"url",
|
||||
"collaborate",
|
||||
"invite",
|
||||
],
|
||||
perform: async () => {
|
||||
setShareDialogState({ isOpen: true, type: "share" });
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "GitHub",
|
||||
icon: GithubIcon,
|
||||
category: DEFAULT_CATEGORIES.links,
|
||||
predicate: true,
|
||||
keywords: [
|
||||
"issues",
|
||||
"bugs",
|
||||
"requests",
|
||||
"report",
|
||||
"features",
|
||||
"social",
|
||||
"community",
|
||||
],
|
||||
perform: () => {
|
||||
window.open(
|
||||
"https://github.com/excalidraw/excalidraw",
|
||||
"_blank",
|
||||
"noopener noreferrer",
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: t("labels.followUs"),
|
||||
icon: XBrandIcon,
|
||||
category: DEFAULT_CATEGORIES.links,
|
||||
predicate: true,
|
||||
keywords: ["twitter", "contact", "social", "community"],
|
||||
perform: () => {
|
||||
window.open(
|
||||
"https://x.com/excalidraw",
|
||||
"_blank",
|
||||
"noopener noreferrer",
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: t("labels.discordChat"),
|
||||
category: DEFAULT_CATEGORIES.links,
|
||||
predicate: true,
|
||||
icon: DiscordIcon,
|
||||
keywords: [
|
||||
"chat",
|
||||
"talk",
|
||||
"contact",
|
||||
"bugs",
|
||||
"requests",
|
||||
"report",
|
||||
"feedback",
|
||||
"suggestions",
|
||||
"social",
|
||||
"community",
|
||||
],
|
||||
perform: () => {
|
||||
window.open(
|
||||
"https://discord.gg/UexuTaE",
|
||||
"_blank",
|
||||
"noopener noreferrer",
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "YouTube",
|
||||
icon: youtubeIcon,
|
||||
category: DEFAULT_CATEGORIES.links,
|
||||
predicate: true,
|
||||
keywords: ["features", "tutorials", "howto", "help", "community"],
|
||||
perform: () => {
|
||||
window.open(
|
||||
"https://youtube.com/@excalidraw",
|
||||
"_blank",
|
||||
"noopener noreferrer",
|
||||
);
|
||||
},
|
||||
},
|
||||
...(isExcalidrawPlusSignedUser
|
||||
? [
|
||||
{
|
||||
...ExcalidrawPlusAppCommand,
|
||||
label: "Sign in / Go to Excalidraw+",
|
||||
},
|
||||
]
|
||||
: [ExcalidrawPlusCommand, ExcalidrawPlusAppCommand]),
|
||||
|
||||
{
|
||||
label: t("overwriteConfirm.action.excalidrawPlus.button"),
|
||||
category: DEFAULT_CATEGORIES.export,
|
||||
icon: exportToPlus,
|
||||
predicate: true,
|
||||
keywords: ["plus", "export", "save", "backup"],
|
||||
perform: () => {
|
||||
if (excalidrawAPI) {
|
||||
exportToExcalidrawPlus(
|
||||
excalidrawAPI.getSceneElements(),
|
||||
excalidrawAPI.getAppState(),
|
||||
excalidrawAPI.getFiles(),
|
||||
excalidrawAPI.getName(),
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
...CommandPalette.defaultItems.toggleTheme,
|
||||
perform: () => {
|
||||
setAppTheme(
|
||||
editorTheme === THEME.DARK ? THEME.LIGHT : THEME.DARK,
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: t("labels.installPWA"),
|
||||
category: DEFAULT_CATEGORIES.app,
|
||||
predicate: () => !!pwaEvent,
|
||||
perform: () => {
|
||||
if (pwaEvent) {
|
||||
pwaEvent.prompt();
|
||||
pwaEvent.userChoice.then(() => {
|
||||
// event cannot be reused, but we'll hopefully
|
||||
// grab new one as the event should be fired again
|
||||
pwaEvent = null;
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
{isVisualDebuggerEnabled() && excalidrawAPI && (
|
||||
<DebugCanvas
|
||||
appState={excalidrawAPI.getAppState()}
|
||||
scale={window.devicePixelRatio}
|
||||
ref={debugCanvasRef}
|
||||
/>
|
||||
)}
|
||||
</Excalidraw>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const ExcalidrawApp = () => {
|
||||
const isCloudExportWindow =
|
||||
window.location.pathname === "/excalidraw-plus-export";
|
||||
if (isCloudExportWindow) {
|
||||
return <ExcalidrawPlusIframeExport />;
|
||||
}
|
||||
|
||||
return (
|
||||
<TopErrorBoundary>
|
||||
<Provider unstable_createStore={() => appJotaiStore}>
|
||||
<Provider store={appJotaiStore}>
|
||||
<ExcalidrawWrapper />
|
||||
</Provider>
|
||||
</TopErrorBoundary>
|
||||
|
|
|
@ -1,14 +1,21 @@
|
|||
import { Stats } from "@excalidraw/excalidraw";
|
||||
import { copyTextToSystemClipboard } from "@excalidraw/excalidraw/clipboard";
|
||||
import {
|
||||
DEFAULT_VERSION,
|
||||
debounce,
|
||||
getVersion,
|
||||
nFormatter,
|
||||
} from "@excalidraw/common";
|
||||
import { t } from "@excalidraw/excalidraw/i18n";
|
||||
import { useEffect, useState } from "react";
|
||||
import { debounce, getVersion, nFormatter } from "../packages/excalidraw/utils";
|
||||
|
||||
import type { NonDeletedExcalidrawElement } from "@excalidraw/element/types";
|
||||
import type { UIAppState } from "@excalidraw/excalidraw/types";
|
||||
|
||||
import {
|
||||
getElementsStorageSize,
|
||||
getTotalStorageSize,
|
||||
} from "./data/localStorage";
|
||||
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 };
|
||||
|
||||
|
@ -51,24 +58,9 @@ const CustomStats = (props: Props) => {
|
|||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<tr>
|
||||
<th colSpan={2}>{t("stats.storage")}</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{t("stats.scene")}</td>
|
||||
<td>{nFormatter(storageSizes.scene, 1)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{t("stats.total")}</td>
|
||||
<td>{nFormatter(storageSizes.total, 1)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th colSpan={2}>{t("stats.version")}</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td
|
||||
colSpan={2}
|
||||
<Stats.StatsRows order={-1}>
|
||||
<Stats.StatsRow heading>{t("stats.version")}</Stats.StatsRow>
|
||||
<Stats.StatsRow
|
||||
style={{ textAlign: "center", cursor: "pointer" }}
|
||||
onClick={async () => {
|
||||
try {
|
||||
|
@ -81,9 +73,18 @@ const CustomStats = (props: Props) => {
|
|||
{timestamp}
|
||||
<br />
|
||||
{hash}
|
||||
</td>
|
||||
</tr>
|
||||
</>
|
||||
</Stats.StatsRow>
|
||||
|
||||
<Stats.StatsRow heading>{t("stats.storage")}</Stats.StatsRow>
|
||||
<Stats.StatsRow columns={2}>
|
||||
<div>{t("stats.scene")}</div>
|
||||
<div>{nFormatter(storageSizes.scene, 1)}</div>
|
||||
</Stats.StatsRow>
|
||||
<Stats.StatsRow columns={2}>
|
||||
<div>{t("stats.total")}</div>
|
||||
<div>{nFormatter(storageSizes.total, 1)}</div>
|
||||
</Stats.StatsRow>
|
||||
</Stats.StatsRows>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
224
excalidraw-app/ExcalidrawPlusIframeExport.tsx
Normal file
|
@ -0,0 +1,224 @@
|
|||
import { base64urlToString } from "@excalidraw/excalidraw/data/encode";
|
||||
import { ExcalidrawError } from "@excalidraw/excalidraw/errors";
|
||||
import { useLayoutEffect, useRef } from "react";
|
||||
|
||||
import type {
|
||||
FileId,
|
||||
OrderedExcalidrawElement,
|
||||
} from "@excalidraw/element/types";
|
||||
import type { AppState, BinaryFileData } from "@excalidraw/excalidraw/types";
|
||||
|
||||
import { STORAGE_KEYS } from "./app_constants";
|
||||
import { LocalData } from "./data/LocalData";
|
||||
|
||||
const EVENT_REQUEST_SCENE = "REQUEST_SCENE";
|
||||
|
||||
const EXCALIDRAW_PLUS_ORIGIN = import.meta.env.VITE_APP_PLUS_APP;
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// outgoing message
|
||||
// -----------------------------------------------------------------------------
|
||||
type MESSAGE_REQUEST_SCENE = {
|
||||
type: "REQUEST_SCENE";
|
||||
jwt: string;
|
||||
};
|
||||
|
||||
type MESSAGE_FROM_PLUS = MESSAGE_REQUEST_SCENE;
|
||||
|
||||
// incoming messages
|
||||
// -----------------------------------------------------------------------------
|
||||
type MESSAGE_READY = { type: "READY" };
|
||||
type MESSAGE_ERROR = { type: "ERROR"; message: string };
|
||||
type MESSAGE_SCENE_DATA = {
|
||||
type: "SCENE_DATA";
|
||||
elements: OrderedExcalidrawElement[];
|
||||
appState: Pick<AppState, "viewBackgroundColor">;
|
||||
files: { loadedFiles: BinaryFileData[]; erroredFiles: Map<FileId, true> };
|
||||
};
|
||||
|
||||
type MESSAGE_FROM_EDITOR = MESSAGE_ERROR | MESSAGE_SCENE_DATA | MESSAGE_READY;
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
const parseSceneData = async ({
|
||||
rawElementsString,
|
||||
rawAppStateString,
|
||||
}: {
|
||||
rawElementsString: string | null;
|
||||
rawAppStateString: string | null;
|
||||
}): Promise<MESSAGE_SCENE_DATA> => {
|
||||
if (!rawElementsString || !rawAppStateString) {
|
||||
throw new ExcalidrawError("Elements or appstate is missing.");
|
||||
}
|
||||
|
||||
try {
|
||||
const elements = JSON.parse(
|
||||
rawElementsString,
|
||||
) as OrderedExcalidrawElement[];
|
||||
|
||||
if (!elements.length) {
|
||||
throw new ExcalidrawError("Scene is empty, nothing to export.");
|
||||
}
|
||||
|
||||
const appState = JSON.parse(rawAppStateString) as Pick<
|
||||
AppState,
|
||||
"viewBackgroundColor"
|
||||
>;
|
||||
|
||||
const fileIds = elements.reduce((acc, el) => {
|
||||
if ("fileId" in el && el.fileId) {
|
||||
acc.push(el.fileId);
|
||||
}
|
||||
return acc;
|
||||
}, [] as FileId[]);
|
||||
|
||||
const files = await LocalData.fileStorage.getFiles(fileIds);
|
||||
|
||||
return {
|
||||
type: "SCENE_DATA",
|
||||
elements,
|
||||
appState,
|
||||
files,
|
||||
};
|
||||
} catch (error: any) {
|
||||
throw error instanceof ExcalidrawError
|
||||
? error
|
||||
: new ExcalidrawError("Failed to parse scene data.");
|
||||
}
|
||||
};
|
||||
|
||||
const verifyJWT = async ({
|
||||
token,
|
||||
publicKey,
|
||||
}: {
|
||||
token: string;
|
||||
publicKey: string;
|
||||
}) => {
|
||||
try {
|
||||
if (!publicKey) {
|
||||
throw new ExcalidrawError("Public key is undefined");
|
||||
}
|
||||
|
||||
const [header, payload, signature] = token.split(".");
|
||||
|
||||
if (!header || !payload || !signature) {
|
||||
throw new ExcalidrawError("Invalid JWT format");
|
||||
}
|
||||
|
||||
// JWT is using Base64URL encoding
|
||||
const decodedPayload = base64urlToString(payload);
|
||||
const decodedSignature = base64urlToString(signature);
|
||||
|
||||
const data = `${header}.${payload}`;
|
||||
const signatureArrayBuffer = Uint8Array.from(decodedSignature, (c) =>
|
||||
c.charCodeAt(0),
|
||||
);
|
||||
|
||||
const keyData = publicKey.replace(/-----\w+ PUBLIC KEY-----/g, "");
|
||||
const keyArrayBuffer = Uint8Array.from(atob(keyData), (c) =>
|
||||
c.charCodeAt(0),
|
||||
);
|
||||
|
||||
const key = await crypto.subtle.importKey(
|
||||
"spki",
|
||||
keyArrayBuffer,
|
||||
{ name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" },
|
||||
true,
|
||||
["verify"],
|
||||
);
|
||||
|
||||
const isValid = await crypto.subtle.verify(
|
||||
"RSASSA-PKCS1-v1_5",
|
||||
key,
|
||||
signatureArrayBuffer,
|
||||
new TextEncoder().encode(data),
|
||||
);
|
||||
|
||||
if (!isValid) {
|
||||
throw new Error("Invalid JWT");
|
||||
}
|
||||
|
||||
const parsedPayload = JSON.parse(decodedPayload);
|
||||
|
||||
// Check for expiration
|
||||
const currentTime = Math.floor(Date.now() / 1000);
|
||||
if (parsedPayload.exp && parsedPayload.exp < currentTime) {
|
||||
throw new Error("JWT has expired");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to verify JWT:", error);
|
||||
throw new Error(error instanceof Error ? error.message : "Invalid JWT");
|
||||
}
|
||||
};
|
||||
|
||||
export const ExcalidrawPlusIframeExport = () => {
|
||||
const readyRef = useRef(false);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const handleMessage = async (event: MessageEvent<MESSAGE_FROM_PLUS>) => {
|
||||
if (event.origin !== EXCALIDRAW_PLUS_ORIGIN) {
|
||||
throw new ExcalidrawError("Invalid origin");
|
||||
}
|
||||
|
||||
if (event.data.type === EVENT_REQUEST_SCENE) {
|
||||
if (!event.data.jwt) {
|
||||
throw new ExcalidrawError("JWT is missing");
|
||||
}
|
||||
|
||||
try {
|
||||
try {
|
||||
await verifyJWT({
|
||||
token: event.data.jwt,
|
||||
publicKey: import.meta.env.VITE_APP_PLUS_EXPORT_PUBLIC_KEY,
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error(`Failed to verify JWT: ${error.message}`);
|
||||
throw new ExcalidrawError("Failed to verify JWT");
|
||||
}
|
||||
|
||||
const parsedSceneData: MESSAGE_SCENE_DATA = await parseSceneData({
|
||||
rawAppStateString: localStorage.getItem(
|
||||
STORAGE_KEYS.LOCAL_STORAGE_APP_STATE,
|
||||
),
|
||||
rawElementsString: localStorage.getItem(
|
||||
STORAGE_KEYS.LOCAL_STORAGE_ELEMENTS,
|
||||
),
|
||||
});
|
||||
|
||||
event.source!.postMessage(parsedSceneData, {
|
||||
targetOrigin: EXCALIDRAW_PLUS_ORIGIN,
|
||||
});
|
||||
} catch (error) {
|
||||
const responseData: MESSAGE_ERROR = {
|
||||
type: "ERROR",
|
||||
message:
|
||||
error instanceof ExcalidrawError
|
||||
? error.message
|
||||
: "Failed to export scene data",
|
||||
};
|
||||
event.source!.postMessage(responseData, {
|
||||
targetOrigin: EXCALIDRAW_PLUS_ORIGIN,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("message", handleMessage);
|
||||
|
||||
// so we don't send twice in StrictMode
|
||||
if (!readyRef.current) {
|
||||
readyRef.current = true;
|
||||
const message: MESSAGE_FROM_EDITOR = { type: "READY" };
|
||||
window.parent.postMessage(message, EXCALIDRAW_PLUS_ORIGIN);
|
||||
}
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("message", handleMessage);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Since this component is expected to run in a hidden iframe on Excaildraw+,
|
||||
// it doesn't need to render anything. All the data we need is available in
|
||||
// LocalStorage and IndexedDB. It only needs to handle the messaging between
|
||||
// the parent window and the iframe with the relevant data.
|
||||
return null;
|
||||
};
|
|
@ -1,3 +1,37 @@
|
|||
import { unstable_createStore } from "jotai";
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import {
|
||||
atom,
|
||||
Provider,
|
||||
useAtom,
|
||||
useAtomValue,
|
||||
useSetAtom,
|
||||
createStore,
|
||||
type PrimitiveAtom,
|
||||
} from "jotai";
|
||||
import { useLayoutEffect } from "react";
|
||||
|
||||
export const appJotaiStore = unstable_createStore();
|
||||
export const appJotaiStore = createStore();
|
||||
|
||||
export { atom, Provider, useAtom, useAtomValue, useSetAtom };
|
||||
|
||||
export const useAtomWithInitialValue = <
|
||||
T extends unknown,
|
||||
A extends PrimitiveAtom<T>,
|
||||
>(
|
||||
atom: A,
|
||||
initialValue: T | (() => T),
|
||||
) => {
|
||||
const [value, setValue] = useAtom(atom);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (typeof initialValue === "function") {
|
||||
// @ts-ignore
|
||||
setValue(initialValue());
|
||||
} else {
|
||||
setValue(initialValue);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return [value, setValue] as const;
|
||||
};
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import { useSetAtom } from "jotai";
|
||||
import { useI18n, languages } from "@excalidraw/excalidraw/i18n";
|
||||
import React from "react";
|
||||
import { appLangCodeAtom } from "../App";
|
||||
import { useI18n } from "../../packages/excalidraw/i18n";
|
||||
import { languages } from "../../packages/excalidraw/i18n";
|
||||
|
||||
import { useSetAtom } from "../app-jotai";
|
||||
|
||||
import { appLangCodeAtom } from "./language-state";
|
||||
|
||||
export const LanguageList = ({ style }: { style?: React.CSSProperties }) => {
|
||||
const { t, langCode } = useI18n();
|
25
excalidraw-app/app-language/language-detector.ts
Normal file
|
@ -0,0 +1,25 @@
|
|||
import { defaultLang, languages } from "@excalidraw/excalidraw";
|
||||
import LanguageDetector from "i18next-browser-languagedetector";
|
||||
|
||||
export const languageDetector = new LanguageDetector();
|
||||
|
||||
languageDetector.init({
|
||||
languageUtils: {},
|
||||
});
|
||||
|
||||
export const getPreferredLanguage = () => {
|
||||
const detectedLanguages = languageDetector.detect();
|
||||
|
||||
const detectedLanguage = Array.isArray(detectedLanguages)
|
||||
? detectedLanguages[0]
|
||||
: detectedLanguages;
|
||||
|
||||
const initialLanguage =
|
||||
(detectedLanguage
|
||||
? // region code may not be defined if user uses generic preferred language
|
||||
// (e.g. chinese vs instead of chinese-simplified)
|
||||
languages.find((lang) => lang.code.startsWith(detectedLanguage))?.code
|
||||
: null) || defaultLang.code;
|
||||
|
||||
return initialLanguage;
|
||||
};
|
17
excalidraw-app/app-language/language-state.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
import { useEffect } from "react";
|
||||
|
||||
import { atom, useAtom } from "../app-jotai";
|
||||
|
||||
import { getPreferredLanguage, languageDetector } from "./language-detector";
|
||||
|
||||
export const appLangCodeAtom = atom(getPreferredLanguage());
|
||||
|
||||
export const useAppLangCode = () => {
|
||||
const [langCode, setLangCode] = useAtom(appLangCodeAtom);
|
||||
|
||||
useEffect(() => {
|
||||
languageDetector.cacheUserLanguage(langCode);
|
||||
}, [langCode]);
|
||||
|
||||
return [langCode, setLangCode] as const;
|
||||
};
|
|
@ -39,10 +39,15 @@ export const STORAGE_KEYS = {
|
|||
LOCAL_STORAGE_ELEMENTS: "excalidraw",
|
||||
LOCAL_STORAGE_APP_STATE: "excalidraw-state",
|
||||
LOCAL_STORAGE_COLLAB: "excalidraw-collab",
|
||||
LOCAL_STORAGE_LIBRARY: "excalidraw-library",
|
||||
LOCAL_STORAGE_THEME: "excalidraw-theme",
|
||||
LOCAL_STORAGE_DEBUG: "excalidraw-debug",
|
||||
VERSION_DATA_STATE: "version-dataState",
|
||||
VERSION_FILES: "version-files",
|
||||
|
||||
IDB_LIBRARY: "excalidraw-library",
|
||||
|
||||
// do not use apart from migrations
|
||||
__LEGACY_LOCAL_STORAGE_LIBRARY: "excalidraw-library",
|
||||
} as const;
|
||||
|
||||
export const COOKIES = {
|
||||
|
|
|
@ -1,28 +1,58 @@
|
|||
import throttle from "lodash.throttle";
|
||||
import { PureComponent } from "react";
|
||||
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 "../../packages/excalidraw/element/types";
|
||||
import {
|
||||
CaptureUpdateAction,
|
||||
getSceneVersion,
|
||||
restoreElements,
|
||||
zoomToFitBounds,
|
||||
} from "../../packages/excalidraw/index";
|
||||
import { Collaborator, Gesture } from "../../packages/excalidraw/types";
|
||||
reconcileElements,
|
||||
} from "@excalidraw/excalidraw";
|
||||
import { ErrorDialog } from "@excalidraw/excalidraw/components/ErrorDialog";
|
||||
import { APP_NAME, EVENT } from "@excalidraw/common";
|
||||
import {
|
||||
IDLE_THRESHOLD,
|
||||
ACTIVE_THRESHOLD,
|
||||
UserIdleState,
|
||||
assertNever,
|
||||
isDevEnv,
|
||||
isTestEnv,
|
||||
preventUnload,
|
||||
resolvablePromise,
|
||||
throttleRAF,
|
||||
} from "../../packages/excalidraw/utils";
|
||||
} from "@excalidraw/common";
|
||||
import { decryptData } from "@excalidraw/excalidraw/data/encryption";
|
||||
import { getVisibleSceneBounds } from "@excalidraw/element/bounds";
|
||||
import { newElementWith } from "@excalidraw/element/mutateElement";
|
||||
import {
|
||||
isImageElement,
|
||||
isInitializedImageElement,
|
||||
} from "@excalidraw/element/typeChecks";
|
||||
import { AbortError } from "@excalidraw/excalidraw/errors";
|
||||
import { t } from "@excalidraw/excalidraw/i18n";
|
||||
import { withBatchedUpdates } from "@excalidraw/excalidraw/reactUtils";
|
||||
|
||||
import throttle from "lodash.throttle";
|
||||
import { PureComponent } from "react";
|
||||
|
||||
import type {
|
||||
ReconciledExcalidrawElement,
|
||||
RemoteExcalidrawElement,
|
||||
} from "@excalidraw/excalidraw/data/reconcile";
|
||||
import type { ImportedDataState } from "@excalidraw/excalidraw/data/types";
|
||||
import type {
|
||||
ExcalidrawElement,
|
||||
FileId,
|
||||
InitializedExcalidrawImageElement,
|
||||
OrderedExcalidrawElement,
|
||||
} from "@excalidraw/element/types";
|
||||
import type {
|
||||
BinaryFileData,
|
||||
ExcalidrawImperativeAPI,
|
||||
SocketId,
|
||||
Collaborator,
|
||||
Gesture,
|
||||
} from "@excalidraw/excalidraw/types";
|
||||
import type { Mutable, ValueOf } from "@excalidraw/common/utility-types";
|
||||
|
||||
import { appJotaiStore, atom } from "../app-jotai";
|
||||
import {
|
||||
CURSOR_SYNC_TIMEOUT,
|
||||
FILE_UPLOAD_MAX_BYTES,
|
||||
|
@ -37,9 +67,13 @@ import {
|
|||
generateCollaborationLinkData,
|
||||
getCollaborationLink,
|
||||
getSyncableElements,
|
||||
SocketUpdateDataSource,
|
||||
SyncableExcalidrawElement,
|
||||
} from "../data";
|
||||
import {
|
||||
encodeFilesForUpload,
|
||||
FileManager,
|
||||
updateStaleImageStatuses,
|
||||
} from "../data/FileManager";
|
||||
import { LocalData } from "../data/LocalData";
|
||||
import {
|
||||
isSavedToFirebase,
|
||||
loadFilesFromFirebase,
|
||||
|
@ -51,36 +85,15 @@ import {
|
|||
importUsernameFromLocalStorage,
|
||||
saveUsernameToLocalStorage,
|
||||
} from "../data/localStorage";
|
||||
import Portal from "./Portal";
|
||||
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 "../../packages/excalidraw/errors";
|
||||
import {
|
||||
isImageElement,
|
||||
isInitializedImageElement,
|
||||
} from "../../packages/excalidraw/element/typeChecks";
|
||||
import { newElementWith } from "../../packages/excalidraw/element/mutateElement";
|
||||
import {
|
||||
ReconciledElements,
|
||||
reconcileElements as _reconcileElements,
|
||||
} from "./reconciliation";
|
||||
import { decryptData } from "../../packages/excalidraw/data/encryption";
|
||||
import { resetBrowserStateVersions } from "../data/tabSync";
|
||||
import { LocalData } from "../data/LocalData";
|
||||
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";
|
||||
|
||||
import { collabErrorIndicatorAtom } from "./CollabError";
|
||||
import Portal from "./Portal";
|
||||
|
||||
import type {
|
||||
SocketUpdateDataSource,
|
||||
SyncableExcalidrawElement,
|
||||
} from "../data";
|
||||
|
||||
export const collabAPIAtom = atom<CollabAPI | null>(null);
|
||||
export const isCollaboratingAtom = atom(false);
|
||||
|
@ -88,6 +101,8 @@ export const isOfflineAtom = atom(false);
|
|||
|
||||
interface CollabState {
|
||||
errorMessage: string | null;
|
||||
/** errors related to saving */
|
||||
dialogNotifiedErrors: Record<string, boolean>;
|
||||
username: string;
|
||||
activeRoomLink: string | null;
|
||||
}
|
||||
|
@ -107,7 +122,7 @@ export interface CollabAPI {
|
|||
setUsername: CollabInstance["setUsername"];
|
||||
getUsername: CollabInstance["getUsername"];
|
||||
getActiveRoomLink: CollabInstance["getActiveRoomLink"];
|
||||
setErrorMessage: CollabInstance["setErrorMessage"];
|
||||
setCollabError: CollabInstance["setErrorDialog"];
|
||||
}
|
||||
|
||||
interface CollabProps {
|
||||
|
@ -129,6 +144,7 @@ class Collab extends PureComponent<CollabProps, CollabState> {
|
|||
super(props);
|
||||
this.state = {
|
||||
errorMessage: null,
|
||||
dialogNotifiedErrors: {},
|
||||
username: importUsernameFromLocalStorage() || "",
|
||||
activeRoomLink: null,
|
||||
};
|
||||
|
@ -148,7 +164,7 @@ class Collab extends PureComponent<CollabProps, CollabState> {
|
|||
throw new AbortError();
|
||||
}
|
||||
|
||||
return saveFilesToFirebase({
|
||||
const { savedFiles, erroredFiles } = await saveFilesToFirebase({
|
||||
prefix: `${FIREBASE_STORAGE_PREFIXES.collabFiles}/${roomId}`,
|
||||
files: await encodeFilesForUpload({
|
||||
files: addedFiles,
|
||||
|
@ -156,6 +172,29 @@ class Collab extends PureComponent<CollabProps, CollabState> {
|
|||
maxBytes: FILE_UPLOAD_MAX_BYTES,
|
||||
}),
|
||||
});
|
||||
|
||||
return {
|
||||
savedFiles: savedFiles.reduce(
|
||||
(acc: Map<FileId, BinaryFileData>, id) => {
|
||||
const fileData = addedFiles.get(id);
|
||||
if (fileData) {
|
||||
acc.set(id, fileData);
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
new Map(),
|
||||
),
|
||||
erroredFiles: erroredFiles.reduce(
|
||||
(acc: Map<FileId, BinaryFileData>, id) => {
|
||||
const fileData = addedFiles.get(id);
|
||||
if (fileData) {
|
||||
acc.set(id, fileData);
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
new Map(),
|
||||
),
|
||||
};
|
||||
},
|
||||
});
|
||||
this.excalidrawAPI = props.excalidrawAPI;
|
||||
|
@ -197,12 +236,12 @@ class Collab extends PureComponent<CollabProps, CollabState> {
|
|||
setUsername: this.setUsername,
|
||||
getUsername: this.getUsername,
|
||||
getActiveRoomLink: this.getActiveRoomLink,
|
||||
setErrorMessage: this.setErrorMessage,
|
||||
setCollabError: this.setErrorDialog,
|
||||
};
|
||||
|
||||
appJotaiStore.set(collabAPIAtom, collabAPI);
|
||||
|
||||
if (import.meta.env.MODE === ENV.TEST || import.meta.env.DEV) {
|
||||
if (isTestEnv() || isDevEnv()) {
|
||||
window.collab = window.collab || ({} as Window["collab"]);
|
||||
Object.defineProperties(window, {
|
||||
collab: {
|
||||
|
@ -262,7 +301,13 @@ class Collab extends PureComponent<CollabProps, CollabState> {
|
|||
// the purpose is to run in immediately after user decides to stay
|
||||
this.saveCollabRoomToFirebase(syncableElements);
|
||||
|
||||
if (import.meta.env.VITE_APP_DISABLE_PREVENT_UNLOAD !== "true") {
|
||||
preventUnload(event);
|
||||
} else {
|
||||
console.warn(
|
||||
"preventing unload disabled (VITE_APP_DISABLE_PREVENT_UNLOAD)",
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -270,24 +315,39 @@ class Collab extends PureComponent<CollabProps, CollabState> {
|
|||
syncableElements: readonly SyncableExcalidrawElement[],
|
||||
) => {
|
||||
try {
|
||||
const savedData = await saveToFirebase(
|
||||
const storedElements = await saveToFirebase(
|
||||
this.portal,
|
||||
syncableElements,
|
||||
this.excalidrawAPI.getAppState(),
|
||||
);
|
||||
|
||||
if (this.isCollaborating() && savedData && savedData.reconciledElements) {
|
||||
this.handleRemoteSceneUpdate(
|
||||
this.reconcileElements(savedData.reconciledElements),
|
||||
);
|
||||
this.resetErrorIndicator();
|
||||
|
||||
if (this.isCollaborating() && storedElements) {
|
||||
this.handleRemoteSceneUpdate(this._reconcileElements(storedElements));
|
||||
}
|
||||
} catch (error: any) {
|
||||
this.setState({
|
||||
// firestore doesn't return a specific error code when size exceeded
|
||||
errorMessage: /is longer than.*?bytes/.test(error.message)
|
||||
const errorMessage = /is longer than.*?bytes/.test(error.message)
|
||||
? t("errors.collabSaveFailed_sizeExceeded")
|
||||
: t("errors.collabSaveFailed"),
|
||||
: t("errors.collabSaveFailed");
|
||||
|
||||
if (
|
||||
!this.state.dialogNotifiedErrors[errorMessage] ||
|
||||
!this.isCollaborating()
|
||||
) {
|
||||
this.setErrorDialog(errorMessage);
|
||||
this.setState({
|
||||
dialogNotifiedErrors: {
|
||||
...this.state.dialogNotifiedErrors,
|
||||
[errorMessage]: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (this.isCollaborating()) {
|
||||
this.setErrorIndicator(errorMessage);
|
||||
}
|
||||
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
@ -296,6 +356,7 @@ class Collab extends PureComponent<CollabProps, CollabState> {
|
|||
this.queueBroadcastAllElements.cancel();
|
||||
this.queueSaveToFirebase.cancel();
|
||||
this.loadImageFiles.cancel();
|
||||
this.resetErrorIndicator(true);
|
||||
|
||||
this.saveCollabRoomToFirebase(
|
||||
getSyncableElements(
|
||||
|
@ -334,7 +395,7 @@ class Collab extends PureComponent<CollabProps, CollabState> {
|
|||
|
||||
this.excalidrawAPI.updateScene({
|
||||
elements,
|
||||
commitToHistory: false,
|
||||
captureUpdate: CaptureUpdateAction.NEVER,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
@ -369,7 +430,7 @@ class Collab extends PureComponent<CollabProps, CollabState> {
|
|||
.filter((element) => {
|
||||
return (
|
||||
isInitializedImageElement(element) &&
|
||||
!this.fileManager.isFileHandled(element.fileId) &&
|
||||
!this.fileManager.isFileTracked(element.fileId) &&
|
||||
!element.isDeleted &&
|
||||
(opts.forceFetchFiles
|
||||
? element.status !== "pending" ||
|
||||
|
@ -407,7 +468,7 @@ class Collab extends PureComponent<CollabProps, CollabState> {
|
|||
|
||||
startCollaboration = async (
|
||||
existingRoomLinkData: null | { roomId: string; roomKey: string },
|
||||
): Promise<ImportedDataState | null> => {
|
||||
) => {
|
||||
if (!this.state.username) {
|
||||
import("@excalidraw/random-username").then(({ getRandomUsername }) => {
|
||||
const username = getRandomUsername();
|
||||
|
@ -433,7 +494,11 @@ class Collab extends PureComponent<CollabProps, CollabState> {
|
|||
);
|
||||
}
|
||||
|
||||
const scenePromise = resolvablePromise<ImportedDataState | null>();
|
||||
// TODO: `ImportedDataState` type here seems abused
|
||||
const scenePromise = resolvablePromise<
|
||||
| (ImportedDataState & { elements: readonly OrderedExcalidrawElement[] })
|
||||
| null
|
||||
>();
|
||||
|
||||
this.setIsCollaborating(true);
|
||||
LocalData.pauseSave("collaboration");
|
||||
|
@ -464,7 +529,7 @@ class Collab extends PureComponent<CollabProps, CollabState> {
|
|||
this.portal.socket.once("connect_error", fallbackInitializationHandler);
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
this.setState({ errorMessage: error.message });
|
||||
this.setErrorDialog(error.message);
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -475,14 +540,13 @@ class Collab extends PureComponent<CollabProps, CollabState> {
|
|||
}
|
||||
return element;
|
||||
});
|
||||
// remove deleted elements from elements array & history to ensure we don't
|
||||
// remove deleted elements from elements array to ensure we don't
|
||||
// expose potentially sensitive user data in case user manually deletes
|
||||
// existing elements (or clears scene), which would otherwise be persisted
|
||||
// to database even if deleted before creating the room.
|
||||
this.excalidrawAPI.history.clear();
|
||||
this.excalidrawAPI.updateScene({
|
||||
elements,
|
||||
commitToHistory: true,
|
||||
captureUpdate: CaptureUpdateAction.NEVER,
|
||||
});
|
||||
|
||||
this.saveCollabRoomToFirebase(getSyncableElements(elements));
|
||||
|
@ -516,10 +580,9 @@ class Collab extends PureComponent<CollabProps, CollabState> {
|
|||
if (!this.portal.socketInitialized) {
|
||||
this.initializeRoom({ fetchScene: false });
|
||||
const remoteElements = decryptedData.payload.elements;
|
||||
const reconciledElements = this.reconcileElements(remoteElements);
|
||||
this.handleRemoteSceneUpdate(reconciledElements, {
|
||||
init: true,
|
||||
});
|
||||
const reconciledElements =
|
||||
this._reconcileElements(remoteElements);
|
||||
this.handleRemoteSceneUpdate(reconciledElements);
|
||||
// noop if already resolved via init from firebase
|
||||
scenePromise.resolve({
|
||||
elements: reconciledElements,
|
||||
|
@ -530,7 +593,7 @@ class Collab extends PureComponent<CollabProps, CollabState> {
|
|||
}
|
||||
case WS_SUBTYPES.UPDATE:
|
||||
this.handleRemoteSceneUpdate(
|
||||
this.reconcileElements(decryptedData.payload.elements),
|
||||
this._reconcileElements(decryptedData.payload.elements),
|
||||
);
|
||||
break;
|
||||
case WS_SUBTYPES.MOUSE_LOCATION: {
|
||||
|
@ -678,17 +741,15 @@ class Collab extends PureComponent<CollabProps, CollabState> {
|
|||
return null;
|
||||
};
|
||||
|
||||
private reconcileElements = (
|
||||
private _reconcileElements = (
|
||||
remoteElements: readonly ExcalidrawElement[],
|
||||
): ReconciledElements => {
|
||||
): ReconciledExcalidrawElement[] => {
|
||||
const localElements = this.getSceneElementsIncludingDeleted();
|
||||
const appState = this.excalidrawAPI.getAppState();
|
||||
|
||||
remoteElements = restoreElements(remoteElements, null);
|
||||
|
||||
const reconciledElements = _reconcileElements(
|
||||
const restoredRemoteElements = restoreElements(remoteElements, null);
|
||||
const reconciledElements = reconcileElements(
|
||||
localElements,
|
||||
remoteElements,
|
||||
restoredRemoteElements as RemoteExcalidrawElement[],
|
||||
appState,
|
||||
);
|
||||
|
||||
|
@ -719,20 +780,13 @@ class Collab extends PureComponent<CollabProps, CollabState> {
|
|||
}, LOAD_IMAGES_TIMEOUT);
|
||||
|
||||
private handleRemoteSceneUpdate = (
|
||||
elements: ReconciledElements,
|
||||
{ init = false }: { init?: boolean } = {},
|
||||
elements: ReconciledExcalidrawElement[],
|
||||
) => {
|
||||
this.excalidrawAPI.updateScene({
|
||||
elements,
|
||||
commitToHistory: !!init,
|
||||
captureUpdate: CaptureUpdateAction.NEVER,
|
||||
});
|
||||
|
||||
// We haven't yet implemented multiplayer undo functionality, so we clear the undo stack
|
||||
// when we receive any messages from another peer. This UX can be pretty rough -- if you
|
||||
// undo, a user makes a change, and then try to redo, your element(s) will be lost. However,
|
||||
// right now we think this is the right tradeoff.
|
||||
this.excalidrawAPI.history.clear();
|
||||
|
||||
this.loadImageFiles();
|
||||
};
|
||||
|
||||
|
@ -865,7 +919,7 @@ class Collab extends PureComponent<CollabProps, CollabState> {
|
|||
this.portal.broadcastIdleChange(userState);
|
||||
};
|
||||
|
||||
broadcastElements = (elements: readonly ExcalidrawElement[]) => {
|
||||
broadcastElements = (elements: readonly OrderedExcalidrawElement[]) => {
|
||||
if (
|
||||
getSceneVersion(elements) >
|
||||
this.getLastBroadcastedOrReceivedSceneVersion()
|
||||
|
@ -876,7 +930,7 @@ class Collab extends PureComponent<CollabProps, CollabState> {
|
|||
}
|
||||
};
|
||||
|
||||
syncElements = (elements: readonly ExcalidrawElement[]) => {
|
||||
syncElements = (elements: readonly OrderedExcalidrawElement[]) => {
|
||||
this.broadcastElements(elements);
|
||||
this.queueSaveToFirebase();
|
||||
};
|
||||
|
@ -923,8 +977,26 @@ class Collab extends PureComponent<CollabProps, CollabState> {
|
|||
|
||||
getActiveRoomLink = () => this.state.activeRoomLink;
|
||||
|
||||
setErrorMessage = (errorMessage: string | null) => {
|
||||
this.setState({ errorMessage });
|
||||
setErrorIndicator = (errorMessage: string | null) => {
|
||||
appJotaiStore.set(collabErrorIndicatorAtom, {
|
||||
message: errorMessage,
|
||||
nonce: Date.now(),
|
||||
});
|
||||
};
|
||||
|
||||
resetErrorIndicator = (resetDialogNotifiedErrors = false) => {
|
||||
appJotaiStore.set(collabErrorIndicatorAtom, { message: null, nonce: 0 });
|
||||
if (resetDialogNotifiedErrors) {
|
||||
this.setState({
|
||||
dialogNotifiedErrors: {},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
setErrorDialog = (errorMessage: string | null) => {
|
||||
this.setState({
|
||||
errorMessage,
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
|
@ -933,7 +1005,7 @@ class Collab extends PureComponent<CollabProps, CollabState> {
|
|||
return (
|
||||
<>
|
||||
{errorMessage != null && (
|
||||
<ErrorDialog onClose={() => this.setState({ errorMessage: null })}>
|
||||
<ErrorDialog onClose={() => this.setErrorDialog(null)}>
|
||||
{errorMessage}
|
||||
</ErrorDialog>
|
||||
)}
|
||||
|
@ -948,7 +1020,7 @@ declare global {
|
|||
}
|
||||
}
|
||||
|
||||
if (import.meta.env.MODE === ENV.TEST || import.meta.env.DEV) {
|
||||
if (isTestEnv() || isDevEnv()) {
|
||||
window.collab = window.collab || ({} as Window["collab"]);
|
||||
}
|
||||
|
||||
|
|
35
excalidraw-app/collab/CollabError.scss
Normal file
|
@ -0,0 +1,35 @@
|
|||
@import "../../packages/excalidraw/css/variables.module.scss";
|
||||
|
||||
.excalidraw {
|
||||
.collab-errors-button {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
margin-inline-end: 1rem;
|
||||
|
||||
color: var(--color-danger);
|
||||
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.collab-errors-button-shake {
|
||||
animation: strong-shake 0.15s 6;
|
||||
}
|
||||
|
||||
@keyframes strong-shake {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
25% {
|
||||
transform: rotate(10deg);
|
||||
}
|
||||
50% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
75% {
|
||||
transform: rotate(-10deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
}
|
||||
}
|
55
excalidraw-app/collab/CollabError.tsx
Normal file
|
@ -0,0 +1,55 @@
|
|||
import { Tooltip } from "@excalidraw/excalidraw/components/Tooltip";
|
||||
import { warning } from "@excalidraw/excalidraw/components/icons";
|
||||
import clsx from "clsx";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
import { atom } from "../app-jotai";
|
||||
|
||||
import "./CollabError.scss";
|
||||
|
||||
type ErrorIndicator = {
|
||||
message: string | null;
|
||||
/** used to rerun the useEffect responsible for animation */
|
||||
nonce: number;
|
||||
};
|
||||
|
||||
export const collabErrorIndicatorAtom = atom<ErrorIndicator>({
|
||||
message: null,
|
||||
nonce: 0,
|
||||
});
|
||||
|
||||
const CollabError = ({ collabError }: { collabError: ErrorIndicator }) => {
|
||||
const [isAnimating, setIsAnimating] = useState(false);
|
||||
const clearAnimationRef = useRef<string | number>(0);
|
||||
|
||||
useEffect(() => {
|
||||
setIsAnimating(true);
|
||||
clearAnimationRef.current = window.setTimeout(() => {
|
||||
setIsAnimating(false);
|
||||
}, 1000);
|
||||
|
||||
return () => {
|
||||
window.clearTimeout(clearAnimationRef.current);
|
||||
};
|
||||
}, [collabError.message, collabError.nonce]);
|
||||
|
||||
if (!collabError.message) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip label={collabError.message} long={true}>
|
||||
<div
|
||||
className={clsx("collab-errors-button", {
|
||||
"collab-errors-button-shake": isAnimating,
|
||||
})}
|
||||
>
|
||||
{warning}
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
CollabError.displayName = "CollabError";
|
||||
|
||||
export default CollabError;
|
|
@ -1,24 +1,25 @@
|
|||
import {
|
||||
isSyncableElement,
|
||||
SocketUpdateData,
|
||||
SocketUpdateDataSource,
|
||||
} from "../data";
|
||||
import { CaptureUpdateAction } from "@excalidraw/excalidraw";
|
||||
import { trackEvent } from "@excalidraw/excalidraw/analytics";
|
||||
import { encryptData } from "@excalidraw/excalidraw/data/encryption";
|
||||
import { newElementWith } from "@excalidraw/element/mutateElement";
|
||||
import throttle from "lodash.throttle";
|
||||
|
||||
import { TCollabClass } from "./Collab";
|
||||
|
||||
import { ExcalidrawElement } from "../../packages/excalidraw/element/types";
|
||||
import { WS_EVENTS, FILE_UPLOAD_TIMEOUT, WS_SUBTYPES } from "../app_constants";
|
||||
import {
|
||||
import type { UserIdleState } from "@excalidraw/common";
|
||||
import type { OrderedExcalidrawElement } from "@excalidraw/element/types";
|
||||
import type {
|
||||
OnUserFollowedPayload,
|
||||
SocketId,
|
||||
UserIdleState,
|
||||
} from "../../packages/excalidraw/types";
|
||||
import { trackEvent } from "../../packages/excalidraw/analytics";
|
||||
import throttle from "lodash.throttle";
|
||||
import { newElementWith } from "../../packages/excalidraw/element/mutateElement";
|
||||
import { BroadcastedExcalidrawElement } from "./reconciliation";
|
||||
import { encryptData } from "../../packages/excalidraw/data/encryption";
|
||||
import { PRECEDING_ELEMENT_KEY } from "../../packages/excalidraw/constants";
|
||||
} from "@excalidraw/excalidraw/types";
|
||||
|
||||
import { WS_EVENTS, FILE_UPLOAD_TIMEOUT, WS_SUBTYPES } from "../app_constants";
|
||||
import { isSyncableElement } from "../data";
|
||||
|
||||
import type {
|
||||
SocketUpdateData,
|
||||
SocketUpdateDataSource,
|
||||
SyncableExcalidrawElement,
|
||||
} from "../data";
|
||||
import type { TCollabClass } from "./Collab";
|
||||
import type { Socket } from "socket.io-client";
|
||||
|
||||
class Portal {
|
||||
|
@ -116,24 +117,31 @@ class Portal {
|
|||
}
|
||||
}
|
||||
|
||||
this.collab.excalidrawAPI.updateScene({
|
||||
elements: this.collab.excalidrawAPI
|
||||
let isChanged = false;
|
||||
const newElements = this.collab.excalidrawAPI
|
||||
.getSceneElementsIncludingDeleted()
|
||||
.map((element) => {
|
||||
if (this.collab.fileManager.shouldUpdateImageElementStatus(element)) {
|
||||
isChanged = true;
|
||||
// this will signal collaborators to pull image data from server
|
||||
// (using mutation instead of newElementWith otherwise it'd break
|
||||
// in-progress dragging)
|
||||
return newElementWith(element, { status: "saved" });
|
||||
}
|
||||
return element;
|
||||
}),
|
||||
});
|
||||
|
||||
if (isChanged) {
|
||||
this.collab.excalidrawAPI.updateScene({
|
||||
elements: newElements,
|
||||
captureUpdate: CaptureUpdateAction.NEVER,
|
||||
});
|
||||
}
|
||||
}, FILE_UPLOAD_TIMEOUT);
|
||||
|
||||
broadcastScene = async (
|
||||
updateType: WS_SUBTYPES.INIT | WS_SUBTYPES.UPDATE,
|
||||
allElements: readonly ExcalidrawElement[],
|
||||
elements: readonly OrderedExcalidrawElement[],
|
||||
syncAll: boolean,
|
||||
) => {
|
||||
if (updateType === WS_SUBTYPES.INIT && !syncAll) {
|
||||
|
@ -143,25 +151,17 @@ class Portal {
|
|||
// sync out only the elements we think we need to to save bandwidth.
|
||||
// periodically we'll resync the whole thing to make sure no one diverges
|
||||
// due to a dropped message (server goes down etc).
|
||||
const syncableElements = allElements.reduce(
|
||||
(acc, element: BroadcastedExcalidrawElement, idx, elements) => {
|
||||
const syncableElements = elements.reduce((acc, element) => {
|
||||
if (
|
||||
(syncAll ||
|
||||
!this.broadcastedElementVersions.has(element.id) ||
|
||||
element.version >
|
||||
this.broadcastedElementVersions.get(element.id)!) &&
|
||||
element.version > this.broadcastedElementVersions.get(element.id)!) &&
|
||||
isSyncableElement(element)
|
||||
) {
|
||||
acc.push({
|
||||
...element,
|
||||
// z-index info for the reconciler
|
||||
[PRECEDING_ELEMENT_KEY]: idx === 0 ? "^" : elements[idx - 1]?.id,
|
||||
});
|
||||
acc.push(element);
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
[] as BroadcastedExcalidrawElement[],
|
||||
);
|
||||
}, [] as SyncableExcalidrawElement[]);
|
||||
|
||||
const data: SocketUpdateDataSource[typeof updateType] = {
|
||||
type: updateType,
|
||||
|
|
|
@ -1,219 +0,0 @@
|
|||
import { useRef, useState } from "react";
|
||||
import * as Popover from "@radix-ui/react-popover";
|
||||
|
||||
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 "../../packages/excalidraw/components/Dialog";
|
||||
import {
|
||||
copyIcon,
|
||||
playerPlayIcon,
|
||||
playerStopFilledIcon,
|
||||
share,
|
||||
shareIOS,
|
||||
shareWindows,
|
||||
tablerCheckIcon,
|
||||
} from "../../packages/excalidraw/components/icons";
|
||||
import { TextField } from "../../packages/excalidraw/components/TextField";
|
||||
import { FilledButton } from "../../packages/excalidraw/components/FilledButton";
|
||||
|
||||
import { ReactComponent as CollabImage } from "../../packages/excalidraw/assets/lock.svg";
|
||||
import "./RoomDialog.scss";
|
||||
|
||||
const getShareIcon = () => {
|
||||
const navigator = window.navigator as any;
|
||||
const isAppleBrowser = /Apple/.test(navigator.vendor);
|
||||
const isWindowsBrowser = navigator.appVersion.indexOf("Win") !== -1;
|
||||
|
||||
if (isAppleBrowser) {
|
||||
return shareIOS;
|
||||
} else if (isWindowsBrowser) {
|
||||
return shareWindows;
|
||||
}
|
||||
|
||||
return share;
|
||||
};
|
||||
|
||||
export type RoomModalProps = {
|
||||
handleClose: () => void;
|
||||
activeRoomLink: string;
|
||||
username: string;
|
||||
onUsernameChange: (username: string) => void;
|
||||
onRoomCreate: () => void;
|
||||
onRoomDestroy: () => void;
|
||||
setErrorMessage: (message: string) => void;
|
||||
};
|
||||
|
||||
export const RoomModal = ({
|
||||
activeRoomLink,
|
||||
onRoomCreate,
|
||||
onRoomDestroy,
|
||||
setErrorMessage,
|
||||
username,
|
||||
onUsernameChange,
|
||||
handleClose,
|
||||
}: RoomModalProps) => {
|
||||
const { t } = useI18n();
|
||||
const [justCopied, setJustCopied] = useState(false);
|
||||
const timerRef = useRef<number>(0);
|
||||
const ref = useRef<HTMLInputElement>(null);
|
||||
const isShareSupported = "share" in navigator;
|
||||
|
||||
const copyRoomLink = async () => {
|
||||
try {
|
||||
await copyTextToSystemClipboard(activeRoomLink);
|
||||
|
||||
setJustCopied(true);
|
||||
|
||||
if (timerRef.current) {
|
||||
window.clearTimeout(timerRef.current);
|
||||
}
|
||||
|
||||
timerRef.current = window.setTimeout(() => {
|
||||
setJustCopied(false);
|
||||
}, 3000);
|
||||
} catch (error: any) {
|
||||
setErrorMessage(error.message);
|
||||
}
|
||||
|
||||
ref.current?.select();
|
||||
};
|
||||
|
||||
const shareRoomLink = async () => {
|
||||
try {
|
||||
await navigator.share({
|
||||
title: t("roomDialog.shareTitle"),
|
||||
text: t("roomDialog.shareTitle"),
|
||||
url: activeRoomLink,
|
||||
});
|
||||
} catch (error: any) {
|
||||
// Just ignore.
|
||||
}
|
||||
};
|
||||
|
||||
if (activeRoomLink) {
|
||||
return (
|
||||
<>
|
||||
<h3 className="RoomDialog__active__header">
|
||||
{t("labels.liveCollaboration")}
|
||||
</h3>
|
||||
<TextField
|
||||
value={username}
|
||||
placeholder="Your name"
|
||||
label="Your name"
|
||||
onChange={onUsernameChange}
|
||||
onKeyDown={(event) => event.key === KEYS.ENTER && handleClose()}
|
||||
/>
|
||||
<div className="RoomDialog__active__linkRow">
|
||||
<TextField
|
||||
ref={ref}
|
||||
label="Link"
|
||||
readonly
|
||||
fullWidth
|
||||
value={activeRoomLink}
|
||||
/>
|
||||
{isShareSupported && (
|
||||
<FilledButton
|
||||
size="large"
|
||||
variant="icon"
|
||||
label="Share"
|
||||
icon={getShareIcon()}
|
||||
className="RoomDialog__active__share"
|
||||
onClick={shareRoomLink}
|
||||
/>
|
||||
)}
|
||||
<Popover.Root open={justCopied}>
|
||||
<Popover.Trigger asChild>
|
||||
<FilledButton
|
||||
size="large"
|
||||
label="Copy link"
|
||||
icon={copyIcon}
|
||||
onClick={copyRoomLink}
|
||||
/>
|
||||
</Popover.Trigger>
|
||||
<Popover.Content
|
||||
onOpenAutoFocus={(event) => event.preventDefault()}
|
||||
onCloseAutoFocus={(event) => event.preventDefault()}
|
||||
className="RoomDialog__popover"
|
||||
side="top"
|
||||
align="end"
|
||||
sideOffset={5.5}
|
||||
>
|
||||
{tablerCheckIcon} copied
|
||||
</Popover.Content>
|
||||
</Popover.Root>
|
||||
</div>
|
||||
<div className="RoomDialog__active__description">
|
||||
<p>
|
||||
<span
|
||||
role="img"
|
||||
aria-hidden="true"
|
||||
className="RoomDialog__active__description__emoji"
|
||||
>
|
||||
🔒{" "}
|
||||
</span>
|
||||
{t("roomDialog.desc_privacy")}
|
||||
</p>
|
||||
<p>{t("roomDialog.desc_exitSession")}</p>
|
||||
</div>
|
||||
|
||||
<div className="RoomDialog__active__actions">
|
||||
<FilledButton
|
||||
size="large"
|
||||
variant="outlined"
|
||||
color="danger"
|
||||
label={t("roomDialog.button_stopSession")}
|
||||
icon={playerStopFilledIcon}
|
||||
onClick={() => {
|
||||
trackEvent("share", "room closed");
|
||||
onRoomDestroy();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="RoomDialog__inactive__illustration">
|
||||
<CollabImage />
|
||||
</div>
|
||||
<div className="RoomDialog__inactive__header">
|
||||
{t("labels.liveCollaboration")}
|
||||
</div>
|
||||
|
||||
<div className="RoomDialog__inactive__description">
|
||||
<strong>{t("roomDialog.desc_intro")}</strong>
|
||||
{t("roomDialog.desc_privacy")}
|
||||
</div>
|
||||
|
||||
<div className="RoomDialog__inactive__start_session">
|
||||
<FilledButton
|
||||
size="large"
|
||||
label={t("roomDialog.button_startSession")}
|
||||
icon={playerPlayIcon}
|
||||
onClick={() => {
|
||||
trackEvent("share", "room creation", `ui (${getFrame()})`);
|
||||
onRoomCreate();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const RoomDialog = (props: RoomModalProps) => {
|
||||
return (
|
||||
<Dialog size="small" onCloseRequest={props.handleClose} title={false}>
|
||||
<div className="RoomDialog">
|
||||
<RoomModal {...props} />
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default RoomDialog;
|
|
@ -1,154 +0,0 @@
|
|||
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";
|
||||
};
|
||||
|
||||
export type BroadcastedExcalidrawElement = ExcalidrawElement & {
|
||||
[PRECEDING_ELEMENT_KEY]?: string;
|
||||
};
|
||||
|
||||
const shouldDiscardRemoteElement = (
|
||||
localAppState: AppState,
|
||||
local: ExcalidrawElement | undefined,
|
||||
remote: BroadcastedExcalidrawElement,
|
||||
): boolean => {
|
||||
if (
|
||||
local &&
|
||||
// local element is being edited
|
||||
(local.id === localAppState.editingElement?.id ||
|
||||
local.id === localAppState.resizingElement?.id ||
|
||||
local.id === localAppState.draggingElement?.id ||
|
||||
// local element is newer
|
||||
local.version > remote.version ||
|
||||
// resolve conflicting edits deterministically by taking the one with
|
||||
// the lowest versionNonce
|
||||
(local.version === remote.version &&
|
||||
local.versionNonce < remote.versionNonce))
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
export const reconcileElements = (
|
||||
localElements: readonly ExcalidrawElement[],
|
||||
remoteElements: readonly BroadcastedExcalidrawElement[],
|
||||
localAppState: AppState,
|
||||
): ReconciledElements => {
|
||||
const localElementsData =
|
||||
arrayToMapWithIndex<ExcalidrawElement>(localElements);
|
||||
|
||||
const reconciledElements: ExcalidrawElement[] = localElements.slice();
|
||||
|
||||
const duplicates = new WeakMap<ExcalidrawElement, true>();
|
||||
|
||||
let cursor = 0;
|
||||
let offset = 0;
|
||||
|
||||
let remoteElementIdx = -1;
|
||||
for (const remoteElement of remoteElements) {
|
||||
remoteElementIdx++;
|
||||
|
||||
const local = localElementsData.get(remoteElement.id);
|
||||
|
||||
if (shouldDiscardRemoteElement(localAppState, local?.[0], remoteElement)) {
|
||||
if (remoteElement[PRECEDING_ELEMENT_KEY]) {
|
||||
delete remoteElement[PRECEDING_ELEMENT_KEY];
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Mark duplicate for removal as it'll be replaced with the remote element
|
||||
if (local) {
|
||||
// Unless the remote and local elements are the same element in which case
|
||||
// we need to keep it as we'd otherwise discard it from the resulting
|
||||
// array.
|
||||
if (local[0] === remoteElement) {
|
||||
continue;
|
||||
}
|
||||
duplicates.set(local[0], true);
|
||||
}
|
||||
|
||||
// parent may not be defined in case the remote client is running an older
|
||||
// excalidraw version
|
||||
const parent =
|
||||
remoteElement[PRECEDING_ELEMENT_KEY] ||
|
||||
remoteElements[remoteElementIdx - 1]?.id ||
|
||||
null;
|
||||
|
||||
if (parent != null) {
|
||||
delete remoteElement[PRECEDING_ELEMENT_KEY];
|
||||
|
||||
// ^ indicates the element is the first in elements array
|
||||
if (parent === "^") {
|
||||
offset++;
|
||||
if (cursor === 0) {
|
||||
reconciledElements.unshift(remoteElement);
|
||||
localElementsData.set(remoteElement.id, [
|
||||
remoteElement,
|
||||
cursor - offset,
|
||||
]);
|
||||
} else {
|
||||
reconciledElements.splice(cursor + 1, 0, remoteElement);
|
||||
localElementsData.set(remoteElement.id, [
|
||||
remoteElement,
|
||||
cursor + 1 - offset,
|
||||
]);
|
||||
cursor++;
|
||||
}
|
||||
} else {
|
||||
let idx = localElementsData.has(parent)
|
||||
? localElementsData.get(parent)![1]
|
||||
: null;
|
||||
if (idx != null) {
|
||||
idx += offset;
|
||||
}
|
||||
if (idx != null && idx >= cursor) {
|
||||
reconciledElements.splice(idx + 1, 0, remoteElement);
|
||||
offset++;
|
||||
localElementsData.set(remoteElement.id, [
|
||||
remoteElement,
|
||||
idx + 1 - offset,
|
||||
]);
|
||||
cursor = idx + 1;
|
||||
} else if (idx != null) {
|
||||
reconciledElements.splice(cursor + 1, 0, remoteElement);
|
||||
offset++;
|
||||
localElementsData.set(remoteElement.id, [
|
||||
remoteElement,
|
||||
cursor + 1 - offset,
|
||||
]);
|
||||
cursor++;
|
||||
} else {
|
||||
reconciledElements.push(remoteElement);
|
||||
localElementsData.set(remoteElement.id, [
|
||||
remoteElement,
|
||||
reconciledElements.length - 1 - offset,
|
||||
]);
|
||||
}
|
||||
}
|
||||
// no parent z-index information, local element exists → replace in place
|
||||
} else if (local) {
|
||||
reconciledElements[local[1]] = remoteElement;
|
||||
localElementsData.set(remoteElement.id, [remoteElement, local[1]]);
|
||||
// otherwise push to the end
|
||||
} else {
|
||||
reconciledElements.push(remoteElement);
|
||||
localElementsData.set(remoteElement.id, [
|
||||
remoteElement,
|
||||
reconciledElements.length - 1 - offset,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
const ret: readonly ExcalidrawElement[] = reconciledElements.filter(
|
||||
(element) => !duplicates.has(element),
|
||||
);
|
||||
|
||||
return ret as ReconciledElements;
|
||||
};
|
160
excalidraw-app/components/AI.tsx
Normal file
|
@ -0,0 +1,160 @@
|
|||
import {
|
||||
DiagramToCodePlugin,
|
||||
exportToBlob,
|
||||
getTextFromElements,
|
||||
MIME_TYPES,
|
||||
TTDDialog,
|
||||
} from "@excalidraw/excalidraw";
|
||||
import { getDataURL } from "@excalidraw/excalidraw/data/blob";
|
||||
import { safelyParseJSON } from "@excalidraw/common";
|
||||
|
||||
import type { ExcalidrawImperativeAPI } from "@excalidraw/excalidraw/types";
|
||||
|
||||
export const AIComponents = ({
|
||||
excalidrawAPI,
|
||||
}: {
|
||||
excalidrawAPI: ExcalidrawImperativeAPI;
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<DiagramToCodePlugin
|
||||
generate={async ({ frame, children }) => {
|
||||
const appState = excalidrawAPI.getAppState();
|
||||
|
||||
const blob = await exportToBlob({
|
||||
elements: children,
|
||||
appState: {
|
||||
...appState,
|
||||
exportBackground: true,
|
||||
viewBackgroundColor: appState.viewBackgroundColor,
|
||||
},
|
||||
exportingFrame: frame,
|
||||
files: excalidrawAPI.getFiles(),
|
||||
mimeType: MIME_TYPES.jpg,
|
||||
});
|
||||
|
||||
const dataURL = await getDataURL(blob);
|
||||
|
||||
const textFromFrameChildren = getTextFromElements(children);
|
||||
|
||||
const response = await fetch(
|
||||
`${
|
||||
import.meta.env.VITE_APP_AI_BACKEND
|
||||
}/v1/ai/diagram-to-code/generate`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
texts: textFromFrameChildren,
|
||||
image: dataURL,
|
||||
theme: appState.theme,
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
const text = await response.text();
|
||||
const errorJSON = safelyParseJSON(text);
|
||||
|
||||
if (!errorJSON) {
|
||||
throw new Error(text);
|
||||
}
|
||||
|
||||
if (errorJSON.statusCode === 429) {
|
||||
return {
|
||||
html: `<html>
|
||||
<body style="margin: 0; text-align: center">
|
||||
<div style="display: flex; align-items: center; justify-content: center; flex-direction: column; height: 100vh; padding: 0 60px">
|
||||
<div style="color:red">Too many requests today,</br>please try again tomorrow!</div>
|
||||
</br>
|
||||
</br>
|
||||
<div>You can also try <a href="${
|
||||
import.meta.env.VITE_APP_PLUS_LP
|
||||
}/plus?utm_source=excalidraw&utm_medium=app&utm_content=d2c" target="_blank" rel="noreferrer noopener">Excalidraw+</a> to get more requests.</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>`,
|
||||
};
|
||||
}
|
||||
|
||||
throw new Error(errorJSON.message || text);
|
||||
}
|
||||
|
||||
try {
|
||||
const { html } = await response.json();
|
||||
|
||||
if (!html) {
|
||||
throw new Error("Generation failed (invalid response)");
|
||||
}
|
||||
return {
|
||||
html,
|
||||
};
|
||||
} catch (error: any) {
|
||||
throw new Error("Generation failed (invalid response)");
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<TTDDialog
|
||||
onTextSubmit={async (input) => {
|
||||
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");
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -1,10 +1,14 @@
|
|||
import { Footer } from "@excalidraw/excalidraw/index";
|
||||
import React from "react";
|
||||
import { Footer } from "../../packages/excalidraw/index";
|
||||
import { EncryptedIcon } from "./EncryptedIcon";
|
||||
import { ExcalidrawPlusAppLink } from "./ExcalidrawPlusAppLink";
|
||||
|
||||
import { isExcalidrawPlusSignedUser } from "../app_constants";
|
||||
|
||||
export const AppFooter = React.memo(() => {
|
||||
import { DebugFooter, isVisualDebuggerEnabled } from "./DebugCanvas";
|
||||
import { EncryptedIcon } from "./EncryptedIcon";
|
||||
import { ExcalidrawPlusAppLink } from "./ExcalidrawPlusAppLink";
|
||||
|
||||
export const AppFooter = React.memo(
|
||||
({ onChange }: { onChange: () => void }) => {
|
||||
return (
|
||||
<Footer>
|
||||
<div
|
||||
|
@ -14,6 +18,7 @@ export const AppFooter = React.memo(() => {
|
|||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
{isVisualDebuggerEnabled() && <DebugFooter onChange={onChange} />}
|
||||
{isExcalidrawPlusSignedUser ? (
|
||||
<ExcalidrawPlusAppLink />
|
||||
) : (
|
||||
|
@ -22,4 +27,5 @@ export const AppFooter = React.memo(() => {
|
|||
</div>
|
||||
</Footer>
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
|
|
@ -1,12 +1,27 @@
|
|||
import {
|
||||
loginIcon,
|
||||
ExcalLogo,
|
||||
eyeIcon,
|
||||
} from "@excalidraw/excalidraw/components/icons";
|
||||
import { MainMenu } from "@excalidraw/excalidraw/index";
|
||||
import React from "react";
|
||||
import { PlusPromoIcon } from "../../packages/excalidraw/components/icons";
|
||||
import { MainMenu } from "../../packages/excalidraw/index";
|
||||
import { LanguageList } from "./LanguageList";
|
||||
|
||||
import { isDevEnv } from "@excalidraw/common";
|
||||
|
||||
import type { Theme } from "@excalidraw/element/types";
|
||||
|
||||
import { LanguageList } from "../app-language/LanguageList";
|
||||
import { isExcalidrawPlusSignedUser } from "../app_constants";
|
||||
|
||||
import { saveDebugState } from "./DebugCanvas";
|
||||
|
||||
export const AppMainMenu: React.FC<{
|
||||
onCollabDialogOpen: () => any;
|
||||
isCollaborating: boolean;
|
||||
isCollabEnabled: boolean;
|
||||
theme: Theme | "system";
|
||||
setTheme: (theme: Theme | "system") => void;
|
||||
refresh: () => void;
|
||||
}> = React.memo((props) => {
|
||||
return (
|
||||
<MainMenu>
|
||||
|
@ -20,22 +35,53 @@ export const AppMainMenu: React.FC<{
|
|||
onSelect={() => props.onCollabDialogOpen()}
|
||||
/>
|
||||
)}
|
||||
|
||||
<MainMenu.DefaultItems.CommandPalette className="highlighted" />
|
||||
<MainMenu.DefaultItems.SearchMenu />
|
||||
<MainMenu.DefaultItems.Help />
|
||||
<MainMenu.DefaultItems.ClearCanvas />
|
||||
<MainMenu.Separator />
|
||||
<MainMenu.ItemLink
|
||||
icon={PlusPromoIcon}
|
||||
icon={ExcalLogo}
|
||||
href={`${
|
||||
import.meta.env.VITE_APP_PLUS_LP
|
||||
}/plus?utm_source=excalidraw&utm_medium=app&utm_content=hamburger`}
|
||||
className="ExcalidrawPlus"
|
||||
className=""
|
||||
>
|
||||
Excalidraw+
|
||||
</MainMenu.ItemLink>
|
||||
<MainMenu.DefaultItems.Socials />
|
||||
<MainMenu.ItemLink
|
||||
icon={loginIcon}
|
||||
href={`${import.meta.env.VITE_APP_PLUS_APP}${
|
||||
isExcalidrawPlusSignedUser ? "" : "/sign-up"
|
||||
}?utm_source=signin&utm_medium=app&utm_content=hamburger`}
|
||||
className="highlighted"
|
||||
>
|
||||
{isExcalidrawPlusSignedUser ? "Sign in" : "Sign up"}
|
||||
</MainMenu.ItemLink>
|
||||
{isDevEnv() && (
|
||||
<MainMenu.Item
|
||||
icon={eyeIcon}
|
||||
onClick={() => {
|
||||
if (window.visualDebug) {
|
||||
delete window.visualDebug;
|
||||
saveDebugState({ enabled: false });
|
||||
} else {
|
||||
window.visualDebug = { data: [] };
|
||||
saveDebugState({ enabled: true });
|
||||
}
|
||||
props?.refresh();
|
||||
}}
|
||||
>
|
||||
Visual Debug
|
||||
</MainMenu.Item>
|
||||
)}
|
||||
<MainMenu.Separator />
|
||||
<MainMenu.DefaultItems.ToggleTheme />
|
||||
<MainMenu.DefaultItems.ToggleTheme
|
||||
allowSystemTheme
|
||||
theme={props.theme}
|
||||
onSelect={props.setTheme}
|
||||
/>
|
||||
<MainMenu.ItemCustom>
|
||||
<LanguageList style={{ width: "100%" }} />
|
||||
</MainMenu.ItemCustom>
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
import { loginIcon } from "@excalidraw/excalidraw/components/icons";
|
||||
import { POINTER_EVENTS } from "@excalidraw/common";
|
||||
import { useI18n } from "@excalidraw/excalidraw/i18n";
|
||||
import { WelcomeScreen } from "@excalidraw/excalidraw/index";
|
||||
import React from "react";
|
||||
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 "../../packages/excalidraw/constants";
|
||||
|
||||
export const AppWelcomeScreen: React.FC<{
|
||||
onCollabDialogOpen: () => any;
|
||||
|
@ -61,9 +62,9 @@ export const AppWelcomeScreen: React.FC<{
|
|||
import.meta.env.VITE_APP_PLUS_LP
|
||||
}/plus?utm_source=excalidraw&utm_medium=app&utm_content=welcomeScreenGuest`}
|
||||
shortcut={null}
|
||||
icon={PlusPromoIcon}
|
||||
icon={loginIcon}
|
||||
>
|
||||
Try Excalidraw Plus!
|
||||
Sign up
|
||||
</WelcomeScreen.Center.MenuItemLink>
|
||||
)}
|
||||
</WelcomeScreen.Center.Menu>
|
||||
|
|
348
excalidraw-app/components/DebugCanvas.tsx
Normal file
|
@ -0,0 +1,348 @@
|
|||
import {
|
||||
ArrowheadArrowIcon,
|
||||
CloseIcon,
|
||||
TrashIcon,
|
||||
} from "@excalidraw/excalidraw/components/icons";
|
||||
import {
|
||||
bootstrapCanvas,
|
||||
getNormalizedCanvasDimensions,
|
||||
} from "@excalidraw/excalidraw/renderer/helpers";
|
||||
import { type AppState } from "@excalidraw/excalidraw/types";
|
||||
import { throttleRAF } from "@excalidraw/common";
|
||||
import { useCallback, useImperativeHandle, useRef } from "react";
|
||||
|
||||
import {
|
||||
isLineSegment,
|
||||
type GlobalPoint,
|
||||
type LineSegment,
|
||||
} from "@excalidraw/math";
|
||||
import { isCurve } from "@excalidraw/math/curve";
|
||||
|
||||
import type { DebugElement } from "@excalidraw/excalidraw/visualdebug";
|
||||
|
||||
import type { Curve } from "@excalidraw/math";
|
||||
|
||||
import { STORAGE_KEYS } from "../app_constants";
|
||||
|
||||
const renderLine = (
|
||||
context: CanvasRenderingContext2D,
|
||||
zoom: number,
|
||||
segment: LineSegment<GlobalPoint>,
|
||||
color: string,
|
||||
) => {
|
||||
context.save();
|
||||
context.strokeStyle = color;
|
||||
context.beginPath();
|
||||
context.moveTo(segment[0][0] * zoom, segment[0][1] * zoom);
|
||||
context.lineTo(segment[1][0] * zoom, segment[1][1] * zoom);
|
||||
context.stroke();
|
||||
context.restore();
|
||||
};
|
||||
|
||||
const renderCubicBezier = (
|
||||
context: CanvasRenderingContext2D,
|
||||
zoom: number,
|
||||
[start, control1, control2, end]: Curve<GlobalPoint>,
|
||||
color: string,
|
||||
) => {
|
||||
context.save();
|
||||
context.strokeStyle = color;
|
||||
context.beginPath();
|
||||
context.moveTo(start[0] * zoom, start[1] * zoom);
|
||||
context.bezierCurveTo(
|
||||
control1[0] * zoom,
|
||||
control1[1] * zoom,
|
||||
control2[0] * zoom,
|
||||
control2[1] * zoom,
|
||||
end[0] * zoom,
|
||||
end[1] * zoom,
|
||||
);
|
||||
context.stroke();
|
||||
context.restore();
|
||||
};
|
||||
|
||||
const renderOrigin = (context: CanvasRenderingContext2D, zoom: number) => {
|
||||
context.strokeStyle = "#888";
|
||||
context.save();
|
||||
context.beginPath();
|
||||
context.moveTo(-10 * zoom, -10 * zoom);
|
||||
context.lineTo(10 * zoom, 10 * zoom);
|
||||
context.moveTo(10 * zoom, -10 * zoom);
|
||||
context.lineTo(-10 * zoom, 10 * zoom);
|
||||
context.stroke();
|
||||
context.save();
|
||||
};
|
||||
|
||||
const render = (
|
||||
frame: DebugElement[],
|
||||
context: CanvasRenderingContext2D,
|
||||
appState: AppState,
|
||||
) => {
|
||||
frame.forEach((el: DebugElement) => {
|
||||
switch (true) {
|
||||
case isLineSegment(el.data):
|
||||
renderLine(
|
||||
context,
|
||||
appState.zoom.value,
|
||||
el.data as LineSegment<GlobalPoint>,
|
||||
el.color,
|
||||
);
|
||||
break;
|
||||
case isCurve(el.data):
|
||||
renderCubicBezier(
|
||||
context,
|
||||
appState.zoom.value,
|
||||
el.data as Curve<GlobalPoint>,
|
||||
el.color,
|
||||
);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unknown element type ${JSON.stringify(el)}`);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const _debugRenderer = (
|
||||
canvas: HTMLCanvasElement,
|
||||
appState: AppState,
|
||||
scale: number,
|
||||
refresh: () => void,
|
||||
) => {
|
||||
const [normalizedWidth, normalizedHeight] = getNormalizedCanvasDimensions(
|
||||
canvas,
|
||||
scale,
|
||||
);
|
||||
|
||||
if (appState.height !== canvas.height || appState.width !== canvas.width) {
|
||||
refresh();
|
||||
}
|
||||
|
||||
const context = bootstrapCanvas({
|
||||
canvas,
|
||||
scale,
|
||||
normalizedWidth,
|
||||
normalizedHeight,
|
||||
viewBackgroundColor: "transparent",
|
||||
});
|
||||
|
||||
// Apply zoom
|
||||
context.save();
|
||||
context.translate(
|
||||
appState.scrollX * appState.zoom.value,
|
||||
appState.scrollY * appState.zoom.value,
|
||||
);
|
||||
|
||||
renderOrigin(context, appState.zoom.value);
|
||||
|
||||
if (
|
||||
window.visualDebug?.currentFrame &&
|
||||
window.visualDebug?.data &&
|
||||
window.visualDebug.data.length > 0
|
||||
) {
|
||||
// Render only one frame
|
||||
const [idx] = debugFrameData();
|
||||
|
||||
render(window.visualDebug.data[idx], context, appState);
|
||||
} else {
|
||||
// Render all debug frames
|
||||
window.visualDebug?.data.forEach((frame) => {
|
||||
render(frame, context, appState);
|
||||
});
|
||||
}
|
||||
|
||||
if (window.visualDebug) {
|
||||
window.visualDebug!.data =
|
||||
window.visualDebug?.data.map((frame) =>
|
||||
frame.filter((el) => el.permanent),
|
||||
) ?? [];
|
||||
}
|
||||
};
|
||||
|
||||
const debugFrameData = (): [number, number] => {
|
||||
const currentFrame = window.visualDebug?.currentFrame ?? 0;
|
||||
const frameCount = window.visualDebug?.data.length ?? 0;
|
||||
|
||||
if (frameCount > 0) {
|
||||
return [currentFrame % frameCount, window.visualDebug?.currentFrame ?? 0];
|
||||
}
|
||||
|
||||
return [0, 0];
|
||||
};
|
||||
|
||||
export const saveDebugState = (debug: { enabled: boolean }) => {
|
||||
try {
|
||||
localStorage.setItem(
|
||||
STORAGE_KEYS.LOCAL_STORAGE_DEBUG,
|
||||
JSON.stringify(debug),
|
||||
);
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
export const debugRenderer = throttleRAF(
|
||||
(
|
||||
canvas: HTMLCanvasElement,
|
||||
appState: AppState,
|
||||
scale: number,
|
||||
refresh: () => void,
|
||||
) => {
|
||||
_debugRenderer(canvas, appState, scale, refresh);
|
||||
},
|
||||
{ trailing: true },
|
||||
);
|
||||
|
||||
export const loadSavedDebugState = () => {
|
||||
let debug;
|
||||
try {
|
||||
const savedDebugState = localStorage.getItem(
|
||||
STORAGE_KEYS.LOCAL_STORAGE_DEBUG,
|
||||
);
|
||||
if (savedDebugState) {
|
||||
debug = JSON.parse(savedDebugState) as { enabled: boolean };
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
return debug ?? { enabled: false };
|
||||
};
|
||||
|
||||
export const isVisualDebuggerEnabled = () =>
|
||||
Array.isArray(window.visualDebug?.data);
|
||||
|
||||
export const DebugFooter = ({ onChange }: { onChange: () => void }) => {
|
||||
const moveForward = useCallback(() => {
|
||||
if (
|
||||
!window.visualDebug?.currentFrame ||
|
||||
isNaN(window.visualDebug?.currentFrame ?? -1)
|
||||
) {
|
||||
window.visualDebug!.currentFrame = 0;
|
||||
}
|
||||
window.visualDebug!.currentFrame += 1;
|
||||
onChange();
|
||||
}, [onChange]);
|
||||
const moveBackward = useCallback(() => {
|
||||
if (
|
||||
!window.visualDebug?.currentFrame ||
|
||||
isNaN(window.visualDebug?.currentFrame ?? -1) ||
|
||||
window.visualDebug?.currentFrame < 1
|
||||
) {
|
||||
window.visualDebug!.currentFrame = 1;
|
||||
}
|
||||
window.visualDebug!.currentFrame -= 1;
|
||||
onChange();
|
||||
}, [onChange]);
|
||||
const reset = useCallback(() => {
|
||||
window.visualDebug!.currentFrame = undefined;
|
||||
onChange();
|
||||
}, [onChange]);
|
||||
const trashFrames = useCallback(() => {
|
||||
if (window.visualDebug) {
|
||||
window.visualDebug.currentFrame = undefined;
|
||||
window.visualDebug.data = [];
|
||||
}
|
||||
onChange();
|
||||
}, [onChange]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
className="ToolIcon_type_button"
|
||||
data-testid="debug-forward"
|
||||
aria-label="Move forward"
|
||||
type="button"
|
||||
onClick={trashFrames}
|
||||
>
|
||||
<div
|
||||
className="ToolIcon__icon"
|
||||
aria-hidden="true"
|
||||
aria-disabled="false"
|
||||
>
|
||||
{TrashIcon}
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
className="ToolIcon_type_button"
|
||||
data-testid="debug-forward"
|
||||
aria-label="Move forward"
|
||||
type="button"
|
||||
onClick={moveBackward}
|
||||
>
|
||||
<div
|
||||
className="ToolIcon__icon"
|
||||
aria-hidden="true"
|
||||
aria-disabled="false"
|
||||
>
|
||||
<ArrowheadArrowIcon flip />
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
className="ToolIcon_type_button"
|
||||
data-testid="debug-forward"
|
||||
aria-label="Move forward"
|
||||
type="button"
|
||||
onClick={reset}
|
||||
>
|
||||
<div
|
||||
className="ToolIcon__icon"
|
||||
aria-hidden="true"
|
||||
aria-disabled="false"
|
||||
>
|
||||
{CloseIcon}
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
className="ToolIcon_type_button"
|
||||
data-testid="debug-backward"
|
||||
aria-label="Move backward"
|
||||
type="button"
|
||||
onClick={moveForward}
|
||||
>
|
||||
<div
|
||||
className="ToolIcon__icon"
|
||||
aria-hidden="true"
|
||||
aria-disabled="false"
|
||||
>
|
||||
<ArrowheadArrowIcon />
|
||||
</div>
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
interface DebugCanvasProps {
|
||||
appState: AppState;
|
||||
scale: number;
|
||||
ref?: React.Ref<HTMLCanvasElement>;
|
||||
}
|
||||
|
||||
const DebugCanvas = ({ appState, scale, ref }: DebugCanvasProps) => {
|
||||
const { width, height } = appState;
|
||||
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
useImperativeHandle<HTMLCanvasElement | null, HTMLCanvasElement | null>(
|
||||
ref,
|
||||
() => canvasRef.current,
|
||||
[canvasRef],
|
||||
);
|
||||
|
||||
return (
|
||||
<canvas
|
||||
style={{
|
||||
width,
|
||||
height,
|
||||
position: "absolute",
|
||||
zIndex: 2,
|
||||
pointerEvents: "none",
|
||||
}}
|
||||
width={width * scale}
|
||||
height={height * scale}
|
||||
ref={canvasRef}
|
||||
>
|
||||
Debug Canvas
|
||||
</canvas>
|
||||
);
|
||||
};
|
||||
|
||||
export default DebugCanvas;
|
|
@ -1,6 +1,6 @@
|
|||
import { shield } from "../../packages/excalidraw/components/icons";
|
||||
import { Tooltip } from "../../packages/excalidraw/components/Tooltip";
|
||||
import { useI18n } from "../../packages/excalidraw/i18n";
|
||||
import { Tooltip } from "@excalidraw/excalidraw/components/Tooltip";
|
||||
import { shield } from "@excalidraw/excalidraw/components/icons";
|
||||
import { useI18n } from "@excalidraw/excalidraw/i18n";
|
||||
|
||||
export const EncryptedIcon = () => {
|
||||
const { t } = useI18n();
|
||||
|
@ -8,7 +8,7 @@ export const EncryptedIcon = () => {
|
|||
return (
|
||||
<a
|
||||
className="encrypted-icon tooltip"
|
||||
href="https://blog.excalidraw.com/end-to-end-encryption/"
|
||||
href="https://plus.excalidraw.com/blog/end-to-end-encryption"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label={t("encrypted.link")}
|
||||
|
|
|
@ -1,37 +1,41 @@
|
|||
import React from "react";
|
||||
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 "../../packages/excalidraw/element/types";
|
||||
import {
|
||||
AppState,
|
||||
BinaryFileData,
|
||||
BinaryFiles,
|
||||
} from "../../packages/excalidraw/types";
|
||||
import { uploadBytes, ref } from "firebase/storage";
|
||||
import { nanoid } from "nanoid";
|
||||
import { useI18n } from "../../packages/excalidraw/i18n";
|
||||
|
||||
import { trackEvent } from "@excalidraw/excalidraw/analytics";
|
||||
import { Card } from "@excalidraw/excalidraw/components/Card";
|
||||
import { ExcalidrawLogo } from "@excalidraw/excalidraw/components/ExcalidrawLogo";
|
||||
import { ToolButton } from "@excalidraw/excalidraw/components/ToolButton";
|
||||
import { MIME_TYPES, getFrame } from "@excalidraw/common";
|
||||
import {
|
||||
encryptData,
|
||||
generateEncryptionKey,
|
||||
} from "../../packages/excalidraw/data/encryption";
|
||||
import { isInitializedImageElement } from "../../packages/excalidraw/element/typeChecks";
|
||||
} from "@excalidraw/excalidraw/data/encryption";
|
||||
import { serializeAsJSON } from "@excalidraw/excalidraw/data/json";
|
||||
import { isInitializedImageElement } from "@excalidraw/element/typeChecks";
|
||||
import { useI18n } from "@excalidraw/excalidraw/i18n";
|
||||
|
||||
import type {
|
||||
FileId,
|
||||
NonDeletedExcalidrawElement,
|
||||
} from "@excalidraw/element/types";
|
||||
import type {
|
||||
AppState,
|
||||
BinaryFileData,
|
||||
BinaryFiles,
|
||||
} from "@excalidraw/excalidraw/types";
|
||||
|
||||
import { FILE_UPLOAD_MAX_BYTES } from "../app_constants";
|
||||
import { encodeFilesForUpload } from "../data/FileManager";
|
||||
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";
|
||||
import { loadFirebaseStorage, saveFilesToFirebase } from "../data/firebase";
|
||||
|
||||
export const exportToExcalidrawPlus = async (
|
||||
elements: readonly NonDeletedExcalidrawElement[],
|
||||
appState: Partial<AppState>,
|
||||
files: BinaryFiles,
|
||||
name: string,
|
||||
) => {
|
||||
const firebase = await loadFirebaseStorage();
|
||||
const storage = await loadFirebaseStorage();
|
||||
|
||||
const id = `${nanoid(12)}`;
|
||||
|
||||
|
@ -48,12 +52,10 @@ export const exportToExcalidrawPlus = async (
|
|||
},
|
||||
);
|
||||
|
||||
await firebase
|
||||
.storage()
|
||||
.ref(`/migrations/scenes/${id}`)
|
||||
.put(blob, {
|
||||
const storageRef = ref(storage, `/migrations/scenes/${id}`);
|
||||
await uploadBytes(storageRef, blob, {
|
||||
customMetadata: {
|
||||
data: JSON.stringify({ version: 2, name: appState.name }),
|
||||
data: JSON.stringify({ version: 2, name }),
|
||||
created: Date.now().toString(),
|
||||
},
|
||||
});
|
||||
|
@ -89,9 +91,10 @@ export const ExportToExcalidrawPlus: React.FC<{
|
|||
elements: readonly NonDeletedExcalidrawElement[];
|
||||
appState: Partial<AppState>;
|
||||
files: BinaryFiles;
|
||||
name: string;
|
||||
onError: (error: Error) => void;
|
||||
onSuccess: () => void;
|
||||
}> = ({ elements, appState, files, onError, onSuccess }) => {
|
||||
}> = ({ elements, appState, files, name, onError, onSuccess }) => {
|
||||
const { t } = useI18n();
|
||||
return (
|
||||
<Card color="primary">
|
||||
|
@ -117,7 +120,7 @@ export const ExportToExcalidrawPlus: React.FC<{
|
|||
onClick={async () => {
|
||||
try {
|
||||
trackEvent("export", "eplus", `ui (${getFrame()})`);
|
||||
await exportToExcalidrawPlus(elements, appState, files);
|
||||
await exportToExcalidrawPlus(elements, appState, files, name);
|
||||
onSuccess();
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import { THEME } from "@excalidraw/common";
|
||||
import oc from "open-color";
|
||||
import React from "react";
|
||||
import { THEME } from "../../packages/excalidraw/constants";
|
||||
import { Theme } from "../../packages/excalidraw/element/types";
|
||||
|
||||
import type { Theme } from "@excalidraw/element/types";
|
||||
|
||||
// https://github.com/tholman/github-corners
|
||||
export const GitHubCorner = React.memo(
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import React from "react";
|
||||
import Trans from "@excalidraw/excalidraw/components/Trans";
|
||||
import { t } from "@excalidraw/excalidraw/i18n";
|
||||
import * as Sentry from "@sentry/browser";
|
||||
import { t } from "../../packages/excalidraw/i18n";
|
||||
import Trans from "../../packages/excalidraw/components/Trans";
|
||||
import React from "react";
|
||||
|
||||
interface TopErrorBoundaryState {
|
||||
hasError: boolean;
|
||||
|
@ -67,6 +67,8 @@ export class TopErrorBoundary extends React.Component<
|
|||
|
||||
window.open(
|
||||
`https://github.com/excalidraw/excalidraw/issues/new?body=${body}`,
|
||||
"_blank",
|
||||
"noopener noreferrer",
|
||||
);
|
||||
}
|
||||
|
||||
|
|