Compare commits

..

18 Commits

Author SHA1 Message Date
Arnošt Pleskot
bef0f3e51d feat: add new socket message when user joins room 2023-07-15 14:27:39 +02:00
Arnošt Pleskot
28b0095c8a fix: making linter happy 2023-07-14 20:49:25 +02:00
Arnošt Pleskot
c4ff0594e3 fix: get socketId from emitted message 2023-07-13 10:24:36 +02:00
Arnošt Pleskot
4608e809b1 feat: filter disconnected users 2023-07-13 10:07:31 +02:00
Arnošt Pleskot
db5149ab5d feat: store userId in localStorage 2023-07-12 17:22:46 +02:00
Arnošt Pleskot
2bdf09153c feat: use userId instead of socketId 2023-07-12 16:56:21 +02:00
Arnošt Pleskot
62df03d78d feat: submit scene to firebase on collab pause 2023-07-11 16:55:21 +02:00
Arnošt Pleskot
a1d3350131 fix: connect users when resumed from firebase 2023-07-11 16:55:20 +02:00
Arnošt Pleskot
a2d371bf1d feat: when no users in room fetch data from firebase immediately 2023-07-11 16:55:19 +02:00
Arnošt Pleskot
e340103250 feat: firebase fallback for resume collaboration 2023-07-11 16:55:18 +02:00
Arnošt Pleskot
0567af1bcb fix: properly sync after reconnecting 2023-07-11 16:55:17 +02:00
Arnošt Pleskot
2ffeff442a feat: disable viewMode on update and init message 2023-07-11 16:55:16 +02:00
Arnošt Pleskot
ef190ebf30 feat: add optional spinner into toast message 2023-07-11 16:55:15 +02:00
Arnošt Pleskot
e1ff9791f2 feat: move logic from index.tsx into Collab.tsx 2023-07-11 16:55:14 +02:00
Arnošt Pleskot
aa91af8f7d fix: emit scene init event after reconnecting 2023-07-11 16:55:13 +02:00
Arnošt Pleskot
52254bca7c fix: do not fire pause logic on window focus/blur event 2023-07-11 16:55:12 +02:00
Arnošt Pleskot
addf9d71fa feat: pause collab when user switches tabs in the browser 2023-07-11 16:55:11 +02:00
Arnošt Pleskot
1badf14a93 chore: update socket.io-client 2023-07-11 16:55:00 +02:00
234 changed files with 20991 additions and 21536 deletions

View File

@@ -1,5 +0,0 @@
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

View File

@@ -27,10 +27,7 @@
"start": {
"name": "Start Excalidraw",
"command": "yarn start",
"runAtStart": true,
"preview": {
"port": 3000
}
"runAtStart": true
},
"test": {
"name": "Run Tests",
@@ -40,11 +37,7 @@
"install-deps": {
"name": "Install Dependencies",
"command": "yarn install",
"restartOn": {
"files": ["yarn.lock"],
"branch": false,
"resume": false
}
"restartOn": { "files": ["yarn.lock"] }
}
}
}

View File

@@ -1,39 +1,30 @@
VITE_APP_BACKEND_V2_GET_URL=https://json-dev.excalidraw.com/api/v2/
VITE_APP_BACKEND_V2_POST_URL=https://json-dev.excalidraw.com/api/v2/post/
REACT_APP_BACKEND_V2_GET_URL=https://json-dev.excalidraw.com/api/v2/
REACT_APP_BACKEND_V2_POST_URL=https://json-dev.excalidraw.com/api/v2/post/
VITE_APP_LIBRARY_URL=https://libraries.excalidraw.com
VITE_APP_LIBRARY_BACKEND=https://us-central1-excalidraw-room-persistence.cloudfunctions.net/libraries
REACT_APP_LIBRARY_URL=https://libraries.excalidraw.com
REACT_APP_LIBRARY_BACKEND=https://us-central1-excalidraw-room-persistence.cloudfunctions.net/libraries
# collaboration WebSocket server (https://github.com/excalidraw/excalidraw-room)
VITE_APP_WS_SERVER_URL=http://localhost:3002
REACT_APP_WS_SERVER_URL=http://localhost:3002
# set this only if using the collaboration workflow we use on excalidraw.com
VITE_APP_PORTAL_URL=
REACT_APP_PORTAL_URL=
VITE_APP_FIREBASE_CONFIG='{"apiKey":"AIzaSyCMkxA60XIW8KbqMYL7edC4qT5l4qHX2h8","authDomain":"excalidraw-oss-dev.firebaseapp.com","projectId":"excalidraw-oss-dev","storageBucket":"excalidraw-oss-dev.appspot.com","messagingSenderId":"664559512677","appId":"1:664559512677:web:a385181f2928d328a7aa8c"}'
REACT_APP_FIREBASE_CONFIG='{"apiKey":"AIzaSyCMkxA60XIW8KbqMYL7edC4qT5l4qHX2h8","authDomain":"excalidraw-oss-dev.firebaseapp.com","projectId":"excalidraw-oss-dev","storageBucket":"excalidraw-oss-dev.appspot.com","messagingSenderId":"664559512677","appId":"1:664559512677:web:a385181f2928d328a7aa8c"}'
# 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=
REACT_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
REACT_APP_DEV_DISABLE_LIVE_RELOAD=
REACT_APP_DISABLE_TRACKING=true
FAST_REFRESH=false
# The port the run the dev server
VITE_APP_PORT=3000
#Debug flags
# To enable bounding box for text containers
VITE_APP_DEBUG_ENABLE_TEXT_CONTAINER_BOUNDING_BOX=
# Set this flag to false if you want to open the overlay by default
VITE_APP_COLLAPSE_OVERLAY=true
# Set this flag to false to disable eslint
VITE_APP_ENABLE_ESLINT=true
REACT_APP_DEBUG_ENABLE_TEXT_CONTAINER_BOUNDING_BOX=

View File

@@ -1,15 +1,15 @@
VITE_APP_BACKEND_V2_GET_URL=https://json.excalidraw.com/api/v2/
VITE_APP_BACKEND_V2_POST_URL=https://json.excalidraw.com/api/v2/post/
REACT_APP_BACKEND_V2_GET_URL=https://json.excalidraw.com/api/v2/
REACT_APP_BACKEND_V2_POST_URL=https://json.excalidraw.com/api/v2/post/
VITE_APP_LIBRARY_URL=https://libraries.excalidraw.com
VITE_APP_LIBRARY_BACKEND=https://us-central1-excalidraw-room-persistence.cloudfunctions.net/libraries
REACT_APP_LIBRARY_URL=https://libraries.excalidraw.com
REACT_APP_LIBRARY_BACKEND=https://us-central1-excalidraw-room-persistence.cloudfunctions.net/libraries
VITE_APP_PORTAL_URL=https://portal.excalidraw.com
REACT_APP_PORTAL_URL=https://portal.excalidraw.com
# Fill to set socket server URL used for collaboration.
# Meant for forks only: excalidraw.com uses custom VITE_APP_PORTAL_URL flow
VITE_APP_WS_SERVER_URL=
# Meant for forks only: excalidraw.com uses custom REACT_APP_PORTAL_URL flow
REACT_APP_WS_SERVER_URL=
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"}'
REACT_APP_FIREBASE_CONFIG='{"apiKey":"AIzaSyAd15pYlMci_xIp9ko6wkEsDzAAA0Dn0RU","authDomain":"excalidraw-room-persistence.firebaseapp.com","databaseURL":"https://excalidraw-room-persistence.firebaseio.com","projectId":"excalidraw-room-persistence","storageBucket":"excalidraw-room-persistence.appspot.com","messagingSenderId":"654800341332","appId":"1:654800341332:web:4a692de832b55bd57ce0c1"}'
VITE_APP_PLUS_APP=https://app.excalidraw.com
VITE_APP_DISABLE_TRACKING=
REACT_APP_PLUS_APP=https://app.excalidraw.com
REACT_APP_DISABLE_TRACKING=

View File

@@ -1,43 +1,7 @@
{
"extends": ["plugin:react-hooks/recommended"],
"parser": "@typescript-eslint/parser",
"plugins": ["react", "@typescript-eslint"],
"root": true,
"extends": ["@excalidraw/eslint-config", "react-app"],
"rules": {
"import/no-anonymous-default-export": "off",
"no-restricted-globals": "off",
"@typescript-eslint/no-unused-vars": "warn",
"curly": "warn",
"dot-notation": "warn",
"no-console": [
"warn",
{
"allow": ["warn", "error", "info"]
}
],
"no-else-return": "warn",
"no-lonely-if": "warn",
"no-restricted-syntax": [
"warn",
{
"message": "Use 't(...)' instead of literal text in JSX",
"selector": "JSXText[value=/\\w/]"
}
],
"no-unneeded-ternary": "warn",
"no-unused-expressions": "warn",
"no-unused-vars": "off",
"no-useless-return": "warn",
"no-var": "warn",
"object-shorthand": "warn",
"one-var": ["warn", "never"],
"prefer-arrow-callback": "warn",
"prefer-const": [
"warn",
{
"destructuring": "all"
}
],
"prefer-template": "warn"
"no-restricted-globals": "off"
}
}

View File

@@ -12,10 +12,10 @@ jobs:
- uses: actions/checkout@v2
with:
fetch-depth: 2
- name: Setup Node.js 18.x
- name: Setup Node.js 14.x
uses: actions/setup-node@v2
with:
node-version: 18.x
node-version: 14.x
- name: Set up publish access
run: |
npm config set //registry.npmjs.org/:_authToken ${NPM_TOKEN}

View File

@@ -32,10 +32,10 @@ jobs:
with:
ref: ${{ steps.sha.outputs.result }}
fetch-depth: 2
- name: Setup Node.js 18.x
- name: Setup Node.js 14.x
uses: actions/setup-node@v2
with:
node-version: 18.x
node-version: 14.x
- name: Set up publish access
run: |
npm config set //registry.npmjs.org/:_authToken ${NPM_TOKEN}

View File

@@ -9,10 +9,10 @@ jobs:
steps:
- uses: actions/checkout@v2
- name: Setup Node.js 18.x
- name: Setup Node.js 14.x
uses: actions/setup-node@v2
with:
node-version: 18.x
node-version: 14.x
- name: Install and lint
run: |

View File

@@ -14,10 +14,10 @@ jobs:
with:
token: ${{ secrets.PUSH_TRANSLATIONS_COVERAGE_PAT }}
- name: Setup Node.js 18.x
- name: Setup Node.js 14.x
uses: actions/setup-node@v2
with:
node-version: 18.x
node-version: 14.x
- name: Create report file
run: |

View File

@@ -1,7 +1,7 @@
name: Semantic PR title
on:
pull_request:
pull_request_target:
types:
- opened
- edited

View File

@@ -10,10 +10,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Setup Node.js 18.x
- name: Setup Node.js 14.x
uses: actions/setup-node@v2
with:
node-version: 18.x
node-version: 14.x
- name: Install and build
run: |
yarn --frozen-lockfile

View File

@@ -1,30 +0,0 @@
name: "Bundle Size check @excalidraw/excalidraw"
on:
pull_request:
branches:
- master
jobs:
size:
runs-on: ubuntu-latest
env:
CI_JOB_NUMBER: 1
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Setup Node.js 18.x
uses: actions/setup-node@v3
with:
node-version: 18.x
- name: Install
run: yarn --frozen-lockfile
- name: Install in src/packages/excalidraw
run: yarn --frozen-lockfile
working-directory: src/packages/excalidraw
env:
CI: true
- uses: andresz1/size-limit-action@v1
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
build_script: build:umd
skip_step: install
directory: src/packages/excalidraw

View File

@@ -1,26 +0,0 @@
name: Test Coverage PR
on:
pull_request:
jobs:
coverage:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
steps:
- uses: actions/checkout@v2
- name: "Install Node"
uses: actions/setup-node@v2
with:
node-version: "18.x"
- name: "Install Deps"
run: yarn --frozen-lockfile
- name: "Test Coverage"
run: yarn test:coverage
- name: "Report Coverage"
if: always() # Also generate the report if tests are failing
uses: davelosert/vitest-coverage-report-action@v2
with:
github-token: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -7,10 +7,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Setup Node.js 18.x
- name: Setup Node.js 14.x
uses: actions/setup-node@v2
with:
node-version: 18.x
node-version: 14.x
- name: Install and test
run: |
yarn --frozen-lockfile

2
.gitignore vendored
View File

@@ -26,5 +26,3 @@ src/packages/excalidraw/example/public/bundle.js
src/packages/excalidraw/example/public/excalidraw-assets-dev
src/packages/excalidraw/example/public/excalidraw.development.js
coverage
dev-dist
html

View File

@@ -5,10 +5,9 @@ const { CLIEngine } = require("eslint");
const cli = new CLIEngine({});
module.exports = {
"*.{js,ts,tsx}": (files) => {
"*.{js,ts,tsx}": files => {
return (
"eslint --max-warnings=0 --fix " +
files.filter((file) => !cli.isPathIgnored(file)).join(" ")
"eslint --max-warnings=0 --fix " + files.filter(file => !cli.isPathIgnored(file)).join(" ")
);
},
"*.{css,scss,json,md,html,yml}": ["prettier --write"],

2
.nvmrc
View File

@@ -1 +1 @@
18
14

View File

@@ -1,4 +1,4 @@
FROM node:18 AS build
FROM node:14-alpine AS build
WORKDIR /opt/node_app

View File

@@ -9,7 +9,7 @@ You will need to import the `Footer` component from the package and wrap your co
```jsx live
function App() {
return (
<div style={{ height: "500px" }}>
<div style={{ height: "500px"}}>
<Excalidraw>
<Footer>
<button
@@ -39,7 +39,7 @@ const MobileFooter = ({}) => {
<Footer>
<button
className="custom-footer"
style={{ marginLeft: "20px", height: "2rem" }}
style= {{ marginLeft: '20px', height: '2rem'}}
onClick={() => alert("This is custom footer in mobile menu")}
>
custom footer
@@ -65,4 +65,4 @@ const App = () => (
// Need to render when code is span across multiple components
// in Live Code blocks editor
render(<App />);
```
```

View File

@@ -1,13 +1,7 @@
# initialData
<pre>
&#123; elements?:{" "}
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">
ExcalidrawElement[]
</a>
, appState?: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95">
AppState
</a> &#125;
&#123; elements?: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">ExcalidrawElement[]</a>, appState?: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95">AppState</a> &#125;
</pre>
This helps to load Excalidraw with `initialData`. It must be an object or a [promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/Promise) which resolves to an object containing the below optional fields.
@@ -52,7 +46,7 @@ function App() {
},
],
appState: { zenModeEnabled: true, viewBackgroundColor: "#a5d8ff" },
scrollToContent: true,
scrollToContent: true
}}
/>
</div>

View File

@@ -1,36 +1,34 @@
# Props
All `props` are _optional_.
All `props` are *optional*.
| Name | Type | Default | Description |
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
| [`initialData`](/docs/@excalidraw/excalidraw/api/props/initialdata) | `object` &#124; `null` &#124; <code>Promise<object &#124; null></code> | `null` | The initial data with which app loads. |
| [`ref`](/docs/@excalidraw/excalidraw/api/props/ref) | `object` | \_ | `Ref` to be passed to Excalidraw |
| [`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. |
| --- | --- | --- | --- |
| [`initialData`](/docs/@excalidraw/excalidraw/api/props/initialdata) | `object` &#124; `null` &#124; <code>Promise<object &#124; null></code> | `null` | The initial data with which app loads. |
| [`ref`](/docs/@excalidraw/excalidraw/api/props/ref) | `object` | _ | `Ref` to be passed to Excalidraw |
| [`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. |
| [`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. |
| [`renderSidebar`](/docs/@excalidraw/excalidraw/api/props/render-props#rendersidebar) | `function` | \_ | Render function that renders custom sidebar. |
| [`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. |
| [`renderSidebar`](/docs/@excalidraw/excalidraw/api/props/render-props#rendersidebar) | `function` | _ | Render function that renders custom sidebar. |
| [`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"` &#124; `"dark"` | `"light"` | The theme of the Excalidraw component |
| [`name`](#name) | `string` | | Name of the drawing |
| [`UIOptions`](/docs/@excalidraw/excalidraw/api/props/ui-options) | `object` | [DEFAULT UI OPTIONS](https://github.com/excalidraw/excalidraw/blob/master/src/constants.ts#L151) | To customise UI options. Currently we support customising [`canvas actions`](#canvasactions) |
| [`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 |
| [`renderEmbeddable`](/docs/@excalidraw/excalidraw/api/props/render-props#renderEmbeddable) | `function` | \_ | Render function that can override the built-in `<iframe>` |
| [`generateIdForFile`](#generateidforfile) | `function` | _ | Allows you to override `id` generation for files added on canvas |
### Storing custom data on Excalidraw elements
@@ -95,14 +93,7 @@ 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/src/types.ts#L115">
{" "}
AppState["activeTool"]
</a>
, pointerDownState: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L424">
PointerDownState
</a>) => void
(activeTool: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L115"> AppState["activeTool"]</a>, pointerDownState: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L424">PointerDownState</a>) => void
</pre>
### onScrollChange
@@ -118,11 +109,7 @@ This prop if passed will be triggered when canvas is scrolled and has the below
This callback is triggered if passed when something is pasted into the scene. You can use this callback in case you want to do something additional when the paste event occurs.
<pre>
(data:{" "}
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/clipboard.ts#L18">
ClipboardData
</a>
, event: ClipboardEvent &#124; null) => boolean
(data: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/clipboard.ts#L18">ClipboardData</a>, event: ClipboardEvent &#124; null) => boolean
</pre>
This callback must return a `boolean` value or a [promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/Promise) which resolves to a boolean value.
@@ -148,11 +135,8 @@ It is invoked with empty items when user clears the library. You can use this ca
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()`.
<pre>
(element:{" "}
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">
ExcalidrawElement
</a>
, event: CustomEvent&lt;&#123; nativeEvent: MouseEvent }&gt;) => void
(element: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">ExcalidrawElement</a>,
event: CustomEvent&lt;&#123; nativeEvent: MouseEvent }&gt;) => void
</pre>
Example:
@@ -195,30 +179,31 @@ import { defaultLang, languages } from "@excalidraw/excalidraw";
### viewModeEnabled
This prop indicates whether the app is in `view mode`. When supplied, the value takes precedence over _intialData.appState.viewModeEnabled_, the `view mode` will be fully controlled by the host app, and users won't be able to toggle it from within the app.
This prop indicates whether the app is in `view mode`. When supplied, the value takes precedence over *intialData.appState.viewModeEnabled*, the `view mode` will be fully controlled by the host app, and users won't be able to toggle it from within the app.
### zenModeEnabled
This prop indicates whether the app is in `zen mode`. When supplied, the value takes precedence over _intialData.appState.zenModeEnabled_, the `zen mode` will be fully controlled by the host app, and users won't be able to toggle it from within the app.
This prop indicates whether the app is in `zen mode`. When supplied, the value takes precedence over *intialData.appState.zenModeEnabled*, the `zen mode` will be fully controlled by the host app, and users won't be able to toggle it from within the app.
### gridModeEnabled
This prop indicates whether the shows the grid. When supplied, the value takes precedence over _intialData.appState.gridModeEnabled_, the grid will be fully controlled by the host app, and users won't be able to toggle it from within the app.
This prop indicates whether the shows the grid. When supplied, the value takes precedence over *intialData.appState.gridModeEnabled*, the grid will be fully controlled by the host app, and users won't be able to toggle it from within the app.
### 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.
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
This prop controls Excalidraw's theme. When supplied, the value takes precedence over _intialData.appState.theme_, the theme will be fully controlled by the host app, and users won't be able to toggle it from within the app unless _UIOptions.canvasActions.toggleTheme_ is set to `true`, in which case the `theme` prop will control Excalidraw's default theme with ability to allow theme switching (you must take care of updating the `theme` prop when you detect a change to `appState.theme` from the [onChange](#onchange) callback).
This prop controls Excalidraw's theme. When supplied, the value takes precedence over *intialData.appState.theme*, the theme will be fully controlled by the host app, and users won't be able to toggle it from within the app unless *UIOptions.canvasActions.toggleTheme* is set to `true`, in which case the `theme` prop will control Excalidraw's default theme with ability to allow theme switching (you must take care of updating the `theme` prop when you detect a change to `appState.theme` from the [onChange](#onchange) callback).
You can use [`THEME`](/docs/@excalidraw/excalidraw/api/utils#theme) to specify the theme.
### name
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.
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
@@ -230,6 +215,7 @@ Indicates whether to bind keyboard events to `document`. Disabled by default, me
Enable this if you want Excalidraw to handle keyboard even if the component isn't focused (e.g. a user is interacting with the navbar, sidebar, or similar).
### autoFocus
This prop indicates whether to `focus` the Excalidraw component on page load. Defaults to false.
@@ -239,15 +225,6 @@ This prop indicates whether to `focus` the Excalidraw component on page load. De
Allows you to override `id` generation for files added on canvas (images). By default, an SHA-1 digest of the file is used.
```tsx
(file: File) => string | Promise<string>;
(file: File) => string | Promise<string>
```
### validateEmbeddable
```tsx
validateEmbeddable?: boolean | string[] | RegExp | RegExp[] | ((link: string) => boolean | undefined)
```
This is an optional property. By default we support a handful of well-known sites. You may allow additional sites or disallow the default ones by supplying a custom validator. If you pass `true`, all URLs will be allowed. You can also supply a list of hostnames, RegExp (or list of RegExp objects), or a function. If the function returns `undefined`, the built-in validator will be used.
Supplying a list of hostnames (with or without `www.`) is the preferred way to allow a specific list of domains.

View File

@@ -6,7 +6,8 @@
(isMobile: boolean, appState:
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95">
AppState
</a>) => JSX | null
</a>
) => JSX | null
</pre>
A function returning `JSX` to render `custom` UI in the top right corner of the app.
@@ -73,7 +74,6 @@ You can render `custom sidebar` using this prop. This sidebar is the same that t
You need to import the `Sidebar` component from `excalidraw` package and pass your content as its `children`. The function `renderSidebar` should return the `Sidebar` instance.
### Sidebar
The `<Sidebar>` component takes these props (all are optional except `children`):
| Prop | Type | Description |
@@ -84,7 +84,9 @@ The `<Sidebar>` component takes these props (all are optional except `children`)
| `docked` | `boolean` | Indicates whether the sidebar is`docked`. By default, the sidebar is `undocked`. If passed, the docking becomes controlled, and you are responsible for updating the `docked` state by listening on `onDock` callback. To decide the breakpoint for docking you can use [UIOptions.dockedSidebarBreakpoint](/docs/@excalidraw/excalidraw/api/props/ui-options#dockedsidebarbreakpoint) for more info on docking. |
| `dockable` | `boolean` | Indicates whether to show the `dock` button so that user can `dock` the sidebar. If `false`, you can still dock programmatically by passing `docked` as `true`. |
The sidebar will always include a header with `close / dock` buttons (when applicable). You can also add custom content to the header, by rendering `<Sidebar.Header>` as a child of the `<Sidebar>` component. Note that the custom header will still include the default buttons.
The sidebar will always include a header with `close / dock` buttons (when applicable).
You can also add custom content to the header, by rendering `<Sidebar.Header>` as a child of the `<Sidebar>` component. Note that the custom header will still include the default buttons.
### Sidebar.Header
@@ -100,10 +102,7 @@ function App() {
return (
<div style={{ height: "500px" }}>
<button
className="custom-button"
onClick={() => excalidrawAPI.toggleMenu("customSidebar")}
>
<button className="custom-button" onClick={() => excalidrawAPI.toggleMenu("customSidebar")}>
Toggle Custom Sidebar
</button>
<Excalidraw
@@ -122,20 +121,3 @@ function App() {
);
}
```
## renderEmbeddable
<pre>
(element: NonDeleted&lt;ExcalidrawEmbeddableElement&gt;, appState:{" "}
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95">
AppState
</a>
) => JSX.Element | null
</pre>
Allows you to replace the renderer for embeddable elements (which renders `<iframe>` elements).
| Parameter | Type | Description |
| --- | --- | --- |
| `element` | `NonDeleted<ExcalidrawEmbeddableElement>` | The embeddable element to be rendered. |
| `appState` | `AppState` | The current state of the UI. |

View File

@@ -65,7 +65,7 @@ If user choses to `dock` the sidebar, it will push the right part of the UI towa
function App() {
return (
<div style={{ height: "500px" }}>
<Excalidraw UIOptions={{ dockedSidebarBreakpoint: 200 }} />
<Excalidraw UIOptions={{dockedSidebarBreakpoint: 200}}/>
</div>
);
}

View File

@@ -14,44 +14,35 @@ We're working on much improved export utilities. Stay tuned!
**_Signature_**
<pre>
exportToCanvas(&#123;
<br />
&nbsp; elements,
<br />
&nbsp; appState
<br />
&nbsp; getDimensions,
<br />
&nbsp; files,
<br />
&nbsp; exportPadding?: number;
<br />
&#125;:{" "}
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/packages/utils.ts#L21">
ExportOpts
</a>
exportToCanvas(&#123;<br/>&nbsp;
elements,<br/>&nbsp;
appState<br/>&nbsp;
getDimensions,<br/>&nbsp;
files,<br/>&nbsp;
exportPadding?: number;<br/>
&#125;: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/packages/utils.ts#L21">ExportOpts</a>
</pre>
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `elements` | [Excalidraw Element []](https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114) | | The elements to be exported to canvas. |
| `appState` | [AppState](https://github.com/excalidraw/excalidraw/blob/master/src/packages/utils.ts#L23) | [Default App State](https://github.com/excalidraw/excalidraw/blob/master/src/appState.ts#L17) | The app state of the scene. |
| [`getDimensions`](#getdimensions) | `function` | \_ | A function which returns the `width`, `height`, and optionally `scale` (defaults to `1`), with which canvas is to be exported. |
| `maxWidthOrHeight` | `number` | \_ | The maximum `width` or `height` of the exported image. If provided, `getDimensions` is ignored. |
| `files` | [BinaryFiles](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L59) | \_ | The files added to the scene. |
| [`getDimensions`](#getdimensions) | `function` | _ | A function which returns the `width`, `height`, and optionally `scale` (defaults to `1`), with which canvas is to be exported. |
| `maxWidthOrHeight` | `number` | _ | The maximum `width` or `height` of the exported image. If provided, `getDimensions` is ignored. |
| `files` | [BinaryFiles](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L59) | _ | The files added to the scene. |
| `exportPadding` | `number` | `10` | The `padding` to be added on canvas. |
#### getDimensions
```tsx
(width: number, height: number) => {
(width: number, height: number) => {
width: number,
height: number,
scale?: number
height: number,
scale?: number
}
```
A function which returns the `width`, `height`, and optionally `scale` (defaults to `1`), with which canvas is to be exported.
A function which returns the `width`, `height`, and optionally `scale` (defaults to `1`), with which canvas is to be exported.
**How to use**
@@ -66,17 +57,17 @@ function App() {
const [canvasUrl, setCanvasUrl] = useState("");
const [excalidrawAPI, setExcalidrawAPI] = useState(null);
return (
return (
<>
<button
className="custom-button"
onClick={async () => {
if (!excalidrawAPI) {
return;
return
}
const elements = excalidrawAPI.getSceneElements();
if (!elements || !elements.length) {
return;
return
}
const canvas = await exportToCanvas({
elements,
@@ -85,9 +76,7 @@ function App() {
exportWithDarkMode: false,
},
files: excalidrawAPI.getFiles(),
getDimensions: () => {
return { width: 350, height: 350 };
},
getDimensions: () => { return {width: 350, height: 350}}
});
const ctx = canvas.getContext("2d");
ctx.font = "30px Virgil";
@@ -101,38 +90,31 @@ function App() {
<img src={canvasUrl} alt="" />
</div>
<div style={{ height: "400px" }}>
<Excalidraw ref={(api) => setExcalidrawAPI(api)} />
<Excalidraw ref={(api) => setExcalidrawAPI(api)}
/>
</div>
</>
);
)
}
```
### exportToBlob
**_Signature_**
<pre>
exportToBlob(
<br />
&nbsp; opts:{" "}
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/packages/utils.ts#L14">
ExportOpts
</a>{" "}
& &#123;
<br />
&nbsp; mimeType?: string,
<br />
&nbsp; quality?: number,
<br />
&nbsp; exportPadding?: number;
<br />
})
exportToBlob(<br/>&nbsp;
opts: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/packages/utils.ts#L14">ExportOpts</a> & &#123;<br/>&nbsp;
mimeType?: string,<br/>&nbsp;
quality?: number,<br/>&nbsp;
exportPadding?: number;<br/>
})
</pre>
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `opts` | `object` | \_ | This param is passed to `exportToCanvas`. You can refer to [`exportToCanvas`](#exporttocanvas) |
| `opts` | `object` | _ | This param is passed to `exportToCanvas`. You can refer to [`exportToCanvas`](#exporttocanvas) |
| `mimeType` | `string` | `image/png` | Indicates the image format. |
| `quality` | `number` | `0.92` | A value between `0` and `1` indicating the [image quality](https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob#parameters). Applies only to `image/jpeg`/`image/webp` MIME types. |
| `exportPadding` | `number` | `10` | The padding to be added on canvas. |
@@ -150,31 +132,26 @@ Returns a promise which resolves with a [blob](https://developer.mozilla.org/en-
**_Signature_**
<pre>
exportToSvg(&#123;
<br />
&nbsp; elements:&nbsp;
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">
ExcalidrawElement[]
</a>,<br />
&nbsp; appState:
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95">
{" "}
AppState
</a>,<br />
&nbsp; exportPadding: number,
<br />
&nbsp; metadata: string,
<br />
&nbsp; files:&nbsp;
exportToSvg(&#123;<br/>&nbsp;
elements:&nbsp;
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">
ExcalidrawElement[]
</a>,<br/>&nbsp;
appState:
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95"> AppState
</a>,<br/>&nbsp;
exportPadding: number,<br/>&nbsp;
metadata: string,<br/>&nbsp;
files:&nbsp;
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L59">
BinaryFiles
</a>,<br />
&#125;);
BinaryFiles
</a>,<br/>
&#125;);
</pre>
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| elements | [Excalidraw Element []](https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114) | | The elements to exported as `svg ` |
| elements | [Excalidraw Element []](https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114) | | The elements to exported as `svg `|
| appState | [AppState](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95) | [defaultAppState](https://github.com/excalidraw/excalidraw/blob/master/src/appState.ts#L11) | The `appState` of the scene |
| exportPadding | number | 10 | The `padding` to be added on canvas |
| files | [BinaryFiles](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L64) | undefined | The `files` added to the scene. |
@@ -186,21 +163,12 @@ This function returns a promise which resolves to `svg` of the exported drawing.
**_Signature_**
<pre>
exportToClipboard(
<br />
&nbsp; opts:{" "}
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/packages/utils.ts#L21">
ExportOpts
</a>{" "}
& &#123;
<br />
&nbsp; mimeType?: string,
<br />
&nbsp; quality?: number;
<br />
&nbsp; type: 'png' | 'svg' |'json'
<br />
})
exportToClipboard(<br/>&nbsp;
opts: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/packages/utils.ts#L21">ExportOpts</a> & &#123;<br/>&nbsp;
mimeType?: string,<br/>&nbsp;
quality?: number;<br/>&nbsp;
type: 'png' | 'svg' |'json'<br/>
})
</pre>
| Name | Type | Default | Description |
@@ -208,7 +176,7 @@ This function returns a promise which resolves to `svg` of the exported drawing.
| `opts` | | | This param is same as the params passed to `exportToCanvas`. You can refer to [`exportToCanvas`](#exporttocanvas). |
| `mimeType` | `string` | `image/png` | Indicates the image format, this will be used when exporting as `png`. |
| `quality` | `number` | `0.92` | A value between `0` and `1` indicating the [image quality](https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob#parameters). Applies only to `image/jpeg` / `image/webp` MIME types. This will be used when exporting as `png`. |
| `type` | 'png' &#124; 'svg' &#124; 'json' | \_ | This determines the format to which the scene data should be `exported`. |
| `type` | 'png' &#124; 'svg' &#124; 'json' | _ | This determines the format to which the scene data should be `exported`. |
**How to use**

View File

@@ -8,15 +8,7 @@ id: "restore"
**_Signature_**
<pre>
restoreAppState(appState:{" "}
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L34">
ImportedDataState["appState"]
</a>
,<br />
&nbsp; localAppState: Partial&lt;
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95">
AppState
</a>> | null): <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95">AppState</a>
restoreAppState(appState: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L34">ImportedDataState["appState"]</a>,<br/>&nbsp; localAppState: Partial&lt;<a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95">AppState</a>> | null): <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95">AppState</a>
</pre>
**_How to use_**
@@ -28,36 +20,26 @@ import { restoreAppState } from "@excalidraw/excalidraw";
This function will make sure all the `keys` have appropriate `values` in [appState](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95) and if any key is missing, it will be set to its `default` value.
When `localAppState` is supplied, it's used in place of values that are missing (`undefined`) in `appState` instead of the defaults.
Use this as a way to not override user's defaults if you persist them. You can pass `null` / `undefined` if not applicable.
Use this as a way to not override user's defaults if you persist them.
You can pass `null` / `undefined` if not applicable.
### restoreElements
**_Signature_**
<pre>
restoreElements( elements:{" "}
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">
ImportedDataState["elements"]
</a>
,<br />
&nbsp; localElements:{" "}
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">
ExcalidrawElement[]
</a>{" "}
| null | undefined):{" "}
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">
ExcalidrawElement[]
</a>
,<br />
&nbsp; opts: &#123; refreshDimensions?: boolean, repairBindings?: boolean }
<br />)
restoreElements(
elements: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">ImportedDataState["elements"]</a>,<br/>&nbsp;
localElements: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">ExcalidrawElement[]</a> | null | undefined): <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">ExcalidrawElement[]</a>,<br/>&nbsp;
opts: &#123; refreshDimensions?: boolean, repairBindings?: boolean }<br/>
)
</pre>
| Prop | Type | Description |
| --- | --- | --- |
| ---- | ---- | ---- |
| `elements` | <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">ImportedDataState["elements"]</a> | The `elements` to be restored |
| [`localElements`](#localelements) | <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">ExcalidrawElement[]</a> &#124; null &#124; undefined | When `localElements` are supplied, they are used to ensure that existing restored elements reuse `version` (and increment it), and regenerate `versionNonce`. |
| [`opts`](#opts) | `Object` | The extra optional parameter to configure restored elements |
| [`localElements`](#localelements) | <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">ExcalidrawElement[]</a> &#124; null &#124; undefined | When `localElements` are supplied, they are used to ensure that existing restored elements reuse `version` (and increment it), and regenerate `versionNonce`. |
| [`opts`](#opts) | `Object` | The extra optional parameter to configure restored elements
#### localElements
@@ -65,13 +47,12 @@ When `localElements` are supplied, they are used to ensure that existing restore
Use this when you `import` elements which may already be present in the scene to ensure that you do not disregard the newly imported elements if you're using element version to detect the update
#### opts
The extra optional parameter to configure restored elements. It has the following attributes
| Prop | Type | Description |
| --- | --- | --- |
| 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. |
| `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. |
**_How to use_**
@@ -88,29 +69,13 @@ Parameter `refreshDimensions` indicates whether we should also `recalculate` tex
**_Signature_**
<pre>
restore( data:{" "}
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L34">
ImportedDataState
</a>
,<br />
&nbsp; localAppState: Partial&lt;
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95">
AppState
</a>
> | null | undefined,
<br />
&nbsp; localElements:{" "}
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">
ExcalidrawElement[]
</a>{" "}
| null | undefined
<br />
):{" "}
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L4">
DataState
</a>
<br />
opts: &#123; refreshDimensions?: boolean, repairBindings?: boolean }<br />)
restore(
data: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L34">ImportedDataState</a>,<br/>&nbsp;
localAppState: Partial&lt;<a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95">AppState</a>> | null | undefined,<br/>&nbsp;
localElements: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">ExcalidrawElement[]</a> | null | undefined<br/>): <a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L4">DataState</a><br/>
opts: &#123; refreshDimensions?: boolean, repairBindings?: boolean }<br/>
)
</pre>
See [`restoreAppState()`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#restoreAppState) about `localAppState`, and [`restoreElements()`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#restoreElements) about `localElements`.
@@ -128,12 +93,8 @@ This function makes sure elements and state is set to appropriate values and set
**_Signature_**
<pre>
restoreLibraryItems(libraryItems:{" "}
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L34">
ImportedDataState["libraryItems"]
</a>
,<br />
&nbsp; defaultStatus: "published" | "unpublished")
restoreLibraryItems(libraryItems: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L34">ImportedDataState["libraryItems"]</a>,<br/>&nbsp;
defaultStatus: "published" | "unpublished")
</pre>
**_How to use_**

View File

@@ -38,7 +38,6 @@ For a complete list of variables, check [theme.scss](https://github.com/excalidr
--color-primary-light: #dcbec9;
}
```
```tsx live
function App() {
return (

View File

@@ -6,11 +6,13 @@ No, Excalidraw package doesn't come with collaboration built in, since the imple
### Turning off Aggressive Anti-Fingerprinting in Brave browser
When _Aggressive Anti-Fingerprinting_ is turned on, the `measureText` API breaks which in turn breaks the Text Elements in your drawings. Here is more [info](https://github.com/excalidraw/excalidraw/pull/6336) on the same.
When *Aggressive Anti-Fingerprinting* is turned on, the `measureText` API breaks which in turn breaks the Text Elements in your drawings. Here is more [info](https://github.com/excalidraw/excalidraw/pull/6336) on the same.
We strongly recommend turning it off. You can follow the steps below on how to do so.
1. Open [excalidraw.com](https://excalidraw.com) in Brave and click on the **Shield** button ![Shield button](../../assets/brave-shield.png)
1. Open [excalidraw.com](https://excalidraw.com) in Brave and click on the **Shield** button
![Shield button](../../assets/brave-shield.png)
<div style={{width:'30rem'}}>
@@ -28,6 +30,8 @@ We strongly recommend turning it off. You can follow the steps below on how to d
If disabling this setting doesn't fix the display of text elements, please consider opening an [issue](https://github.com/excalidraw/excalidraw/issues/new) on our GitHub, or message us on [Discord](https://discord.gg/UexuTaE).
## Need help?
Check out the existing [Q&A](https://github.com/excalidraw/excalidraw/discussions?discussions_q=label%3Apackage%3Aexcalidraw). If you have any queries or need help, ask us [here](https://github.com/excalidraw/excalidraw/discussions?discussions_q=label%3Apackage%3Aexcalidraw).

View File

@@ -41,9 +41,7 @@ import { useState, useEffect } from "react";
export default function App() {
const [Excalidraw, setExcalidraw] = useState(null);
useEffect(() => {
import("@excalidraw/excalidraw").then((comp) =>
setExcalidraw(comp.Excalidraw),
);
import("@excalidraw/excalidraw").then((comp) => setExcalidraw(comp.Excalidraw));
}, []);
return <>{Excalidraw && <Excalidraw />}</>;
}
@@ -82,7 +80,7 @@ import TabItem from "@theme/TabItem";
<TabItem value="html" label="html">
```html
<!doctype html>
<!DOCTYPE html>
<html>
<head>
<title>Excalidraw in browser</title>

View File

@@ -2,7 +2,8 @@
Pull requests are welcome. For major changes, please [open an issue](https://github.com/excalidraw/excalidraw/issues/new) first to discuss what you would like to change.
We have a [roadmap](https://github.com/orgs/excalidraw/projects/3) which we strongly recommend to go through and check if something interests you. For new contributors we would recommend to start with _Easy_ tasks.
We have a [roadmap](https://github.com/orgs/excalidraw/projects/3) which we strongly recommend to go through and check if something interests you.
For new contributors we would recommend to start with *Easy* tasks.
In case you want to pick up something from the roadmap, comment on that issue and one of the project maintainers will assign it to you, post which you can discuss in the issue and start working on it.
@@ -68,7 +69,6 @@ It's also a good idea to consider if your change should include additional tests
Finally - always manually test your changes using the convenient staging environment deployed for each pull request. As much as local development attempts to replicate production, there can still be subtle differences in behavior. For larger features consider testing your change in multiple browsers as well.
:::note Some checks, such as the `lint` and `test`, require approval from the maintainers to run. They will appear as `Expected — Waiting for status to be reported` in the PR checks when they are waiting for approval. :::
## Translating

View File

@@ -18,7 +18,7 @@
"@docusaurus/core": "2.2.0",
"@docusaurus/preset-classic": "2.2.0",
"@docusaurus/theme-live-codeblock": "2.2.0",
"@excalidraw/excalidraw": "0.15.3",
"@excalidraw/excalidraw": "0.15.2",
"@mdx-js/react": "^1.6.22",
"clsx": "^1.2.1",
"docusaurus-plugin-sass": "0.2.3",

View File

@@ -1631,10 +1631,10 @@
url-loader "^4.1.1"
webpack "^5.73.0"
"@excalidraw/excalidraw@0.15.3":
version "0.15.3"
resolved "https://registry.yarnpkg.com/@excalidraw/excalidraw/-/excalidraw-0.15.3.tgz#5dea570f76451adf68bc24d4bfdd67a375cfeab1"
integrity sha512-/gpY7fgMO/AEaFLWnPqzbY8H7ly+/zocFf7D0Is5sWNMD2mhult5tana12lXKLSJ6EAz7ubo1A7LajXzvJXJDA==
"@excalidraw/excalidraw@0.15.2":
version "0.15.2"
resolved "https://registry.yarnpkg.com/@excalidraw/excalidraw/-/excalidraw-0.15.2.tgz#7dba4f6e10c52015a007efb75a9fc1afe598574c"
integrity sha512-rTI02kgWSTXiUdIkBxt9u/581F3eXcqQgJdIxmz54TFtG3ughoxO5fr4t7Fr2LZIturBPqfocQHGKZ0t2KLKgw==
"@hapi/hoek@^9.0.0":
version "9.3.0"
@@ -6611,19 +6611,19 @@ semver@7.0.0:
integrity sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==
semver@^5.4.1:
version "5.7.2"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.2.tgz#48d55db737c3287cd4835e17fa13feace1c41ef8"
integrity sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==
version "5.7.1"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.2.0, semver@^6.3.0:
version "6.3.1"
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4"
integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==
version "6.3.0"
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7:
version "7.5.4"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e"
integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==
version "7.3.7"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.7.tgz#12c5b649afdbf9049707796e22a4028814ce523f"
integrity sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==
dependencies:
lru-cache "^6.0.0"

View File

@@ -51,12 +51,26 @@
"pwacompat": "2.0.17",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-scripts": "5.0.1",
"roughjs": "4.5.2",
"sass": "1.51.0",
"socket.io-client": "2.3.1",
"tunnel-rat": "0.1.2"
"socket.io-client": "4.6.1",
"tunnel-rat": "0.1.2",
"workbox-background-sync": "^6.5.4",
"workbox-broadcast-update": "^6.5.4",
"workbox-cacheable-response": "^6.5.4",
"workbox-core": "^6.5.4",
"workbox-expiration": "^6.5.4",
"workbox-google-analytics": "^6.5.4",
"workbox-navigation-preload": "^6.5.4",
"workbox-precaching": "^6.5.4",
"workbox-range-requests": "^6.5.4",
"workbox-routing": "^6.5.4",
"workbox-strategies": "^6.5.4",
"workbox-streams": "^6.5.4"
},
"devDependencies": {
"@excalidraw/eslint-config": "1.0.0",
"@excalidraw/prettier-config": "1.0.2",
"@types/chai": "4.3.0",
"@types/jest": "27.4.0",
@@ -67,68 +81,68 @@
"@types/react-dom": "18.0.6",
"@types/resize-observer-browser": "0.1.7",
"@types/socket.io-client": "1.4.36",
"@typescript-eslint/eslint-plugin": "6.5.0",
"@typescript-eslint/parser": "6.5.0",
"@vitejs/plugin-react": "3.1.0",
"@vitest/coverage-v8": "0.33.0",
"@vitest/ui": "0.32.2",
"chai": "4.3.6",
"dotenv": "16.0.1",
"eslint": "8.48.0",
"eslint-config-prettier": "9.0.0",
"eslint-plugin-react": "7.33.2",
"eslint-plugin-react-hooks": "4.6.0",
"eslint-config-prettier": "8.5.0",
"eslint-plugin-prettier": "3.3.1",
"http-server": "14.1.1",
"husky": "7.0.4",
"jsdom": "22.1.0",
"jest-canvas-mock": "2.4.0",
"lint-staged": "12.3.7",
"pepjs": "0.5.3",
"prettier": "3.0.3",
"prettier": "2.6.2",
"rewire": "6.0.0",
"typescript": "4.9.4",
"vite": "4.4.2",
"vite-plugin-checker": "0.6.1",
"vite-plugin-ejs": "1.6.4",
"vite-plugin-pwa": "0.16.4",
"vite-plugin-svgr": "2.4.0",
"vitest": "0.34.1",
"vitest-canvas-mock": "0.3.2"
"typescript": "4.9.4"
},
"engines": {
"node": ">=18.0.0"
"node": ">=14.0.0"
},
"homepage": ".",
"jest": {
"collectCoverageFrom": [
"src/**/*.{js,jsx,ts,tsx}"
],
"coveragePathIgnorePatterns": [
"<rootDir>/locales",
"<rootDir>/src/packages/excalidraw/dist/",
"<rootDir>/src/packages/excalidraw/types",
"<rootDir>/src/packages/excalidraw/example"
],
"transformIgnorePatterns": [
"node_modules/(?!(roughjs|points-on-curve|path-data-parser|points-on-path|browser-fs-access|canvas-roundrect-polyfill)/)"
],
"resetMocks": false
},
"name": "excalidraw",
"prettier": "@excalidraw/prettier-config",
"private": true,
"scripts": {
"build-node": "node ./scripts/build-node.js",
"build:app:docker": "cross-env VITE_APP_DISABLE_SENTRY=true VITE_APP_DISABLE_TRACKING=true vite build",
"build:app": "cross-env VITE_APP_GIT_SHA=$VERCEL_GIT_COMMIT_SHA vite build",
"build:app:docker": "cross-env REACT_APP_DISABLE_SENTRY=true REACT_APP_DISABLE_TRACKING=true react-scripts build",
"build:app": "cross-env REACT_APP_GIT_SHA=$VERCEL_GIT_COMMIT_SHA react-scripts build",
"build:version": "node ./scripts/build-version.js",
"build": "yarn build:app && yarn build:version",
"eject": "react-scripts eject",
"fix:code": "yarn test:code --fix",
"fix:other": "yarn prettier --write",
"fix": "yarn fix:other && yarn fix:code",
"locales-coverage": "node scripts/build-locales-coverage.js",
"locales-coverage:description": "node scripts/locales-coverage-description.js",
"prepare": "husky install",
"prettier": "prettier . --ignore-path=.eslintignore",
"start": "vite",
"prettier": "prettier \"**/*.{css,scss,json,md,html,yml}\" --ignore-path=.eslintignore",
"start": "react-scripts start",
"start:production": "npm run build && npx http-server build -a localhost -p 5001 -o",
"test:all": "yarn test:typecheck && yarn test:code && yarn test:other && yarn test:app --watch=false",
"test:app": "vitest --config vitest.config.ts",
"test:all": "yarn test:typecheck && yarn test:code && yarn test:other && yarn test:app --watchAll=false",
"test:app": "react-scripts test --passWithNoTests",
"test:code": "eslint --max-warnings=0 --ext .js,.ts,.tsx .",
"test:debug": "react-scripts --inspect-brk test --runInBand --no-cache",
"test:other": "yarn prettier --list-different",
"test:typecheck": "tsc",
"test:update": "yarn test:app --update --watch=false",
"test:update": "yarn test:app --updateSnapshot --watchAll=false",
"test": "yarn test:app",
"test:coverage": "vitest --coverage",
"test:coverage:watch": "vitest --coverage --watch",
"test:ui": "yarn test --ui",
"test:coverage": "react-scripts test --passWithNoTests --coverage --watchAll",
"autorelease": "node scripts/autorelease.js",
"prerelease": "node scripts/prerelease.js",
"build:preview": "yarn build && vite preview --port 5000",
"release": "node scripts/release.js"
}
}

View File

@@ -1,4 +1,4 @@
<!doctype html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
@@ -78,7 +78,8 @@
}
</style>
<!------------------------------------------------------------------------->
<% if ("%PROD%" === "true") { %>
<% if (process.env.NODE_ENV === "production") { %>
<script>
// Redirect Excalidraw+ users which have auto-redirect enabled.
//
@@ -99,35 +100,41 @@
</script>
<% } %>
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon" />
<link rel="shortcut icon" href="favicon.ico" type="image/x-icon" />
<!-- Excalidraw version -->
<meta name="version" content="{version}" />
<link
rel="preload"
href="/Virgil.woff2"
href="Virgil.woff2"
as="font"
type="font/woff2"
crossorigin="anonymous"
/>
<link
rel="preload"
href="/Cascadia.woff2"
href="Cascadia.woff2"
as="font"
type="font/woff2"
crossorigin="anonymous"
/>
<link rel="stylesheet" href="/fonts.css" type="text/css" />
<% if ("%VITE_APP_DEV_DISABLE_LIVE_RELOAD%"==="true" ) { %>
<link
rel="manifest"
href="manifest.json"
style="--pwacompat-splash-font: 24px Virgil"
/>
<link rel="stylesheet" href="fonts.css" type="text/css" />
<% if (process.env.REACT_APP_DEV_DISABLE_LIVE_RELOAD==="true" ) { %>
<script>
{
const _WebSocket = window.WebSocket;
window.WebSocket = function (url) {
if (/ws:\/\/localhost:.+?\/sockjs-node/.test(url)) {
console.info(
"[!!!] Live reload is disabled via VITE_APP_DEV_DISABLE_LIVE_RELOAD [!!!]",
"[!!!] Live reload is disabled via process.env.REACT_APP_DEV_DISABLE_LIVE_RELOAD [!!!]",
);
} else {
return new _WebSocket(url);
@@ -193,8 +200,7 @@
<h1 class="visually-hidden">Excalidraw</h1>
</header>
<div id="root"></div>
<script type="module" src="/src/index.tsx"></script>
<% if ("%VITE_APP_DEV_DISABLE_LIVE_RELOAD%" !== 'true') { %>
<% if (process.env.REACT_APP_DISABLE_TRACKING !== 'true') { %>
<!-- 100% privacy friendly analytics -->
<script>
// need to load this script dynamically bcs. of iframe embed tracking

View File

@@ -1,20 +0,0 @@
// Since we migrated to Vite, the service worker strategy changed, in CRA it was a custom service worker named service-worker.js and in Vite its sw.js handled by vite-plugin-pwa
// Due to this the existing CRA users were not able to migrate to Vite or any new changes post Vite unless browser is hard refreshed
// Hence adding a self destroying worker so all CRA service workers are destroyed and migrated to Vite
// We should remove this code after sometime when we are confident that
// all users have migrated to Vite
self.addEventListener("install", () => {
self.skipWaiting();
});
self.addEventListener("activate", () => {
self.registration
.unregister()
.then(() => {
return self.clients.matchAll();
})
.then((clients) => {
clients.forEach((client) => client.navigate(client.url));
});
});

View File

@@ -0,0 +1,2 @@
this.workbox=this.workbox||{},this.workbox.backgroundSync=function(t,e,s){"use strict";try{self["workbox:background-sync:4.3.1"]&&_()}catch(t){}const i=3,n="workbox-background-sync",a="requests",r="queueName";class c{constructor(t){this.t=t,this.s=new s.DBWrapper(n,i,{onupgradeneeded:this.i})}async pushEntry(t){delete t.id,t.queueName=this.t,await this.s.add(a,t)}async unshiftEntry(t){const[e]=await this.s.getAllMatching(a,{count:1});e?t.id=e.id-1:delete t.id,t.queueName=this.t,await this.s.add(a,t)}async popEntry(){return this.h({direction:"prev"})}async shiftEntry(){return this.h({direction:"next"})}async getAll(){return await this.s.getAllMatching(a,{index:r,query:IDBKeyRange.only(this.t)})}async deleteEntry(t){await this.s.delete(a,t)}async h({direction:t}){const[e]=await this.s.getAllMatching(a,{direction:t,index:r,query:IDBKeyRange.only(this.t),count:1});if(e)return await this.deleteEntry(e.id),e}i(t){const e=t.target.result;t.oldVersion>0&&t.oldVersion<i&&e.objectStoreNames.contains(a)&&e.deleteObjectStore(a),e.createObjectStore(a,{autoIncrement:!0,keyPath:"id"}).createIndex(r,r,{unique:!1})}}const h=["method","referrer","referrerPolicy","mode","credentials","cache","redirect","integrity","keepalive"];class o{static async fromRequest(t){const e={url:t.url,headers:{}};"GET"!==t.method&&(e.body=await t.clone().arrayBuffer());for(const[s,i]of t.headers.entries())e.headers[s]=i;for(const s of h)void 0!==t[s]&&(e[s]=t[s]);return new o(e)}constructor(t){"navigate"===t.mode&&(t.mode="same-origin"),this.o=t}toObject(){const t=Object.assign({},this.o);return t.headers=Object.assign({},this.o.headers),t.body&&(t.body=t.body.slice(0)),t}toRequest(){return new Request(this.o.url,this.o)}clone(){return new o(this.toObject())}}const u="workbox-background-sync",y=10080,w=new Set;class d{constructor(t,{onSync:s,maxRetentionTime:i}={}){if(w.has(t))throw new e.WorkboxError("duplicate-queue-name",{name:t});w.add(t),this.u=t,this.l=s||this.replayRequests,this.q=i||y,this.m=new c(this.u),this.p()}get name(){return this.u}async pushRequest(t){await this.g(t,"push")}async unshiftRequest(t){await this.g(t,"unshift")}async popRequest(){return this.R("pop")}async shiftRequest(){return this.R("shift")}async getAll(){const t=await this.m.getAll(),e=Date.now(),s=[];for(const i of t){const t=60*this.q*1e3;e-i.timestamp>t?await this.m.deleteEntry(i.id):s.push(f(i))}return s}async g({request:t,metadata:e,timestamp:s=Date.now()},i){const n={requestData:(await o.fromRequest(t.clone())).toObject(),timestamp:s};e&&(n.metadata=e),await this.m[`${i}Entry`](n),this.k?this.D=!0:await this.registerSync()}async R(t){const e=Date.now(),s=await this.m[`${t}Entry`]();if(s){const i=60*this.q*1e3;return e-s.timestamp>i?this.R(t):f(s)}}async replayRequests(){let t;for(;t=await this.shiftRequest();)try{await fetch(t.request.clone())}catch(s){throw await this.unshiftRequest(t),new e.WorkboxError("queue-replay-failed",{name:this.u})}}async registerSync(){if("sync"in registration)try{await registration.sync.register(`${u}:${this.u}`)}catch(t){}}p(){"sync"in registration?self.addEventListener("sync",t=>{if(t.tag===`${u}:${this.u}`){const e=async()=>{let e;this.k=!0;try{await this.l({queue:this})}catch(t){throw e=t}finally{!this.D||e&&!t.lastChance||await this.registerSync(),this.k=!1,this.D=!1}};t.waitUntil(e())}}):this.l({queue:this})}static get _(){return w}}const f=t=>{const e={request:new o(t.requestData).toRequest(),timestamp:t.timestamp};return t.metadata&&(e.metadata=t.metadata),e};return t.Queue=d,t.Plugin=class{constructor(...t){this.v=new d(...t),this.fetchDidFail=this.fetchDidFail.bind(this)}async fetchDidFail({request:t}){await this.v.pushRequest({request:t})}},t}({},workbox.core._private,workbox.core._private);
//# sourceMappingURL=workbox-background-sync.prod.js.map

View File

@@ -0,0 +1,2 @@
this.workbox=this.workbox||{},this.workbox.broadcastUpdate=function(e,t){"use strict";try{self["workbox:broadcast-update:4.3.1"]&&_()}catch(e){}const s=(e,t,s)=>{return!s.some(s=>e.headers.has(s)&&t.headers.has(s))||s.every(s=>{const n=e.headers.has(s)===t.headers.has(s),a=e.headers.get(s)===t.headers.get(s);return n&&a})},n="workbox",a=1e4,i=["content-length","etag","last-modified"],o=async({channel:e,cacheName:t,url:s})=>{const n={type:"CACHE_UPDATED",meta:"workbox-broadcast-update",payload:{cacheName:t,updatedURL:s}};if(e)e.postMessage(n);else{const e=await clients.matchAll({type:"window"});for(const t of e)t.postMessage(n)}};class c{constructor({headersToCheck:e,channelName:t,deferNoticationTimeout:s}={}){this.t=e||i,this.s=t||n,this.i=s||a,this.o()}notifyIfUpdated({oldResponse:e,newResponse:t,url:n,cacheName:a,event:i}){if(!s(e,t,this.t)){const e=(async()=>{i&&i.request&&"navigate"===i.request.mode&&await this.h(i),await this.l({channel:this.u(),cacheName:a,url:n})})();if(i)try{i.waitUntil(e)}catch(e){}return e}}async l(e){await o(e)}u(){return"BroadcastChannel"in self&&!this.p&&(this.p=new BroadcastChannel(this.s)),this.p}h(e){if(!this.m.has(e)){const s=new t.Deferred;this.m.set(e,s);const n=setTimeout(()=>{s.resolve()},this.i);s.promise.then(()=>clearTimeout(n))}return this.m.get(e).promise}o(){this.m=new Map,self.addEventListener("message",e=>{if("WINDOW_READY"===e.data.type&&"workbox-window"===e.data.meta&&this.m.size>0){for(const e of this.m.values())e.resolve();this.m.clear()}})}}return e.BroadcastCacheUpdate=c,e.Plugin=class{constructor(e){this.l=new c(e)}cacheDidUpdate({cacheName:e,oldResponse:t,newResponse:s,request:n,event:a}){t&&this.l.notifyIfUpdated({cacheName:e,oldResponse:t,newResponse:s,event:a,url:n.url})}},e.broadcastUpdate=o,e.responsesAreSame=s,e}({},workbox.core._private);
//# sourceMappingURL=workbox-broadcast-update.prod.js.map

View File

@@ -0,0 +1,2 @@
this.workbox=this.workbox||{},this.workbox.cacheableResponse=function(t){"use strict";try{self["workbox:cacheable-response:4.3.1"]&&_()}catch(t){}class s{constructor(t={}){this.t=t.statuses,this.s=t.headers}isResponseCacheable(t){let s=!0;return this.t&&(s=this.t.includes(t.status)),this.s&&s&&(s=Object.keys(this.s).some(s=>t.headers.get(s)===this.s[s])),s}}return t.CacheableResponse=s,t.Plugin=class{constructor(t){this.i=new s(t)}cacheWillUpdate({response:t}){return this.i.isResponseCacheable(t)?t:null}},t}({});
//# sourceMappingURL=workbox-cacheable-response.prod.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,2 @@
this.workbox=this.workbox||{},this.workbox.expiration=function(t,e,s,i,a,n){"use strict";try{self["workbox:expiration:4.3.1"]&&_()}catch(t){}const h="workbox-expiration",c="cache-entries",r=t=>{const e=new URL(t,location);return e.hash="",e.href};class o{constructor(t){this.t=t,this.s=new e.DBWrapper(h,1,{onupgradeneeded:t=>this.i(t)})}i(t){const e=t.target.result.createObjectStore(c,{keyPath:"id"});e.createIndex("cacheName","cacheName",{unique:!1}),e.createIndex("timestamp","timestamp",{unique:!1}),s.deleteDatabase(this.t)}async setTimestamp(t,e){t=r(t),await this.s.put(c,{url:t,timestamp:e,cacheName:this.t,id:this.h(t)})}async getTimestamp(t){return(await this.s.get(c,this.h(t))).timestamp}async expireEntries(t,e){const s=await this.s.transaction(c,"readwrite",(s,i)=>{const a=s.objectStore(c),n=[];let h=0;a.index("timestamp").openCursor(null,"prev").onsuccess=(({target:s})=>{const a=s.result;if(a){const s=a.value;s.cacheName===this.t&&(t&&s.timestamp<t||e&&h>=e?n.push(a.value):h++),a.continue()}else i(n)})}),i=[];for(const t of s)await this.s.delete(c,t.id),i.push(t.url);return i}h(t){return this.t+"|"+r(t)}}class u{constructor(t,e={}){this.o=!1,this.u=!1,this.l=e.maxEntries,this.p=e.maxAgeSeconds,this.t=t,this.m=new o(t)}async expireEntries(){if(this.o)return void(this.u=!0);this.o=!0;const t=this.p?Date.now()-1e3*this.p:void 0,e=await this.m.expireEntries(t,this.l),s=await caches.open(this.t);for(const t of e)await s.delete(t);this.o=!1,this.u&&(this.u=!1,this.expireEntries())}async updateTimestamp(t){await this.m.setTimestamp(t,Date.now())}async isURLExpired(t){return await this.m.getTimestamp(t)<Date.now()-1e3*this.p}async delete(){this.u=!1,await this.m.expireEntries(1/0)}}return t.CacheExpiration=u,t.Plugin=class{constructor(t={}){this.D=t,this.p=t.maxAgeSeconds,this.g=new Map,t.purgeOnQuotaError&&n.registerQuotaErrorCallback(()=>this.deleteCacheAndMetadata())}k(t){if(t===a.cacheNames.getRuntimeName())throw new i.WorkboxError("expire-custom-caches-only");let e=this.g.get(t);return e||(e=new u(t,this.D),this.g.set(t,e)),e}cachedResponseWillBeUsed({event:t,request:e,cacheName:s,cachedResponse:i}){if(!i)return null;let a=this.N(i);const n=this.k(s);n.expireEntries();const h=n.updateTimestamp(e.url);if(t)try{t.waitUntil(h)}catch(t){}return a?i:null}N(t){if(!this.p)return!0;const e=this._(t);return null===e||e>=Date.now()-1e3*this.p}_(t){if(!t.headers.has("date"))return null;const e=t.headers.get("date"),s=new Date(e).getTime();return isNaN(s)?null:s}async cacheDidUpdate({cacheName:t,request:e}){const s=this.k(t);await s.updateTimestamp(e.url),await s.expireEntries()}async deleteCacheAndMetadata(){for(const[t,e]of this.g)await caches.delete(t),await e.delete();this.g=new Map}},t}({},workbox.core._private,workbox.core._private,workbox.core._private,workbox.core._private,workbox.core);
//# sourceMappingURL=workbox-expiration.prod.js.map

View File

@@ -0,0 +1,2 @@
this.workbox=this.workbox||{},this.workbox.navigationPreload=function(t){"use strict";try{self["workbox:navigation-preload:4.3.1"]&&_()}catch(t){}function e(){return Boolean(self.registration&&self.registration.navigationPreload)}return t.disable=function(){e()&&self.addEventListener("activate",t=>{t.waitUntil(self.registration.navigationPreload.disable().then(()=>{}))})},t.enable=function(t){e()&&self.addEventListener("activate",e=>{e.waitUntil(self.registration.navigationPreload.enable().then(()=>{t&&self.registration.navigationPreload.setHeaderValue(t)}))})},t.isSupported=e,t}({});
//# sourceMappingURL=workbox-navigation-preload.prod.js.map

View File

@@ -0,0 +1,2 @@
this.workbox=this.workbox||{},this.workbox.googleAnalytics=function(e,t,o,n,a,c,w){"use strict";try{self["workbox:google-analytics:4.3.1"]&&_()}catch(e){}const r=/^\/(\w+\/)?collect/,s=e=>async({queue:t})=>{let o;for(;o=await t.shiftRequest();){const{request:n,timestamp:a}=o,c=new URL(n.url);try{const w="POST"===n.method?new URLSearchParams(await n.clone().text()):c.searchParams,r=a-(Number(w.get("qt"))||0),s=Date.now()-r;if(w.set("qt",s),e.parameterOverrides)for(const t of Object.keys(e.parameterOverrides)){const o=e.parameterOverrides[t];w.set(t,o)}"function"==typeof e.hitFilter&&e.hitFilter.call(null,w),await fetch(new Request(c.origin+c.pathname,{body:w.toString(),method:"POST",mode:"cors",credentials:"omit",headers:{"Content-Type":"text/plain"}}))}catch(e){throw await t.unshiftRequest(o),e}}},i=e=>{const t=({url:e})=>"www.google-analytics.com"===e.hostname&&r.test(e.pathname),o=new w.NetworkOnly({plugins:[e]});return[new n.Route(t,o,"GET"),new n.Route(t,o,"POST")]},l=e=>{const t=new c.NetworkFirst({cacheName:e});return new n.Route(({url:e})=>"www.google-analytics.com"===e.hostname&&"/analytics.js"===e.pathname,t,"GET")},m=e=>{const t=new c.NetworkFirst({cacheName:e});return new n.Route(({url:e})=>"www.googletagmanager.com"===e.hostname&&"/gtag/js"===e.pathname,t,"GET")},u=e=>{const t=new c.NetworkFirst({cacheName:e});return new n.Route(({url:e})=>"www.googletagmanager.com"===e.hostname&&"/gtm.js"===e.pathname,t,"GET")};return e.initialize=((e={})=>{const n=o.cacheNames.getGoogleAnalyticsName(e.cacheName),c=new t.Plugin("workbox-google-analytics",{maxRetentionTime:2880,onSync:s(e)}),w=[u(n),l(n),m(n),...i(c)],r=new a.Router;for(const e of w)r.registerRoute(e);r.addFetchListener()}),e}({},workbox.backgroundSync,workbox.core._private,workbox.routing,workbox.routing,workbox.strategies,workbox.strategies);
//# sourceMappingURL=workbox-offline-ga.prod.js.map

View File

@@ -0,0 +1,2 @@
this.workbox=this.workbox||{},this.workbox.precaching=function(t,e,n,s,c){"use strict";try{self["workbox:precaching:4.3.1"]&&_()}catch(t){}const o=[],i={get:()=>o,add(t){o.push(...t)}};const a="__WB_REVISION__";function r(t){if(!t)throw new c.WorkboxError("add-to-cache-list-unexpected-type",{entry:t});if("string"==typeof t){const e=new URL(t,location);return{cacheKey:e.href,url:e.href}}const{revision:e,url:n}=t;if(!n)throw new c.WorkboxError("add-to-cache-list-unexpected-type",{entry:t});if(!e){const t=new URL(n,location);return{cacheKey:t.href,url:t.href}}const s=new URL(n,location),o=new URL(n,location);return o.searchParams.set(a,e),{cacheKey:o.href,url:s.href}}class l{constructor(t){this.t=e.cacheNames.getPrecacheName(t),this.s=new Map}addToCacheList(t){for(const e of t){const{cacheKey:t,url:n}=r(e);if(this.s.has(n)&&this.s.get(n)!==t)throw new c.WorkboxError("add-to-cache-list-conflicting-entries",{firstEntry:this.s.get(n),secondEntry:t});this.s.set(n,t)}}async install({event:t,plugins:e}={}){const n=[],s=[],c=await caches.open(this.t),o=await c.keys(),i=new Set(o.map(t=>t.url));for(const t of this.s.values())i.has(t)?s.push(t):n.push(t);const a=n.map(n=>this.o({event:t,plugins:e,url:n}));return await Promise.all(a),{updatedURLs:n,notUpdatedURLs:s}}async activate(){const t=await caches.open(this.t),e=await t.keys(),n=new Set(this.s.values()),s=[];for(const c of e)n.has(c.url)||(await t.delete(c),s.push(c.url));return{deletedURLs:s}}async o({url:t,event:e,plugins:o}){const i=new Request(t,{credentials:"same-origin"});let a,r=await s.fetchWrapper.fetch({event:e,plugins:o,request:i});for(const t of o||[])"cacheWillUpdate"in t&&(a=t.cacheWillUpdate.bind(t));if(!(a?a({event:e,request:i,response:r}):r.status<400))throw new c.WorkboxError("bad-precaching-response",{url:t,status:r.status});r.redirected&&(r=await async function(t){const e=t.clone(),n="body"in e?Promise.resolve(e.body):e.blob(),s=await n;return new Response(s,{headers:e.headers,status:e.status,statusText:e.statusText})}(r)),await n.cacheWrapper.put({event:e,plugins:o,request:i,response:r,cacheName:this.t,matchOptions:{ignoreSearch:!0}})}getURLsToCacheKeys(){return this.s}getCachedURLs(){return[...this.s.keys()]}getCacheKeyForURL(t){const e=new URL(t,location);return this.s.get(e.href)}}let u;const h=()=>(u||(u=new l),u);const d=(t,e)=>{const n=h().getURLsToCacheKeys();for(const s of function*(t,{ignoreURLParametersMatching:e,directoryIndex:n,cleanURLs:s,urlManipulation:c}={}){const o=new URL(t,location);o.hash="",yield o.href;const i=function(t,e){for(const n of[...t.searchParams.keys()])e.some(t=>t.test(n))&&t.searchParams.delete(n);return t}(o,e);if(yield i.href,n&&i.pathname.endsWith("/")){const t=new URL(i);t.pathname+=n,yield t.href}if(s){const t=new URL(i);t.pathname+=".html",yield t.href}if(c){const t=c({url:o});for(const e of t)yield e.href}}(t,e)){const t=n.get(s);if(t)return t}};let w=!1;const f=t=>{w||((({ignoreURLParametersMatching:t=[/^utm_/],directoryIndex:n="index.html",cleanURLs:s=!0,urlManipulation:c=null}={})=>{const o=e.cacheNames.getPrecacheName();addEventListener("fetch",e=>{const i=d(e.request.url,{cleanURLs:s,directoryIndex:n,ignoreURLParametersMatching:t,urlManipulation:c});if(!i)return;let a=caches.open(o).then(t=>t.match(i)).then(t=>t||fetch(i));e.respondWith(a)})})(t),w=!0)},y=t=>{const e=h(),n=i.get();t.waitUntil(e.install({event:t,plugins:n}).catch(t=>{throw t}))},p=t=>{const e=h(),n=i.get();t.waitUntil(e.activate({event:t,plugins:n}))},L=t=>{h().addToCacheList(t),t.length>0&&(addEventListener("install",y),addEventListener("activate",p))};return t.addPlugins=(t=>{i.add(t)}),t.addRoute=f,t.cleanupOutdatedCaches=(()=>{addEventListener("activate",t=>{const n=e.cacheNames.getPrecacheName();t.waitUntil((async(t,e="-precache-")=>{const n=(await caches.keys()).filter(n=>n.includes(e)&&n.includes(self.registration.scope)&&n!==t);return await Promise.all(n.map(t=>caches.delete(t))),n})(n).then(t=>{}))})}),t.getCacheKeyForURL=(t=>{return h().getCacheKeyForURL(t)}),t.precache=L,t.precacheAndRoute=((t,e)=>{L(t),f(e)}),t.PrecacheController=l,t}({},workbox.core._private,workbox.core._private,workbox.core._private,workbox.core._private);
//# sourceMappingURL=workbox-precaching.prod.js.map

View File

@@ -0,0 +1,2 @@
this.workbox=this.workbox||{},this.workbox.rangeRequests=function(e,n){"use strict";try{self["workbox:range-requests:4.3.1"]&&_()}catch(e){}async function t(e,t){try{if(206===t.status)return t;const s=e.headers.get("range");if(!s)throw new n.WorkboxError("no-range-header");const a=function(e){const t=e.trim().toLowerCase();if(!t.startsWith("bytes="))throw new n.WorkboxError("unit-must-be-bytes",{normalizedRangeHeader:t});if(t.includes(","))throw new n.WorkboxError("single-range-only",{normalizedRangeHeader:t});const s=/(\d*)-(\d*)/.exec(t);if(null===s||!s[1]&&!s[2])throw new n.WorkboxError("invalid-range-values",{normalizedRangeHeader:t});return{start:""===s[1]?null:Number(s[1]),end:""===s[2]?null:Number(s[2])}}(s),r=await t.blob(),i=function(e,t,s){const a=e.size;if(s>a||t<0)throw new n.WorkboxError("range-not-satisfiable",{size:a,end:s,start:t});let r,i;return null===t?(r=a-s,i=a):null===s?(r=t,i=a):(r=t,i=s+1),{start:r,end:i}}(r,a.start,a.end),o=r.slice(i.start,i.end),u=o.size,l=new Response(o,{status:206,statusText:"Partial Content",headers:t.headers});return l.headers.set("Content-Length",u),l.headers.set("Content-Range",`bytes ${i.start}-${i.end-1}/`+r.size),l}catch(e){return new Response("",{status:416,statusText:"Range Not Satisfiable"})}}return e.createPartialResponse=t,e.Plugin=class{async cachedResponseWillBeUsed({request:e,cachedResponse:n}){return n&&e.headers.has("range")?await t(e,n):n}},e}({},workbox.core._private);
//# sourceMappingURL=workbox-range-requests.prod.js.map

View File

@@ -0,0 +1,2 @@
this.workbox=this.workbox||{},this.workbox.routing=function(t,e,r){"use strict";try{self["workbox:routing:4.3.1"]&&_()}catch(t){}const s="GET",n=t=>t&&"object"==typeof t?t:{handle:t};class o{constructor(t,e,r){this.handler=n(e),this.match=t,this.method=r||s}}class i extends o{constructor(t,{whitelist:e=[/./],blacklist:r=[]}={}){super(t=>this.t(t),t),this.s=e,this.o=r}t({url:t,request:e}){if("navigate"!==e.mode)return!1;const r=t.pathname+t.search;for(const t of this.o)if(t.test(r))return!1;return!!this.s.some(t=>t.test(r))}}class u extends o{constructor(t,e,r){super(({url:e})=>{const r=t.exec(e.href);return r?e.origin!==location.origin&&0!==r.index?null:r.slice(1):null},e,r)}}class c{constructor(){this.i=new Map}get routes(){return this.i}addFetchListener(){self.addEventListener("fetch",t=>{const{request:e}=t,r=this.handleRequest({request:e,event:t});r&&t.respondWith(r)})}addCacheListener(){self.addEventListener("message",async t=>{if(t.data&&"CACHE_URLS"===t.data.type){const{payload:e}=t.data,r=Promise.all(e.urlsToCache.map(t=>{"string"==typeof t&&(t=[t]);const e=new Request(...t);return this.handleRequest({request:e})}));t.waitUntil(r),t.ports&&t.ports[0]&&(await r,t.ports[0].postMessage(!0))}})}handleRequest({request:t,event:e}){const r=new URL(t.url,location);if(!r.protocol.startsWith("http"))return;let s,{params:n,route:o}=this.findMatchingRoute({url:r,request:t,event:e}),i=o&&o.handler;if(!i&&this.u&&(i=this.u),i){try{s=i.handle({url:r,request:t,event:e,params:n})}catch(t){s=Promise.reject(t)}return s&&this.h&&(s=s.catch(t=>this.h.handle({url:r,event:e,err:t}))),s}}findMatchingRoute({url:t,request:e,event:r}){const s=this.i.get(e.method)||[];for(const n of s){let s,o=n.match({url:t,request:e,event:r});if(o)return Array.isArray(o)&&o.length>0?s=o:o.constructor===Object&&Object.keys(o).length>0&&(s=o),{route:n,params:s}}return{}}setDefaultHandler(t){this.u=n(t)}setCatchHandler(t){this.h=n(t)}registerRoute(t){this.i.has(t.method)||this.i.set(t.method,[]),this.i.get(t.method).push(t)}unregisterRoute(t){if(!this.i.has(t.method))throw new r.WorkboxError("unregister-route-but-not-found-with-method",{method:t.method});const e=this.i.get(t.method).indexOf(t);if(!(e>-1))throw new r.WorkboxError("unregister-route-route-not-registered");this.i.get(t.method).splice(e,1)}}let a;const h=()=>(a||((a=new c).addFetchListener(),a.addCacheListener()),a);return t.NavigationRoute=i,t.RegExpRoute=u,t.registerNavigationRoute=((t,r={})=>{const s=e.cacheNames.getPrecacheName(r.cacheName),n=new i(async()=>{try{const e=await caches.match(t,{cacheName:s});if(e)return e;throw new Error(`The cache ${s} did not have an entry for `+`${t}.`)}catch(e){return fetch(t)}},{whitelist:r.whitelist,blacklist:r.blacklist});return h().registerRoute(n),n}),t.registerRoute=((t,e,s="GET")=>{let n;if("string"==typeof t){const r=new URL(t,location);n=new o(({url:t})=>t.href===r.href,e,s)}else if(t instanceof RegExp)n=new u(t,e,s);else if("function"==typeof t)n=new o(t,e,s);else{if(!(t instanceof o))throw new r.WorkboxError("unsupported-route-type",{moduleName:"workbox-routing",funcName:"registerRoute",paramName:"capture"});n=t}return h().registerRoute(n),n}),t.Route=o,t.Router=c,t.setCatchHandler=(t=>{h().setCatchHandler(t)}),t.setDefaultHandler=(t=>{h().setDefaultHandler(t)}),t}({},workbox.core._private,workbox.core._private);
//# sourceMappingURL=workbox-routing.prod.js.map

View File

@@ -0,0 +1,2 @@
this.workbox=this.workbox||{},this.workbox.strategies=function(e,t,s,n,r){"use strict";try{self["workbox:strategies:4.3.1"]&&_()}catch(e){}class i{constructor(e={}){this.t=t.cacheNames.getRuntimeName(e.cacheName),this.s=e.plugins||[],this.i=e.fetchOptions||null,this.h=e.matchOptions||null}async handle({event:e,request:t}){return this.makeRequest({event:e,request:t||e.request})}async makeRequest({event:e,request:t}){"string"==typeof t&&(t=new Request(t));let n,i=await s.cacheWrapper.match({cacheName:this.t,request:t,event:e,matchOptions:this.h,plugins:this.s});if(!i)try{i=await this.u(t,e)}catch(e){n=e}if(!i)throw new r.WorkboxError("no-response",{url:t.url,error:n});return i}async u(e,t){const r=await n.fetchWrapper.fetch({request:e,event:t,fetchOptions:this.i,plugins:this.s}),i=r.clone(),h=s.cacheWrapper.put({cacheName:this.t,request:e,response:i,event:t,plugins:this.s});if(t)try{t.waitUntil(h)}catch(e){}return r}}class h{constructor(e={}){this.t=t.cacheNames.getRuntimeName(e.cacheName),this.s=e.plugins||[],this.h=e.matchOptions||null}async handle({event:e,request:t}){return this.makeRequest({event:e,request:t||e.request})}async makeRequest({event:e,request:t}){"string"==typeof t&&(t=new Request(t));const n=await s.cacheWrapper.match({cacheName:this.t,request:t,event:e,matchOptions:this.h,plugins:this.s});if(!n)throw new r.WorkboxError("no-response",{url:t.url});return n}}const u={cacheWillUpdate:({response:e})=>200===e.status||0===e.status?e:null};class a{constructor(e={}){if(this.t=t.cacheNames.getRuntimeName(e.cacheName),e.plugins){let t=e.plugins.some(e=>!!e.cacheWillUpdate);this.s=t?e.plugins:[u,...e.plugins]}else this.s=[u];this.o=e.networkTimeoutSeconds,this.i=e.fetchOptions||null,this.h=e.matchOptions||null}async handle({event:e,request:t}){return this.makeRequest({event:e,request:t||e.request})}async makeRequest({event:e,request:t}){const s=[];"string"==typeof t&&(t=new Request(t));const n=[];let i;if(this.o){const{id:r,promise:h}=this.l({request:t,event:e,logs:s});i=r,n.push(h)}const h=this.q({timeoutId:i,request:t,event:e,logs:s});n.push(h);let u=await Promise.race(n);if(u||(u=await h),!u)throw new r.WorkboxError("no-response",{url:t.url});return u}l({request:e,logs:t,event:s}){let n;return{promise:new Promise(t=>{n=setTimeout(async()=>{t(await this.p({request:e,event:s}))},1e3*this.o)}),id:n}}async q({timeoutId:e,request:t,logs:r,event:i}){let h,u;try{u=await n.fetchWrapper.fetch({request:t,event:i,fetchOptions:this.i,plugins:this.s})}catch(e){h=e}if(e&&clearTimeout(e),h||!u)u=await this.p({request:t,event:i});else{const e=u.clone(),n=s.cacheWrapper.put({cacheName:this.t,request:t,response:e,event:i,plugins:this.s});if(i)try{i.waitUntil(n)}catch(e){}}return u}p({event:e,request:t}){return s.cacheWrapper.match({cacheName:this.t,request:t,event:e,matchOptions:this.h,plugins:this.s})}}class c{constructor(e={}){this.t=t.cacheNames.getRuntimeName(e.cacheName),this.s=e.plugins||[],this.i=e.fetchOptions||null}async handle({event:e,request:t}){return this.makeRequest({event:e,request:t||e.request})}async makeRequest({event:e,request:t}){let s,i;"string"==typeof t&&(t=new Request(t));try{i=await n.fetchWrapper.fetch({request:t,event:e,fetchOptions:this.i,plugins:this.s})}catch(e){s=e}if(!i)throw new r.WorkboxError("no-response",{url:t.url,error:s});return i}}class o{constructor(e={}){if(this.t=t.cacheNames.getRuntimeName(e.cacheName),this.s=e.plugins||[],e.plugins){let t=e.plugins.some(e=>!!e.cacheWillUpdate);this.s=t?e.plugins:[u,...e.plugins]}else this.s=[u];this.i=e.fetchOptions||null,this.h=e.matchOptions||null}async handle({event:e,request:t}){return this.makeRequest({event:e,request:t||e.request})}async makeRequest({event:e,request:t}){"string"==typeof t&&(t=new Request(t));const n=this.u({request:t,event:e});let i,h=await s.cacheWrapper.match({cacheName:this.t,request:t,event:e,matchOptions:this.h,plugins:this.s});if(h){if(e)try{e.waitUntil(n)}catch(i){}}else try{h=await n}catch(e){i=e}if(!h)throw new r.WorkboxError("no-response",{url:t.url,error:i});return h}async u({request:e,event:t}){const r=await n.fetchWrapper.fetch({request:e,event:t,fetchOptions:this.i,plugins:this.s}),i=s.cacheWrapper.put({cacheName:this.t,request:e,response:r.clone(),event:t,plugins:this.s});if(t)try{t.waitUntil(i)}catch(e){}return r}}const l={cacheFirst:i,cacheOnly:h,networkFirst:a,networkOnly:c,staleWhileRevalidate:o},q=e=>{const t=l[e];return e=>new t(e)},w=q("cacheFirst"),p=q("cacheOnly"),v=q("networkFirst"),y=q("networkOnly"),m=q("staleWhileRevalidate");return e.CacheFirst=i,e.CacheOnly=h,e.NetworkFirst=a,e.NetworkOnly=c,e.StaleWhileRevalidate=o,e.cacheFirst=w,e.cacheOnly=p,e.networkFirst=v,e.networkOnly=y,e.staleWhileRevalidate=m,e}({},workbox.core._private,workbox.core._private,workbox.core._private,workbox.core._private);
//# sourceMappingURL=workbox-strategies.prod.js.map

View File

@@ -0,0 +1,2 @@
this.workbox=this.workbox||{},this.workbox.streams=function(e){"use strict";try{self["workbox:streams:4.3.1"]&&_()}catch(e){}function n(e){const n=e.map(e=>Promise.resolve(e).then(e=>(function(e){return e.body&&e.body.getReader?e.body.getReader():e.getReader?e.getReader():new Response(e).body.getReader()})(e)));let t,r;const s=new Promise((e,n)=>{t=e,r=n});let o=0;return{done:s,stream:new ReadableStream({pull(e){return n[o].then(e=>e.read()).then(r=>{if(r.done)return++o>=n.length?(e.close(),void t()):this.pull(e);e.enqueue(r.value)}).catch(e=>{throw r(e),e})},cancel(){t()}})}}function t(e={}){const n=new Headers(e);return n.has("content-type")||n.set("content-type","text/html"),n}function r(e,r){const{done:s,stream:o}=n(e),a=t(r);return{done:s,response:new Response(o,{headers:a})}}let s=void 0;function o(){if(void 0===s)try{new ReadableStream({start(){}}),s=!0}catch(e){s=!1}return s}return e.concatenate=n,e.concatenateToResponse=r,e.isSupported=o,e.strategy=function(e,n){return async({event:s,url:a,params:c})=>{if(o()){const{done:t,response:o}=r(e.map(e=>e({event:s,url:a,params:c})),n);return s.waitUntil(t),o}const i=await Promise.all(e.map(e=>e({event:s,url:a,params:c})).map(async e=>{const n=await e;return n instanceof Response?n.blob():n})),u=t(n);return new Response(new Blob(i),{headers:u})}},e}({});
//# sourceMappingURL=workbox-streams.prod.js.map

View File

@@ -0,0 +1,2 @@
!function(){"use strict";try{self["workbox:sw:4.3.1"]&&_()}catch(t){}const t="https://storage.googleapis.com/workbox-cdn/releases/4.3.1",e={backgroundSync:"background-sync",broadcastUpdate:"broadcast-update",cacheableResponse:"cacheable-response",core:"core",expiration:"expiration",googleAnalytics:"offline-ga",navigationPreload:"navigation-preload",precaching:"precaching",rangeRequests:"range-requests",routing:"routing",strategies:"strategies",streams:"streams"};self.workbox=new class{constructor(){return this.v={},this.t={debug:"localhost"===self.location.hostname,modulePathPrefix:null,modulePathCb:null},this.s=this.t.debug?"dev":"prod",this.o=!1,new Proxy(this,{get(t,s){if(t[s])return t[s];const o=e[s];return o&&t.loadModule(`workbox-${o}`),t[s]}})}setConfig(t={}){if(this.o)throw new Error("Config must be set before accessing workbox.* modules");Object.assign(this.t,t),this.s=this.t.debug?"dev":"prod"}loadModule(t){const e=this.i(t);try{importScripts(e),this.o=!0}catch(s){throw console.error(`Unable to import module '${t}' from '${e}'.`),s}}i(e){if(this.t.modulePathCb)return this.t.modulePathCb(e,this.t.debug);let s=[t];const o=`${e}.${this.s}.js`,r=this.t.modulePathPrefix;return r&&""===(s=r.split("/"))[s.length-1]&&s.splice(s.length-1,1),s.push(o),s.join("/")}}}();
//# sourceMappingURL=workbox-sw.js.map

View File

@@ -0,0 +1,2 @@
try{self["workbox:window:4.3.1"]&&_()}catch(n){}var n=function(n,t){return new Promise(function(i){var e=new MessageChannel;e.port1.onmessage=function(n){return i(n.data)},n.postMessage(t,[e.port2])})};function t(n,t){for(var i=0;i<t.length;i++){var e=t[i];e.enumerable=e.enumerable||!1,e.configurable=!0,"value"in e&&(e.writable=!0),Object.defineProperty(n,e.key,e)}}function i(n){if(void 0===n)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return n}try{self["workbox:core:4.3.1"]&&_()}catch(n){}var e=function(){var n=this;this.promise=new Promise(function(t,i){n.resolve=t,n.reject=i})},r=function(n,t){return new URL(n,location).href===new URL(t,location).href},o=function(n,t){Object.assign(this,t,{type:n})};function u(n){return function(){for(var t=[],i=0;i<arguments.length;i++)t[i]=arguments[i];try{return Promise.resolve(n.apply(this,t))}catch(n){return Promise.reject(n)}}}function a(n,t,i){return i?t?t(n):n:(n&&n.then||(n=Promise.resolve(n)),t?n.then(t):n)}function s(){}var c=function(c){var f,h;function v(n,t){var r;return void 0===t&&(t={}),(r=c.call(this)||this).t=n,r.i=t,r.o=0,r.u=new e,r.s=new e,r.h=new e,r.v=r.v.bind(i(i(r))),r.l=r.l.bind(i(i(r))),r.g=r.g.bind(i(i(r))),r.m=r.m.bind(i(i(r))),r}h=c,(f=v).prototype=Object.create(h.prototype),f.prototype.constructor=f,f.__proto__=h;var l,w,g,d=v.prototype;return d.register=u(function(n){var t,i,e=this,u=(void 0===n?{}:n).immediate,c=void 0!==u&&u;return t=function(){return e.p=Boolean(navigator.serviceWorker.controller),e.P=e.R(),a(e.k(),function(n){e.B=n,e.P&&(e.O=e.P,e.s.resolve(e.P),e.h.resolve(e.P),e.j(e.P),e.P.addEventListener("statechange",e.l,{once:!0}));var t=e.B.waiting;return t&&r(t.scriptURL,e.t)&&(e.O=t,Promise.resolve().then(function(){e.dispatchEvent(new o("waiting",{sw:t,wasWaitingBeforeRegister:!0}))})),e.O&&e.u.resolve(e.O),e.B.addEventListener("updatefound",e.g),navigator.serviceWorker.addEventListener("controllerchange",e.m,{once:!0}),"BroadcastChannel"in self&&(e.C=new BroadcastChannel("workbox"),e.C.addEventListener("message",e.v)),navigator.serviceWorker.addEventListener("message",e.v),e.B})},(i=function(){if(!c&&"complete"!==document.readyState)return function(n,t){if(!t)return n&&n.then?n.then(s):Promise.resolve()}(new Promise(function(n){return addEventListener("load",n)}))}())&&i.then?i.then(t):t(i)}),d.getSW=u(function(){return this.O||this.u.promise}),d.messageSW=u(function(t){return a(this.getSW(),function(i){return n(i,t)})}),d.R=function(){var n=navigator.serviceWorker.controller;if(n&&r(n.scriptURL,this.t))return n},d.k=u(function(){var n=this;return function(n,t){try{var i=n()}catch(n){return t(n)}return i&&i.then?i.then(void 0,t):i}(function(){return a(navigator.serviceWorker.register(n.t,n.i),function(t){return n.L=performance.now(),t})},function(n){throw n})}),d.j=function(t){n(t,{type:"WINDOW_READY",meta:"workbox-window"})},d.g=function(){var n=this.B.installing;this.o>0||!r(n.scriptURL,this.t)||performance.now()>this.L+6e4?(this.W=n,this.B.removeEventListener("updatefound",this.g)):(this.O=n,this.u.resolve(n)),++this.o,n.addEventListener("statechange",this.l)},d.l=function(n){var t=this,i=n.target,e=i.state,r=i===this.W,u=r?"external":"",a={sw:i,originalEvent:n};!r&&this.p&&(a.isUpdate=!0),this.dispatchEvent(new o(u+e,a)),"installed"===e?this._=setTimeout(function(){"installed"===e&&t.B.waiting===i&&t.dispatchEvent(new o(u+"waiting",a))},200):"activating"===e&&(clearTimeout(this._),r||this.s.resolve(i))},d.m=function(n){var t=this.O;t===navigator.serviceWorker.controller&&(this.dispatchEvent(new o("controlling",{sw:t,originalEvent:n})),this.h.resolve(t))},d.v=function(n){var t=n.data;this.dispatchEvent(new o("message",{data:t,originalEvent:n}))},l=v,(w=[{key:"active",get:function(){return this.s.promise}},{key:"controlling",get:function(){return this.h.promise}}])&&t(l.prototype,w),g&&t(l,g),v}(function(){function n(){this.D={}}var t=n.prototype;return t.addEventListener=function(n,t){this.T(n).add(t)},t.removeEventListener=function(n,t){this.T(n).delete(t)},t.dispatchEvent=function(n){n.target=this,this.T(n.type).forEach(function(t){return t(n)})},t.T=function(n){return this.D[n]=this.D[n]||new Set},n}());export{c as Workbox,n as messageSW};
//# sourceMappingURL=workbox-window.prod.es5.mjs.map

View File

@@ -0,0 +1,2 @@
try{self["workbox:window:4.3.1"]&&_()}catch(t){}const t=(t,s)=>new Promise(i=>{let e=new MessageChannel;e.port1.onmessage=(t=>i(t.data)),t.postMessage(s,[e.port2])});try{self["workbox:core:4.3.1"]&&_()}catch(t){}class s{constructor(){this.promise=new Promise((t,s)=>{this.resolve=t,this.reject=s})}}class i{constructor(){this.t={}}addEventListener(t,s){this.s(t).add(s)}removeEventListener(t,s){this.s(t).delete(s)}dispatchEvent(t){t.target=this,this.s(t.type).forEach(s=>s(t))}s(t){return this.t[t]=this.t[t]||new Set}}const e=(t,s)=>new URL(t,location).href===new URL(s,location).href;class n{constructor(t,s){Object.assign(this,s,{type:t})}}const h=200,a=6e4;class o extends i{constructor(t,i={}){super(),this.i=t,this.h=i,this.o=0,this.l=new s,this.g=new s,this.u=new s,this.m=this.m.bind(this),this.v=this.v.bind(this),this.p=this.p.bind(this),this._=this._.bind(this)}async register({immediate:t=!1}={}){t||"complete"===document.readyState||await new Promise(t=>addEventListener("load",t)),this.C=Boolean(navigator.serviceWorker.controller),this.W=this.L(),this.S=await this.B(),this.W&&(this.R=this.W,this.g.resolve(this.W),this.u.resolve(this.W),this.P(this.W),this.W.addEventListener("statechange",this.v,{once:!0}));const s=this.S.waiting;return s&&e(s.scriptURL,this.i)&&(this.R=s,Promise.resolve().then(()=>{this.dispatchEvent(new n("waiting",{sw:s,wasWaitingBeforeRegister:!0}))})),this.R&&this.l.resolve(this.R),this.S.addEventListener("updatefound",this.p),navigator.serviceWorker.addEventListener("controllerchange",this._,{once:!0}),"BroadcastChannel"in self&&(this.T=new BroadcastChannel("workbox"),this.T.addEventListener("message",this.m)),navigator.serviceWorker.addEventListener("message",this.m),this.S}get active(){return this.g.promise}get controlling(){return this.u.promise}async getSW(){return this.R||this.l.promise}async messageSW(s){const i=await this.getSW();return t(i,s)}L(){const t=navigator.serviceWorker.controller;if(t&&e(t.scriptURL,this.i))return t}async B(){try{const t=await navigator.serviceWorker.register(this.i,this.h);return this.U=performance.now(),t}catch(t){throw t}}P(s){t(s,{type:"WINDOW_READY",meta:"workbox-window"})}p(){const t=this.S.installing;this.o>0||!e(t.scriptURL,this.i)||performance.now()>this.U+a?(this.k=t,this.S.removeEventListener("updatefound",this.p)):(this.R=t,this.l.resolve(t)),++this.o,t.addEventListener("statechange",this.v)}v(t){const s=t.target,{state:i}=s,e=s===this.k,a=e?"external":"",o={sw:s,originalEvent:t};!e&&this.C&&(o.isUpdate=!0),this.dispatchEvent(new n(a+i,o)),"installed"===i?this.D=setTimeout(()=>{"installed"===i&&this.S.waiting===s&&this.dispatchEvent(new n(a+"waiting",o))},h):"activating"===i&&(clearTimeout(this.D),e||this.g.resolve(s))}_(t){const s=this.R;s===navigator.serviceWorker.controller&&(this.dispatchEvent(new n("controlling",{sw:s,originalEvent:t})),this.u.resolve(s))}m(t){const{data:s}=t;this.dispatchEvent(new n("message",{data:s,originalEvent:t}))}}export{o as Workbox,t as messageSW};
//# sourceMappingURL=workbox-window.prod.mjs.map

View File

@@ -0,0 +1,2 @@
!function(n,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((n=n||self).workbox={})}(this,function(n){"use strict";try{self["workbox:window:4.3.1"]&&_()}catch(n){}var t=function(n,t){return new Promise(function(i){var e=new MessageChannel;e.port1.onmessage=function(n){return i(n.data)},n.postMessage(t,[e.port2])})};function i(n,t){for(var i=0;i<t.length;i++){var e=t[i];e.enumerable=e.enumerable||!1,e.configurable=!0,"value"in e&&(e.writable=!0),Object.defineProperty(n,e.key,e)}}function e(n){if(void 0===n)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return n}try{self["workbox:core:4.3.1"]&&_()}catch(n){}var r=function(){var n=this;this.promise=new Promise(function(t,i){n.resolve=t,n.reject=i})},o=function(n,t){return new URL(n,location).href===new URL(t,location).href},u=function(n,t){Object.assign(this,t,{type:n})};function s(n){return function(){for(var t=[],i=0;i<arguments.length;i++)t[i]=arguments[i];try{return Promise.resolve(n.apply(this,t))}catch(n){return Promise.reject(n)}}}function a(n,t,i){return i?t?t(n):n:(n&&n.then||(n=Promise.resolve(n)),t?n.then(t):n)}function c(){}var f=function(n){var f,h;function v(t,i){var o;return void 0===i&&(i={}),(o=n.call(this)||this).t=t,o.i=i,o.o=0,o.u=new r,o.s=new r,o.h=new r,o.v=o.v.bind(e(e(o))),o.l=o.l.bind(e(e(o))),o.g=o.g.bind(e(e(o))),o.m=o.m.bind(e(e(o))),o}h=n,(f=v).prototype=Object.create(h.prototype),f.prototype.constructor=f,f.__proto__=h;var l,w,d,g=v.prototype;return g.register=s(function(n){var t,i,e=this,r=(void 0===n?{}:n).immediate,s=void 0!==r&&r;return t=function(){return e.p=Boolean(navigator.serviceWorker.controller),e.P=e.j(),a(e.O(),function(n){e.R=n,e.P&&(e._=e.P,e.s.resolve(e.P),e.h.resolve(e.P),e.k(e.P),e.P.addEventListener("statechange",e.l,{once:!0}));var t=e.R.waiting;return t&&o(t.scriptURL,e.t)&&(e._=t,Promise.resolve().then(function(){e.dispatchEvent(new u("waiting",{sw:t,wasWaitingBeforeRegister:!0}))})),e._&&e.u.resolve(e._),e.R.addEventListener("updatefound",e.g),navigator.serviceWorker.addEventListener("controllerchange",e.m,{once:!0}),"BroadcastChannel"in self&&(e.B=new BroadcastChannel("workbox"),e.B.addEventListener("message",e.v)),navigator.serviceWorker.addEventListener("message",e.v),e.R})},(i=function(){if(!s&&"complete"!==document.readyState)return function(n,t){if(!t)return n&&n.then?n.then(c):Promise.resolve()}(new Promise(function(n){return addEventListener("load",n)}))}())&&i.then?i.then(t):t(i)}),g.getSW=s(function(){return this._||this.u.promise}),g.messageSW=s(function(n){return a(this.getSW(),function(i){return t(i,n)})}),g.j=function(){var n=navigator.serviceWorker.controller;if(n&&o(n.scriptURL,this.t))return n},g.O=s(function(){var n=this;return function(n,t){try{var i=n()}catch(n){return t(n)}return i&&i.then?i.then(void 0,t):i}(function(){return a(navigator.serviceWorker.register(n.t,n.i),function(t){return n.C=performance.now(),t})},function(n){throw n})}),g.k=function(n){t(n,{type:"WINDOW_READY",meta:"workbox-window"})},g.g=function(){var n=this.R.installing;this.o>0||!o(n.scriptURL,this.t)||performance.now()>this.C+6e4?(this.L=n,this.R.removeEventListener("updatefound",this.g)):(this._=n,this.u.resolve(n)),++this.o,n.addEventListener("statechange",this.l)},g.l=function(n){var t=this,i=n.target,e=i.state,r=i===this.L,o=r?"external":"",s={sw:i,originalEvent:n};!r&&this.p&&(s.isUpdate=!0),this.dispatchEvent(new u(o+e,s)),"installed"===e?this.W=setTimeout(function(){"installed"===e&&t.R.waiting===i&&t.dispatchEvent(new u(o+"waiting",s))},200):"activating"===e&&(clearTimeout(this.W),r||this.s.resolve(i))},g.m=function(n){var t=this._;t===navigator.serviceWorker.controller&&(this.dispatchEvent(new u("controlling",{sw:t,originalEvent:n})),this.h.resolve(t))},g.v=function(n){var t=n.data;this.dispatchEvent(new u("message",{data:t,originalEvent:n}))},l=v,(w=[{key:"active",get:function(){return this.s.promise}},{key:"controlling",get:function(){return this.h.promise}}])&&i(l.prototype,w),d&&i(l,d),v}(function(){function n(){this.D={}}var t=n.prototype;return t.addEventListener=function(n,t){this.M(n).add(t)},t.removeEventListener=function(n,t){this.M(n).delete(t)},t.dispatchEvent=function(n){n.target=this,this.M(n.type).forEach(function(t){return t(n)})},t.M=function(n){return this.D[n]=this.D[n]||new Set},n}());n.Workbox=f,n.messageSW=t,Object.defineProperty(n,"__esModule",{value:!0})});
//# sourceMappingURL=workbox-window.prod.umd.js.map

View File

@@ -1,29 +1,30 @@
import { register } from "./register";
import { getSelectedElements } from "../scene";
import { getNonDeletedElements } from "../element";
import { deepCopyElement } from "../element/newElement";
import { randomId } from "../random";
import { t } from "../i18n";
import { LIBRARY_DISABLED_TYPES } from "../constants";
export const actionAddToLibrary = register({
name: "addToLibrary",
trackEvent: { category: "element" },
perform: (elements, appState, _, app) => {
const selectedElements = app.scene.getSelectedElements({
selectedElementIds: appState.selectedElementIds,
includeBoundTextElement: true,
includeElementsInFrames: true,
});
for (const type of LIBRARY_DISABLED_TYPES) {
if (selectedElements.some((element) => element.type === type)) {
return {
commitToHistory: false,
appState: {
...appState,
errorMessage: t(`errors.libraryElementTypeError.${type}`),
},
};
}
const selectedElements = getSelectedElements(
getNonDeletedElements(elements),
appState,
{
includeBoundTextElement: true,
includeElementsInFrames: true,
},
);
if (selectedElements.some((element) => element.type === "image")) {
return {
commitToHistory: false,
appState: {
...appState,
errorMessage: "Support for adding images to the library coming soon!",
},
};
}
return app.library

View File

@@ -13,18 +13,19 @@ import { ExcalidrawElement } from "../element/types";
import { updateFrameMembershipOfSelectedElements } from "../frame";
import { t } from "../i18n";
import { KEYS } from "../keys";
import { isSomeElementSelected } from "../scene";
import { AppClassProperties, AppState } from "../types";
import { getSelectedElements, isSomeElementSelected } from "../scene";
import { AppState } from "../types";
import { arrayToMap, getShortcutKey } from "../utils";
import { register } from "./register";
const alignActionsPredicate = (
elements: readonly ExcalidrawElement[],
appState: AppState,
_: unknown,
app: AppClassProperties,
) => {
const selectedElements = app.scene.getSelectedElements(appState);
const selectedElements = getSelectedElements(
getNonDeletedElements(elements),
appState,
);
return (
selectedElements.length > 1 &&
// TODO enable aligning frames when implemented properly
@@ -35,10 +36,12 @@ const alignActionsPredicate = (
const alignSelectedElements = (
elements: readonly ExcalidrawElement[],
appState: Readonly<AppState>,
app: AppClassProperties,
alignment: Alignment,
) => {
const selectedElements = app.scene.getSelectedElements(appState);
const selectedElements = getSelectedElements(
getNonDeletedElements(elements),
appState,
);
const updatedElements = alignElements(selectedElements, alignment);
@@ -47,7 +50,6 @@ const alignSelectedElements = (
return updateFrameMembershipOfSelectedElements(
elements.map((element) => updatedElementsMap.get(element.id) || element),
appState,
app,
);
};
@@ -55,10 +57,10 @@ export const actionAlignTop = register({
name: "alignTop",
trackEvent: { category: "element" },
predicate: alignActionsPredicate,
perform: (elements, appState, _, app) => {
perform: (elements, appState) => {
return {
appState,
elements: alignSelectedElements(elements, appState, app, {
elements: alignSelectedElements(elements, appState, {
position: "start",
axis: "y",
}),
@@ -67,9 +69,9 @@ export const actionAlignTop = register({
},
keyTest: (event) =>
event[KEYS.CTRL_OR_CMD] && event.shiftKey && event.key === KEYS.ARROW_UP,
PanelComponent: ({ elements, appState, updateData, app }) => (
PanelComponent: ({ elements, appState, updateData }) => (
<ToolButton
hidden={!alignActionsPredicate(elements, appState, null, app)}
hidden={!alignActionsPredicate(elements, appState)}
type="button"
icon={AlignTopIcon}
onClick={() => updateData(null)}
@@ -86,10 +88,10 @@ export const actionAlignBottom = register({
name: "alignBottom",
trackEvent: { category: "element" },
predicate: alignActionsPredicate,
perform: (elements, appState, _, app) => {
perform: (elements, appState) => {
return {
appState,
elements: alignSelectedElements(elements, appState, app, {
elements: alignSelectedElements(elements, appState, {
position: "end",
axis: "y",
}),
@@ -98,9 +100,9 @@ export const actionAlignBottom = register({
},
keyTest: (event) =>
event[KEYS.CTRL_OR_CMD] && event.shiftKey && event.key === KEYS.ARROW_DOWN,
PanelComponent: ({ elements, appState, updateData, app }) => (
PanelComponent: ({ elements, appState, updateData }) => (
<ToolButton
hidden={!alignActionsPredicate(elements, appState, null, app)}
hidden={!alignActionsPredicate(elements, appState)}
type="button"
icon={AlignBottomIcon}
onClick={() => updateData(null)}
@@ -117,10 +119,10 @@ export const actionAlignLeft = register({
name: "alignLeft",
trackEvent: { category: "element" },
predicate: alignActionsPredicate,
perform: (elements, appState, _, app) => {
perform: (elements, appState) => {
return {
appState,
elements: alignSelectedElements(elements, appState, app, {
elements: alignSelectedElements(elements, appState, {
position: "start",
axis: "x",
}),
@@ -129,9 +131,9 @@ export const actionAlignLeft = register({
},
keyTest: (event) =>
event[KEYS.CTRL_OR_CMD] && event.shiftKey && event.key === KEYS.ARROW_LEFT,
PanelComponent: ({ elements, appState, updateData, app }) => (
PanelComponent: ({ elements, appState, updateData }) => (
<ToolButton
hidden={!alignActionsPredicate(elements, appState, null, app)}
hidden={!alignActionsPredicate(elements, appState)}
type="button"
icon={AlignLeftIcon}
onClick={() => updateData(null)}
@@ -148,10 +150,10 @@ export const actionAlignRight = register({
name: "alignRight",
trackEvent: { category: "element" },
predicate: alignActionsPredicate,
perform: (elements, appState, _, app) => {
perform: (elements, appState) => {
return {
appState,
elements: alignSelectedElements(elements, appState, app, {
elements: alignSelectedElements(elements, appState, {
position: "end",
axis: "x",
}),
@@ -160,9 +162,9 @@ export const actionAlignRight = register({
},
keyTest: (event) =>
event[KEYS.CTRL_OR_CMD] && event.shiftKey && event.key === KEYS.ARROW_RIGHT,
PanelComponent: ({ elements, appState, updateData, app }) => (
PanelComponent: ({ elements, appState, updateData }) => (
<ToolButton
hidden={!alignActionsPredicate(elements, appState, null, app)}
hidden={!alignActionsPredicate(elements, appState)}
type="button"
icon={AlignRightIcon}
onClick={() => updateData(null)}
@@ -179,19 +181,19 @@ export const actionAlignVerticallyCentered = register({
name: "alignVerticallyCentered",
trackEvent: { category: "element" },
predicate: alignActionsPredicate,
perform: (elements, appState, _, app) => {
perform: (elements, appState) => {
return {
appState,
elements: alignSelectedElements(elements, appState, app, {
elements: alignSelectedElements(elements, appState, {
position: "center",
axis: "y",
}),
commitToHistory: true,
};
},
PanelComponent: ({ elements, appState, updateData, app }) => (
PanelComponent: ({ elements, appState, updateData }) => (
<ToolButton
hidden={!alignActionsPredicate(elements, appState, null, app)}
hidden={!alignActionsPredicate(elements, appState)}
type="button"
icon={CenterVerticallyIcon}
onClick={() => updateData(null)}
@@ -206,19 +208,19 @@ export const actionAlignHorizontallyCentered = register({
name: "alignHorizontallyCentered",
trackEvent: { category: "element" },
predicate: alignActionsPredicate,
perform: (elements, appState, _, app) => {
perform: (elements, appState) => {
return {
appState,
elements: alignSelectedElements(elements, appState, app, {
elements: alignSelectedElements(elements, appState, {
position: "center",
axis: "x",
}),
commitToHistory: true,
};
},
PanelComponent: ({ elements, appState, updateData, app }) => (
PanelComponent: ({ elements, appState, updateData }) => (
<ToolButton
hidden={!alignActionsPredicate(elements, appState, null, app)}
hidden={!alignActionsPredicate(elements, appState)}
type="button"
icon={CenterHorizontallyIcon}
onClick={() => updateData(null)}

View File

@@ -4,7 +4,7 @@ import {
VERTICAL_ALIGN,
TEXT_ALIGN,
} from "../constants";
import { isTextElement, newElement } from "../element";
import { getNonDeletedElements, isTextElement, newElement } from "../element";
import { mutateElement } from "../element/mutateElement";
import {
computeBoundTextPosition,
@@ -29,6 +29,7 @@ import {
ExcalidrawTextContainer,
ExcalidrawTextElement,
} from "../element/types";
import { getSelectedElements } from "../scene";
import { AppState } from "../types";
import { Mutable } from "../utility-types";
import { getFontString } from "../utils";
@@ -38,13 +39,16 @@ export const actionUnbindText = register({
name: "unbindText",
contextItemLabel: "labels.unbindText",
trackEvent: { category: "element" },
predicate: (elements, appState, _, app) => {
const selectedElements = app.scene.getSelectedElements(appState);
predicate: (elements, appState) => {
const selectedElements = getSelectedElements(elements, appState);
return selectedElements.some((element) => hasBoundTextElement(element));
},
perform: (elements, appState, _, app) => {
const selectedElements = app.scene.getSelectedElements(appState);
perform: (elements, appState) => {
const selectedElements = getSelectedElements(
getNonDeletedElements(elements),
appState,
);
selectedElements.forEach((element) => {
const boundTextElement = getBoundTextElement(element);
if (boundTextElement) {
@@ -89,8 +93,8 @@ export const actionBindText = register({
name: "bindText",
contextItemLabel: "labels.bindText",
trackEvent: { category: "element" },
predicate: (elements, appState, _, app) => {
const selectedElements = app.scene.getSelectedElements(appState);
predicate: (elements, appState) => {
const selectedElements = getSelectedElements(elements, appState);
if (selectedElements.length === 2) {
const textElement =
@@ -113,8 +117,11 @@ export const actionBindText = register({
}
return false;
},
perform: (elements, appState, _, app) => {
const selectedElements = app.scene.getSelectedElements(appState);
perform: (elements, appState) => {
const selectedElements = getSelectedElements(
getNonDeletedElements(elements),
appState,
);
let textElement: ExcalidrawTextElement;
let container: ExcalidrawTextContainer;
@@ -194,13 +201,16 @@ export const actionWrapTextInContainer = register({
name: "wrapTextInContainer",
contextItemLabel: "labels.createContainerFromText",
trackEvent: { category: "element" },
predicate: (elements, appState, _, app) => {
const selectedElements = app.scene.getSelectedElements(appState);
predicate: (elements, appState) => {
const selectedElements = getSelectedElements(elements, appState);
const areTextElements = selectedElements.every((el) => isTextElement(el));
return selectedElements.length > 0 && areTextElements;
},
perform: (elements, appState, _, app) => {
const selectedElements = app.scene.getSelectedElements(appState);
perform: (elements, appState) => {
const selectedElements = getSelectedElements(
getNonDeletedElements(elements),
appState,
);
let updatedElements: readonly ExcalidrawElement[] = elements.slice();
const containerIds: Mutable<AppState["selectedElementIds"]> = {};

View File

@@ -6,7 +6,7 @@ import { getCommonBounds, getNonDeletedElements } from "../element";
import { ExcalidrawElement } from "../element/types";
import { t } from "../i18n";
import { CODES, KEYS } from "../keys";
import { getNormalizedZoom } from "../scene";
import { getNormalizedZoom, getSelectedElements } from "../scene";
import { centerScrollOn } from "../scene/scroll";
import { getStateForZoom } from "../scene/zoom";
import { AppState, NormalizedZoomValue } from "../types";
@@ -302,8 +302,11 @@ export const zoomToFit = ({
export const actionZoomToFitSelectionInViewport = register({
name: "zoomToFitSelectionInViewport",
trackEvent: { category: "canvas" },
perform: (elements, appState, _, app) => {
const selectedElements = app.scene.getSelectedElements(appState);
perform: (elements, appState) => {
const selectedElements = getSelectedElements(
getNonDeletedElements(elements),
appState,
);
return zoomToFit({
targetElements: selectedElements.length ? selectedElements : elements,
appState,
@@ -322,8 +325,11 @@ export const actionZoomToFitSelectionInViewport = register({
export const actionZoomToFitSelection = register({
name: "zoomToFitSelection",
trackEvent: { category: "canvas" },
perform: (elements, appState, _, app) => {
const selectedElements = app.scene.getSelectedElements(appState);
perform: (elements, appState) => {
const selectedElements = getSelectedElements(
getNonDeletedElements(elements),
appState,
);
return zoomToFit({
targetElements: selectedElements.length ? selectedElements : elements,
appState,
@@ -396,7 +402,6 @@ export const actionToggleEraserTool = register({
...appState,
selectedElementIds: {},
selectedGroupIds: {},
activeEmbeddable: null,
activeTool,
},
commitToHistory: true,
@@ -423,7 +428,7 @@ export const actionToggleHandTool = register({
type: "hand",
lastActiveToolBeforeEraser: appState.activeTool,
});
setCursor(app.interactiveCanvas, CURSOR_TYPE.GRAB);
setCursor(app.canvas, CURSOR_TYPE.GRAB);
}
return {
@@ -431,7 +436,6 @@ export const actionToggleHandTool = register({
...appState,
selectedElementIds: {},
selectedGroupIds: {},
activeEmbeddable: null,
activeTool,
},
commitToHistory: true,

View File

@@ -7,6 +7,7 @@ import {
probablySupportsClipboardWriteText,
} from "../clipboard";
import { actionDeleteSelected } from "./actionDeleteSelected";
import { getSelectedElements } from "../scene/selection";
import { exportCanvas } from "../data/index";
import { getNonDeletedElements, isTextElement } from "../element";
import { t } from "../i18n";
@@ -15,8 +16,7 @@ export const actionCopy = register({
name: "copy",
trackEvent: { category: "element" },
perform: (elements, appState, _, app) => {
const elementsToCopy = app.scene.getSelectedElements({
selectedElementIds: appState.selectedElementIds,
const elementsToCopy = getSelectedElements(elements, appState, {
includeBoundTextElement: true,
includeElementsInFrames: true,
});
@@ -75,11 +75,14 @@ export const actionCopyAsSvg = register({
commitToHistory: false,
};
}
const selectedElements = app.scene.getSelectedElements({
selectedElementIds: appState.selectedElementIds,
includeBoundTextElement: true,
includeElementsInFrames: true,
});
const selectedElements = getSelectedElements(
getNonDeletedElements(elements),
appState,
{
includeBoundTextElement: true,
includeElementsInFrames: true,
},
);
try {
await exportCanvas(
"clipboard-svg",
@@ -119,11 +122,14 @@ export const actionCopyAsPng = register({
commitToHistory: false,
};
}
const selectedElements = app.scene.getSelectedElements({
selectedElementIds: appState.selectedElementIds,
includeBoundTextElement: true,
includeElementsInFrames: true,
});
const selectedElements = getSelectedElements(
getNonDeletedElements(elements),
appState,
{
includeBoundTextElement: true,
includeElementsInFrames: true,
},
);
try {
await exportCanvas(
"clipboard",
@@ -171,11 +177,14 @@ export const actionCopyAsPng = register({
export const copyText = register({
name: "copyText",
trackEvent: { category: "element" },
perform: (elements, appState, _, app) => {
const selectedElements = app.scene.getSelectedElements({
selectedElementIds: appState.selectedElementIds,
includeBoundTextElement: true,
});
perform: (elements, appState) => {
const selectedElements = getSelectedElements(
getNonDeletedElements(elements),
appState,
{
includeBoundTextElement: true,
},
);
const text = selectedElements
.reduce((acc: string[], element) => {
@@ -190,15 +199,12 @@ export const copyText = register({
commitToHistory: false,
};
},
predicate: (elements, appState, _, app) => {
predicate: (elements, appState) => {
return (
probablySupportsClipboardWriteText &&
app.scene
.getSelectedElements({
selectedElementIds: appState.selectedElementIds,
includeBoundTextElement: true,
})
.some(isTextElement)
getSelectedElements(elements, appState, {
includeBoundTextElement: true,
}).some(isTextElement)
);
},
contextItemLabel: "labels.copyText",

View File

@@ -158,7 +158,6 @@ export const actionDeleteSelected = register({
...nextAppState,
activeTool: updateActiveTool(appState, { type: "selection" }),
multiElement: null,
activeEmbeddable: null,
},
commitToHistory: isSomeElementSelected(
getNonDeletedElements(elements),

View File

@@ -9,13 +9,19 @@ import { ExcalidrawElement } from "../element/types";
import { updateFrameMembershipOfSelectedElements } from "../frame";
import { t } from "../i18n";
import { CODES, KEYS } from "../keys";
import { isSomeElementSelected } from "../scene";
import { AppClassProperties, AppState } from "../types";
import { getSelectedElements, isSomeElementSelected } from "../scene";
import { AppState } from "../types";
import { arrayToMap, getShortcutKey } from "../utils";
import { register } from "./register";
const enableActionGroup = (appState: AppState, app: AppClassProperties) => {
const selectedElements = app.scene.getSelectedElements(appState);
const enableActionGroup = (
elements: readonly ExcalidrawElement[],
appState: AppState,
) => {
const selectedElements = getSelectedElements(
getNonDeletedElements(elements),
appState,
);
return (
selectedElements.length > 1 &&
// TODO enable distributing frames when implemented properly
@@ -26,10 +32,12 @@ const enableActionGroup = (appState: AppState, app: AppClassProperties) => {
const distributeSelectedElements = (
elements: readonly ExcalidrawElement[],
appState: Readonly<AppState>,
app: AppClassProperties,
distribution: Distribution,
) => {
const selectedElements = app.scene.getSelectedElements(appState);
const selectedElements = getSelectedElements(
getNonDeletedElements(elements),
appState,
);
const updatedElements = distributeElements(selectedElements, distribution);
@@ -38,17 +46,16 @@ const distributeSelectedElements = (
return updateFrameMembershipOfSelectedElements(
elements.map((element) => updatedElementsMap.get(element.id) || element),
appState,
app,
);
};
export const distributeHorizontally = register({
name: "distributeHorizontally",
trackEvent: { category: "element" },
perform: (elements, appState, _, app) => {
perform: (elements, appState) => {
return {
appState,
elements: distributeSelectedElements(elements, appState, app, {
elements: distributeSelectedElements(elements, appState, {
space: "between",
axis: "x",
}),
@@ -57,9 +64,9 @@ export const distributeHorizontally = register({
},
keyTest: (event) =>
!event[KEYS.CTRL_OR_CMD] && event.altKey && event.code === CODES.H,
PanelComponent: ({ elements, appState, updateData, app }) => (
PanelComponent: ({ elements, appState, updateData }) => (
<ToolButton
hidden={!enableActionGroup(appState, app)}
hidden={!enableActionGroup(elements, appState)}
type="button"
icon={DistributeHorizontallyIcon}
onClick={() => updateData(null)}
@@ -75,10 +82,10 @@ export const distributeHorizontally = register({
export const distributeVertically = register({
name: "distributeVertically",
trackEvent: { category: "element" },
perform: (elements, appState, _, app) => {
perform: (elements, appState) => {
return {
appState,
elements: distributeSelectedElements(elements, appState, app, {
elements: distributeSelectedElements(elements, appState, {
space: "between",
axis: "y",
}),
@@ -87,9 +94,9 @@ export const distributeVertically = register({
},
keyTest: (event) =>
!event[KEYS.CTRL_OR_CMD] && event.altKey && event.code === CODES.V,
PanelComponent: ({ elements, appState, updateData, app }) => (
PanelComponent: ({ elements, appState, updateData }) => (
<ToolButton
hidden={!enableActionGroup(appState, app)}
hidden={!enableActionGroup(elements, appState)}
type="button"
icon={DistributeVerticallyIcon}
onClick={() => updateData(null)}

View File

@@ -259,25 +259,22 @@ const duplicateElements = (
return {
elements: finalElements,
appState: {
...appState,
...selectGroupsForSelectedElements(
{
editingGroupId: appState.editingGroupId,
selectedElementIds: nextElementsToSelect.reduce(
(acc: Record<ExcalidrawElement["id"], true>, element) => {
if (!isBoundToContainer(element)) {
acc[element.id] = true;
}
return acc;
},
{},
),
},
getNonDeletedElements(finalElements),
appState,
null,
),
},
appState: selectGroupsForSelectedElements(
{
...appState,
selectedGroupIds: {},
selectedElementIds: nextElementsToSelect.reduce(
(acc: Record<ExcalidrawElement["id"], true>, element) => {
if (!isBoundToContainer(element)) {
acc[element.id] = true;
}
return acc;
},
{},
),
},
getNonDeletedElements(finalElements),
appState,
),
};
};

View File

@@ -1,6 +1,7 @@
import { newElementWith } from "../element/mutateElement";
import { ExcalidrawElement } from "../element/types";
import { KEYS } from "../keys";
import { getSelectedElements } from "../scene";
import { arrayToMap } from "../utils";
import { register } from "./register";
@@ -10,15 +11,14 @@ const shouldLock = (elements: readonly ExcalidrawElement[]) =>
export const actionToggleElementLock = register({
name: "toggleElementLock",
trackEvent: { category: "element" },
predicate: (elements, appState, _, app) => {
const selectedElements = app.scene.getSelectedElements(appState);
predicate: (elements, appState) => {
const selectedElements = getSelectedElements(elements, appState);
return !selectedElements.some(
(element) => element.locked && element.frameId,
);
},
perform: (elements, appState, _, app) => {
const selectedElements = app.scene.getSelectedElements({
selectedElementIds: appState.selectedElementIds,
perform: (elements, appState) => {
const selectedElements = getSelectedElements(elements, appState, {
includeBoundTextElement: true,
includeElementsInFrames: true,
});
@@ -46,9 +46,8 @@ export const actionToggleElementLock = register({
commitToHistory: true,
};
},
contextItemLabel: (elements, appState, app) => {
const selected = app.scene.getSelectedElements({
selectedElementIds: appState.selectedElementIds,
contextItemLabel: (elements, appState) => {
const selected = getSelectedElements(elements, appState, {
includeBoundTextElement: false,
});
if (selected.length === 1 && selected[0].type !== "frame") {
@@ -61,13 +60,12 @@ export const actionToggleElementLock = register({
? "labels.elementLock.lockAll"
: "labels.elementLock.unlockAll";
},
keyTest: (event, appState, elements, app) => {
keyTest: (event, appState, elements) => {
return (
event.key.toLocaleLowerCase() === KEYS.L &&
event[KEYS.CTRL_OR_CMD] &&
event.shiftKey &&
app.scene.getSelectedElements({
selectedElementIds: appState.selectedElementIds,
getSelectedElements(elements, appState, {
includeBoundTextElement: false,
}).length > 0
);

View File

@@ -65,7 +65,7 @@ export const actionChangeExportScale = register({
);
const scaleButtonTitle = `${t(
"imageExportDialog.label.scale",
"buttons.scale",
)} ${s}x (${width}x${height})`;
return (
@@ -102,7 +102,7 @@ export const actionChangeExportBackground = register({
checked={appState.exportBackground}
onChange={(checked) => updateData(checked)}
>
{t("imageExportDialog.label.withBackground")}
{t("labels.withBackground")}
</CheckboxItem>
),
});
@@ -121,8 +121,8 @@ export const actionChangeExportEmbedScene = register({
checked={appState.exportEmbedScene}
onChange={(checked) => updateData(checked)}
>
{t("imageExportDialog.label.embedScene")}
<Tooltip label={t("imageExportDialog.tooltip.embedScene")} long={true}>
{t("labels.exportEmbedScene")}
<Tooltip label={t("labels.exportEmbedScene_details")} long={true}>
<div className="excalidraw-tooltip-icon">{questionCircle}</div>
</Tooltip>
</CheckboxItem>
@@ -277,7 +277,7 @@ export const actionExportWithDarkMode = register({
onChange={(theme: Theme) => {
updateData(theme === THEME.DARK);
}}
title={t("imageExportDialog.label.darkMode")}
title={t("labels.toggleExportColorScheme")}
/>
</div>
),

View File

@@ -19,12 +19,7 @@ import { AppState } from "../types";
export const actionFinalize = register({
name: "finalize",
trackEvent: false,
perform: (
elements,
appState,
_,
{ interactiveCanvas, focusContainer, scene },
) => {
perform: (elements, appState, _, { canvas, focusContainer, scene }) => {
if (appState.editingLinearElement) {
const { elementId, startBindingElement, endBindingElement } =
appState.editingLinearElement;
@@ -137,7 +132,7 @@ export const actionFinalize = register({
appState.activeTool.type !== "freedraw") ||
!multiPointElement
) {
resetCursor(interactiveCanvas);
resetCursor(canvas);
}
let activeTool: AppState["activeTool"];
@@ -165,7 +160,6 @@ export const actionFinalize = register({
multiPointElement
? appState.activeTool
: activeTool,
activeEmbeddable: null,
draggingElement: null,
multiElement: null,
editingElement: null,

View File

@@ -17,12 +17,11 @@ import { updateFrameMembershipOfSelectedElements } from "../frame";
export const actionFlipHorizontal = register({
name: "flipHorizontal",
trackEvent: { category: "element" },
perform: (elements, appState, _, app) => {
perform: (elements, appState) => {
return {
elements: updateFrameMembershipOfSelectedElements(
flipSelectedElements(elements, appState, "horizontal"),
appState,
app,
),
appState,
commitToHistory: true,
@@ -35,12 +34,11 @@ export const actionFlipHorizontal = register({
export const actionFlipVertical = register({
name: "flipVertical",
trackEvent: { category: "element" },
perform: (elements, appState, _, app) => {
perform: (elements, appState) => {
return {
elements: updateFrameMembershipOfSelectedElements(
flipSelectedElements(elements, appState, "vertical"),
appState,
app,
),
appState,
commitToHistory: true,

View File

@@ -3,12 +3,19 @@ import { ExcalidrawElement } from "../element/types";
import { removeAllElementsFromFrame } from "../frame";
import { getFrameElements } from "../frame";
import { KEYS } from "../keys";
import { AppClassProperties, AppState } from "../types";
import { getSelectedElements } from "../scene";
import { AppState } from "../types";
import { setCursorForShape, updateActiveTool } from "../utils";
import { register } from "./register";
const isSingleFrameSelected = (appState: AppState, app: AppClassProperties) => {
const selectedElements = app.scene.getSelectedElements(appState);
const isSingleFrameSelected = (
elements: readonly ExcalidrawElement[],
appState: AppState,
) => {
const selectedElements = getSelectedElements(
getNonDeletedElements(elements),
appState,
);
return selectedElements.length === 1 && selectedElements[0].type === "frame";
};
@@ -16,8 +23,11 @@ const isSingleFrameSelected = (appState: AppState, app: AppClassProperties) => {
export const actionSelectAllElementsInFrame = register({
name: "selectAllElementsInFrame",
trackEvent: { category: "canvas" },
perform: (elements, appState, _, app) => {
const selectedFrame = app.scene.getSelectedElements(appState)[0];
perform: (elements, appState) => {
const selectedFrame = getSelectedElements(
getNonDeletedElements(elements),
appState,
)[0];
if (selectedFrame && selectedFrame.type === "frame") {
const elementsInFrame = getFrameElements(
@@ -29,13 +39,10 @@ export const actionSelectAllElementsInFrame = register({
elements,
appState: {
...appState,
selectedElementIds: elementsInFrame.reduce(
(acc, element) => {
acc[element.id] = true;
return acc;
},
{} as Record<ExcalidrawElement["id"], true>,
),
selectedElementIds: elementsInFrame.reduce((acc, element) => {
acc[element.id] = true;
return acc;
}, {} as Record<ExcalidrawElement["id"], true>),
},
commitToHistory: false,
};
@@ -48,15 +55,17 @@ export const actionSelectAllElementsInFrame = register({
};
},
contextItemLabel: "labels.selectAllElementsInFrame",
predicate: (elements, appState, _, app) =>
isSingleFrameSelected(appState, app),
predicate: (elements, appState) => isSingleFrameSelected(elements, appState),
});
export const actionRemoveAllElementsFromFrame = register({
name: "removeAllElementsFromFrame",
trackEvent: { category: "history" },
perform: (elements, appState, _, app) => {
const selectedFrame = app.scene.getSelectedElements(appState)[0];
perform: (elements, appState) => {
const selectedFrame = getSelectedElements(
getNonDeletedElements(elements),
appState,
)[0];
if (selectedFrame && selectedFrame.type === "frame") {
return {
@@ -78,8 +87,7 @@ export const actionRemoveAllElementsFromFrame = register({
};
},
contextItemLabel: "labels.removeAllElementsFromFrame",
predicate: (elements, appState, _, app) =>
isSingleFrameSelected(appState, app),
predicate: (elements, appState) => isSingleFrameSelected(elements, appState),
});
export const actionupdateFrameRendering = register({
@@ -111,7 +119,7 @@ export const actionSetFrameAsActiveTool = register({
type: "frame",
});
setCursorForShape(app.interactiveCanvas, {
setCursorForShape(app.canvas, {
...appState,
activeTool: nextActiveTool,
});

View File

@@ -4,7 +4,7 @@ import { arrayToMap, getShortcutKey } from "../utils";
import { register } from "./register";
import { UngroupIcon, GroupIcon } from "../components/icons";
import { newElementWith } from "../element/mutateElement";
import { isSomeElementSelected } from "../scene";
import { getSelectedElements, isSomeElementSelected } from "../scene";
import {
getSelectedGroupIds,
selectGroup,
@@ -22,7 +22,7 @@ import {
ExcalidrawFrameElement,
ExcalidrawTextElement,
} from "../element/types";
import { AppClassProperties, AppState } from "../types";
import { AppState } from "../types";
import { isBoundToContainer } from "../element/typeChecks";
import {
getElementsInResizingFrame,
@@ -51,12 +51,14 @@ const allElementsInSameGroup = (elements: readonly ExcalidrawElement[]) => {
const enableActionGroup = (
elements: readonly ExcalidrawElement[],
appState: AppState,
app: AppClassProperties,
) => {
const selectedElements = app.scene.getSelectedElements({
selectedElementIds: appState.selectedElementIds,
includeBoundTextElement: true,
});
const selectedElements = getSelectedElements(
getNonDeletedElements(elements),
appState,
{
includeBoundTextElement: true,
},
);
return (
selectedElements.length >= 2 && !allElementsInSameGroup(selectedElements)
);
@@ -66,10 +68,13 @@ export const actionGroup = register({
name: "group",
trackEvent: { category: "element" },
perform: (elements, appState, _, app) => {
const selectedElements = app.scene.getSelectedElements({
selectedElementIds: appState.selectedElementIds,
includeBoundTextElement: true,
});
const selectedElements = getSelectedElements(
getNonDeletedElements(elements),
appState,
{
includeBoundTextElement: true,
},
);
if (selectedElements.length < 2) {
// nothing to group
return { appState, elements, commitToHistory: false };
@@ -149,26 +154,22 @@ export const actionGroup = register({
];
return {
appState: {
...appState,
...selectGroup(
newGroupId,
{ ...appState, selectedGroupIds: {} },
getNonDeletedElements(nextElements),
),
},
appState: selectGroup(
newGroupId,
{ ...appState, selectedGroupIds: {} },
getNonDeletedElements(nextElements),
),
elements: nextElements,
commitToHistory: true,
};
},
contextItemLabel: "labels.group",
predicate: (elements, appState, _, app) =>
enableActionGroup(elements, appState, app),
predicate: (elements, appState) => enableActionGroup(elements, appState),
keyTest: (event) =>
!event.shiftKey && event[KEYS.CTRL_OR_CMD] && event.key === KEYS.G,
PanelComponent: ({ elements, appState, updateData, app }) => (
PanelComponent: ({ elements, appState, updateData }) => (
<ToolButton
hidden={!enableActionGroup(elements, appState, app)}
hidden={!enableActionGroup(elements, appState)}
type="button"
icon={<GroupIcon theme={appState.theme} />}
onClick={() => updateData(null)}
@@ -190,7 +191,7 @@ export const actionUngroup = register({
let nextElements = [...elements];
const selectedElements = app.scene.getSelectedElements(appState);
const selectedElements = getSelectedElements(nextElements, appState);
const frames = selectedElements
.filter((element) => element.frameId)
.map((element) =>
@@ -215,10 +216,9 @@ export const actionUngroup = register({
});
const updateAppState = selectGroupsForSelectedElements(
appState,
{ ...appState, selectedGroupIds: {} },
getNonDeletedElements(nextElements),
appState,
null,
);
frames.forEach((frame) => {
@@ -246,7 +246,7 @@ export const actionUngroup = register({
);
return {
appState: { ...appState, ...updateAppState },
appState: updateAppState,
elements: nextElements,
commitToHistory: true,
};

View File

@@ -1,6 +1,8 @@
import { getNonDeletedElements } from "../element";
import { LinearElementEditor } from "../element/linearElementEditor";
import { isLinearElement } from "../element/typeChecks";
import { ExcalidrawLinearElement } from "../element/types";
import { getSelectedElements } from "../scene";
import { register } from "./register";
export const actionToggleLinearEditor = register({
@@ -8,18 +10,21 @@ export const actionToggleLinearEditor = register({
trackEvent: {
category: "element",
},
predicate: (elements, appState, _, app) => {
const selectedElements = app.scene.getSelectedElements(appState);
predicate: (elements, appState) => {
const selectedElements = getSelectedElements(elements, appState);
if (selectedElements.length === 1 && isLinearElement(selectedElements[0])) {
return true;
}
return false;
},
perform(elements, appState, _, app) {
const selectedElement = app.scene.getSelectedElements({
selectedElementIds: appState.selectedElementIds,
includeBoundTextElement: true,
})[0] as ExcalidrawLinearElement;
const selectedElement = getSelectedElements(
getNonDeletedElements(elements),
appState,
{
includeBoundTextElement: true,
},
)[0] as ExcalidrawLinearElement;
const editingLinearElement =
appState.editingLinearElement?.elementId === selectedElement.id
@@ -33,11 +38,14 @@ export const actionToggleLinearEditor = register({
commitToHistory: false,
};
},
contextItemLabel: (elements, appState, app) => {
const selectedElement = app.scene.getSelectedElements({
selectedElementIds: appState.selectedElementIds,
includeBoundTextElement: true,
})[0] as ExcalidrawLinearElement;
contextItemLabel: (elements, appState) => {
const selectedElement = getSelectedElements(
getNonDeletedElements(elements),
appState,
{
includeBoundTextElement: true,
},
)[0] as ExcalidrawLinearElement;
return appState.editingLinearElement?.elementId === selectedElement.id
? "labels.lineEditor.exit"
: "labels.lineEditor.edit";

View File

@@ -28,24 +28,21 @@ export const actionSelectAll = register({
}, {});
return {
appState: {
...appState,
...selectGroupsForSelectedElements(
{
editingGroupId: null,
selectedElementIds,
},
getNonDeletedElements(elements),
appState,
app,
),
selectedLinearElement:
// single linear element selected
Object.keys(selectedElementIds).length === 1 &&
isLinearElement(elements[0])
? new LinearElementEditor(elements[0], app.scene)
: null,
},
appState: selectGroupsForSelectedElements(
{
...appState,
selectedLinearElement:
// single linear element selected
Object.keys(selectedElementIds).length === 1 &&
isLinearElement(elements[0])
? new LinearElementEditor(elements[0], app.scene)
: null,
editingGroupId: null,
selectedElementIds,
},
getNonDeletedElements(elements),
appState,
),
commitToHistory: true,
};
},

View File

@@ -90,7 +90,6 @@ export class ActionManager {
event,
this.getAppState(),
this.getElementsIncludingDeleted(),
this.app,
),
);
@@ -169,7 +168,6 @@ export class ActionManager {
appState={this.getAppState()}
updateData={updateData}
appProps={this.app.props}
app={this.app}
data={data}
/>
);

View File

@@ -121,7 +121,6 @@ export type ActionName =
| "removeAllElementsFromFrame"
| "updateFrameRendering"
| "setFrameAsActiveTool"
| "setEmbeddableAsActiveTool"
| "createContainerFromText"
| "wrapTextInContainer";
@@ -131,7 +130,6 @@ export type PanelComponentProps = {
updateData: (formData?: any) => void;
appProps: ExcalidrawProps;
data?: Record<string, any>;
app: AppClassProperties;
};
export interface Action {
@@ -143,14 +141,12 @@ export interface Action {
event: React.KeyboardEvent | KeyboardEvent,
appState: AppState,
elements: readonly ExcalidrawElement[],
app: AppClassProperties,
) => boolean;
contextItemLabel?:
| string
| ((
elements: readonly ExcalidrawElement[],
appState: Readonly<AppState>,
app: AppClassProperties,
) => string);
predicate?: (
elements: readonly ExcalidrawElement[],

View File

@@ -11,7 +11,7 @@ export const trackEvent = (
// Uncomment the next line to track locally
// console.log("Track Event", { category, action, label, value });
if (typeof window === "undefined" || import.meta.env.VITE_WORKER_ID) {
if (typeof window === "undefined" || process.env.JEST_WORKER_ID) {
return;
}

View File

@@ -38,7 +38,6 @@ export const getDefaultAppState = (): Omit<
currentItemStrokeWidth: DEFAULT_ELEMENT_PROPS.strokeWidth,
currentItemTextAlign: DEFAULT_TEXT_ALIGN,
cursorButton: "up",
activeEmbeddable: null,
draggingElement: null,
editingElement: null,
editingGroupId: null,
@@ -140,7 +139,6 @@ const APP_STATE_STORAGE_CONF = (<
currentItemStrokeWidth: { browser: true, export: false, server: false },
currentItemTextAlign: { browser: true, export: false, server: false },
cursorButton: { browser: true, export: false, server: false },
activeEmbeddable: { browser: false, export: false, server: false },
draggingElement: { browser: false, export: false, server: false },
editingElement: { browser: false, export: false, server: false },
editingGroupId: { browser: true, export: false, server: false },
@@ -215,11 +213,11 @@ const _clearAppStateForStorage = <
exportType: ExportType,
) => {
type ExportableKeys = {
[K in keyof typeof APP_STATE_STORAGE_CONF]: (typeof APP_STATE_STORAGE_CONF)[K][ExportType] extends true
[K in keyof typeof APP_STATE_STORAGE_CONF]: typeof APP_STATE_STORAGE_CONF[K][ExportType] extends true
? K
: never;
}[keyof typeof APP_STATE_STORAGE_CONF];
const stateForExport = {} as { [K in ExportableKeys]?: (typeof appState)[K] };
const stateForExport = {} as { [K in ExportableKeys]?: typeof appState[K] };
for (const key of Object.keys(appState) as (keyof typeof appState)[]) {
const propConfig = APP_STATE_STORAGE_CONF[key];
if (propConfig?.[exportType]) {

View File

@@ -6,6 +6,7 @@ import {
import {
DEFAULT_FONT_FAMILY,
DEFAULT_FONT_SIZE,
ENV,
VERTICAL_ALIGN,
} from "./constants";
import { newElement, newLinearElement, newTextElement } from "./element";
@@ -383,7 +384,7 @@ const chartTypeBar = (
y,
groupId,
backgroundColor,
import.meta.env.DEV,
process.env.NODE_ENV === ENV.DEVELOPMENT,
),
];
};
@@ -472,7 +473,7 @@ const chartTypeLine = (
y,
groupId,
backgroundColor,
import.meta.env.DEV,
process.env.NODE_ENV === ENV.DEVELOPMENT,
),
line,
...lines,

View File

@@ -24,7 +24,6 @@ export interface ClipboardData {
files?: BinaryFiles;
text?: string;
errorMessage?: string;
programmaticAPI?: boolean;
}
let CLIPBOARD = "";
@@ -49,7 +48,6 @@ const clipboardContainsElements = (
[
EXPORT_DATA_TYPES.excalidraw,
EXPORT_DATA_TYPES.excalidrawClipboard,
EXPORT_DATA_TYPES.excalidrawClipboardWithAPI,
].includes(contents?.type) &&
Array.isArray(contents.elements)
) {
@@ -193,8 +191,6 @@ export const parseClipboard = async (
try {
const systemClipboardData = JSON.parse(systemClipboard);
const programmaticAPI =
systemClipboardData.type === EXPORT_DATA_TYPES.excalidrawClipboardWithAPI;
if (clipboardContainsElements(systemClipboardData)) {
return {
elements: systemClipboardData.elements,
@@ -202,7 +198,6 @@ export const parseClipboard = async (
text: isPlainPaste
? JSON.stringify(systemClipboardData.elements, null, 2)
: undefined,
programmaticAPI,
};
}
} catch (e) {}

View File

@@ -6,15 +6,12 @@ const pick = <R extends Record<string, any>, K extends readonly (keyof R)[]>(
source: R,
keys: K,
) => {
return keys.reduce(
(acc, key: K[number]) => {
if (key in source) {
acc[key] = source[key];
}
return acc;
},
{} as Pick<R, K[number]>,
) as Pick<R, K[number]>;
return keys.reduce((acc, key: K[number]) => {
if (key in source) {
acc[key] = source[key];
}
return acc;
}, {} as Pick<R, K[number]>) as Pick<R, K[number]>;
};
export type ColorPickerColor =
@@ -24,7 +21,7 @@ export type ColorPickerColor =
export type ColorTuple = readonly [string, string, string, string, string];
export type ColorPalette = Merge<
Record<ColorPickerColor, ColorTuple>,
{ black: "#1e1e1e"; white: "#ffffff"; transparent: "transparent" }
{ black: string; white: string; transparent: string }
>;
// used general type instead of specific type (ColorPalette) to support custom colors

View File

@@ -36,7 +36,7 @@ import {
import "./Actions.scss";
import DropdownMenu from "./dropdownMenu/DropdownMenu";
import { EmbedIcon, extraToolsIcon, frameToolIcon } from "./icons";
import { extraToolsIcon, frameToolIcon } from "./icons";
import { KEYS } from "../keys";
export const SelectedShapeActions = ({
@@ -213,13 +213,13 @@ export const SelectedShapeActions = ({
};
export const ShapesSwitcher = ({
interactiveCanvas,
canvas,
activeTool,
setAppState,
onImageAction,
appState,
}: {
interactiveCanvas: HTMLCanvasElement | null;
canvas: HTMLCanvasElement | null;
activeTool: UIAppState["activeTool"];
setAppState: React.Component<any, UIAppState>["setState"];
onImageAction: (data: { pointerType: PointerType | null }) => void;
@@ -266,11 +266,10 @@ export const ShapesSwitcher = ({
});
setAppState({
activeTool: nextActiveTool,
activeEmbeddable: null,
multiElement: null,
selectedElementIds: {},
});
setCursorForShape(interactiveCanvas, {
setCursorForShape(canvas, {
...appState,
activeTool: nextActiveTool,
});
@@ -284,72 +283,39 @@ export const ShapesSwitcher = ({
<div className="App-toolbar__divider" />
{/* TEMP HACK because dropdown doesn't work well inside mobile toolbar */}
{device.isMobile ? (
<>
<ToolButton
className={clsx("Shape", { fillable: false })}
type="radio"
icon={frameToolIcon}
checked={activeTool.type === "frame"}
name="editor-current-shape"
title={`${capitalizeString(
t("toolBar.frame"),
)}${KEYS.F.toLocaleUpperCase()}`}
keyBindingLabel={KEYS.F.toLocaleUpperCase()}
aria-label={capitalizeString(t("toolBar.frame"))}
aria-keyshortcuts={KEYS.F.toLocaleUpperCase()}
data-testid={`toolbar-frame`}
onPointerDown={({ pointerType }) => {
if (!appState.penDetected && pointerType === "pen") {
setAppState({
penDetected: true,
penMode: true,
});
}
}}
onChange={({ pointerType }) => {
trackEvent("toolbar", "frame", "ui");
const nextActiveTool = updateActiveTool(appState, {
type: "frame",
});
<ToolButton
className={clsx("Shape", { fillable: false })}
type="radio"
icon={frameToolIcon}
checked={activeTool.type === "frame"}
name="editor-current-shape"
title={`${capitalizeString(
t("toolBar.frame"),
)}${KEYS.F.toLocaleUpperCase()}`}
keyBindingLabel={KEYS.F.toLocaleUpperCase()}
aria-label={capitalizeString(t("toolBar.frame"))}
aria-keyshortcuts={KEYS.F.toLocaleUpperCase()}
data-testid={`toolbar-frame`}
onPointerDown={({ pointerType }) => {
if (!appState.penDetected && pointerType === "pen") {
setAppState({
activeTool: nextActiveTool,
multiElement: null,
selectedElementIds: {},
activeEmbeddable: null,
penDetected: true,
penMode: true,
});
}}
/>
<ToolButton
className={clsx("Shape", { fillable: false })}
type="radio"
icon={EmbedIcon}
checked={activeTool.type === "embeddable"}
name="editor-current-shape"
title={capitalizeString(t("toolBar.embeddable"))}
aria-label={capitalizeString(t("toolBar.embeddable"))}
data-testid={`toolbar-embeddable`}
onPointerDown={({ pointerType }) => {
if (!appState.penDetected && pointerType === "pen") {
setAppState({
penDetected: true,
penMode: true,
});
}
}}
onChange={({ pointerType }) => {
trackEvent("toolbar", "embeddable", "ui");
const nextActiveTool = updateActiveTool(appState, {
type: "embeddable",
});
setAppState({
activeTool: nextActiveTool,
multiElement: null,
selectedElementIds: {},
activeEmbeddable: null,
});
}}
/>
</>
}
}}
onChange={({ pointerType }) => {
trackEvent("toolbar", "frame", "ui");
const nextActiveTool = updateActiveTool(appState, {
type: "frame",
});
setAppState({
activeTool: nextActiveTool,
multiElement: null,
selectedElementIds: {},
});
}}
/>
) : (
<DropdownMenu open={isExtraToolsMenuOpen}>
<DropdownMenu.Trigger
@@ -381,22 +347,6 @@ export const ShapesSwitcher = ({
>
{t("toolBar.frame")}
</DropdownMenu.Item>
<DropdownMenu.Item
onSelect={() => {
const nextActiveTool = updateActiveTool(appState, {
type: "embeddable",
});
setAppState({
activeTool: nextActiveTool,
multiElement: null,
selectedElementIds: {},
});
}}
icon={EmbedIcon}
data-testid="toolbar-embeddable"
>
{t("toolBar.embeddable")}
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu>
)}

View File

@@ -4,16 +4,15 @@ import { reseed } from "../random";
import { render, queryByTestId } from "../tests/test-utils";
import ExcalidrawApp from "../excalidraw-app";
import { vi } from "vitest";
const renderStaticScene = vi.spyOn(Renderer, "renderStaticScene");
const renderScene = jest.spyOn(Renderer, "renderScene");
describe("Test <App/>", () => {
beforeEach(async () => {
// Unmount ReactDOM from root
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
localStorage.clear();
renderStaticScene.mockClear();
renderScene.mockClear();
reseed(7);
});

File diff suppressed because it is too large Load Diff

View File

@@ -8,7 +8,7 @@ import {
} from "./colorPickerUtils";
import HotkeyLabel from "./HotkeyLabel";
import { ColorPaletteCustom } from "../../colors";
import { TranslationKeys, t } from "../../i18n";
import { t } from "../../i18n";
interface PickerColorListProps {
palette: ColorPaletteCustom;
@@ -48,11 +48,7 @@ const PickerColorList = ({
(Array.isArray(value) ? value[activeShade] : value) || "transparent";
const keybinding = colorPickerHotkeyBindings[index];
const label = t(
`colors.${key.replace(/\d+/, "")}` as unknown as TranslationKeys,
null,
"",
);
const label = t(`colors.${key.replace(/\d+/, "")}`, null, "");
return (
<button

View File

@@ -1,6 +1,6 @@
import clsx from "clsx";
import { Popover } from "./Popover";
import { t, TranslationKeys } from "../i18n";
import { t } from "../i18n";
import "./ContextMenu.scss";
import {
@@ -82,15 +82,9 @@ export const ContextMenu = React.memo(
let label = "";
if (item.contextItemLabel) {
if (typeof item.contextItemLabel === "function") {
label = t(
item.contextItemLabel(
elements,
appState,
actionManager.app,
) as unknown as TranslationKeys,
);
label = t(item.contextItemLabel(elements, appState));
} else {
label = t(item.contextItemLabel as unknown as TranslationKeys);
label = t(item.contextItemLabel);
}
}

View File

@@ -107,8 +107,7 @@
border-radius: 1rem;
background-color: var(--button-color);
box-shadow:
0 3px 5px -1px rgba(0, 0, 0, 0.28),
box-shadow: 0 3px 5px -1px rgba(0, 0, 0, 0.28),
0 6px 10px 0 rgba(0, 0, 0, 0.14);
font-family: Cascadia;

View File

@@ -8,9 +8,9 @@ import { mutateElement } from "../element/mutateElement";
import { useCreatePortalContainer } from "../hooks/useCreatePortalContainer";
import { useOutsideClick } from "../hooks/useOutsideClick";
import { KEYS } from "../keys";
import { invalidateShapeForElement } from "../renderer/renderElement";
import { getSelectedElements } from "../scene";
import Scene from "../scene/Scene";
import { ShapeCache } from "../scene/ShapeCache";
import { useApp, useExcalidrawContainer, useExcalidrawElements } from "./App";
import "./EyeDropper.scss";
@@ -58,7 +58,7 @@ export const EyeDropper: React.FC<{
return;
}
let currentColor: string = COLOR_PALETTE.black;
let currentColor = COLOR_PALETTE.black;
let isHoldingPointerDown = false;
const ctx = app.canvas.getContext("2d")!;
@@ -77,8 +77,8 @@ export const EyeDropper: React.FC<{
colorPreviewDiv.style.left = `${clientX + 20}px`;
const pixel = ctx.getImageData(
(clientX - appState.offsetLeft) * window.devicePixelRatio,
(clientY - appState.offsetTop) * window.devicePixelRatio,
clientX * window.devicePixelRatio - appState.offsetLeft,
clientY * window.devicePixelRatio - appState.offsetTop,
1,
1,
).data;
@@ -98,7 +98,7 @@ export const EyeDropper: React.FC<{
},
false,
);
ShapeCache.delete(element);
invalidateShapeForElement(element);
}
Scene.getScene(
metaStuffRef.current.selectedElements[0],

View File

@@ -1,5 +1,7 @@
import { t } from "../i18n";
import { AppClassProperties, Device, UIAppState } from "../types";
import { NonDeletedExcalidrawElement } from "../element/types";
import { getSelectedElements } from "../scene";
import { Device, UIAppState } from "../types";
import {
isImageElement,
isLinearElement,
@@ -13,12 +15,17 @@ import "./HintViewer.scss";
interface HintViewerProps {
appState: UIAppState;
elements: readonly NonDeletedExcalidrawElement[];
isMobile: boolean;
device: Device;
app: AppClassProperties;
}
const getHints = ({ appState, isMobile, device, app }: HintViewerProps) => {
const getHints = ({
appState,
elements,
isMobile,
device,
}: HintViewerProps) => {
const { activeTool, isResizing, isRotating, lastPointerDownWith } = appState;
const multiMode = appState.multiElement !== null;
@@ -44,15 +51,11 @@ const getHints = ({ appState, isMobile, device, app }: HintViewerProps) => {
return t("hints.text");
}
if (activeTool.type === "embeddable") {
return t("hints.embeddable");
}
if (appState.activeTool.type === "image" && appState.pendingImageElementId) {
return t("hints.placeImage");
}
const selectedElements = app.scene.getSelectedElements(appState);
const selectedElements = getSelectedElements(elements, appState);
if (
isResizing &&
@@ -112,15 +115,15 @@ const getHints = ({ appState, isMobile, device, app }: HintViewerProps) => {
export const HintViewer = ({
appState,
elements,
isMobile,
device,
app,
}: HintViewerProps) => {
let hint = getHints({
appState,
elements,
isMobile,
device,
app,
});
if (!hint) {
return null;

View File

@@ -34,7 +34,7 @@ const JSONExportModal = ({
actionManager: ActionManager;
onCloseRequest: () => void;
exportOpts: ExportOpts;
canvas: HTMLCanvasElement;
canvas: HTMLCanvasElement | null;
}) => {
const { onExportToBackend } = exportOpts;
return (
@@ -100,7 +100,7 @@ export const JSONExportDialog = ({
files: BinaryFiles;
actionManager: ActionManager;
exportOpts: ExportOpts;
canvas: HTMLCanvasElement;
canvas: HTMLCanvasElement | null;
setAppState: React.Component<any, UIAppState>["setState"];
}) => {
const handleClose = React.useCallback(() => {

View File

@@ -70,16 +70,12 @@
font-weight: 500;
opacity: 0;
visibility: hidden;
transition:
visibility 0s linear 0s,
opacity 0.5s;
transition: visibility 0s linear 0s, opacity 0.5s;
&--visible {
opacity: 1;
visibility: visible;
transition:
visibility 0s linear 300ms,
opacity 0.5s;
transition: visibility 0s linear 300ms, opacity 0.5s;
transition-delay: 0.8s;
}
}

View File

@@ -57,8 +57,7 @@ interface LayerUIProps {
actionManager: ActionManager;
appState: UIAppState;
files: BinaryFiles;
canvas: HTMLCanvasElement;
interactiveCanvas: HTMLCanvasElement | null;
canvas: HTMLCanvasElement | null;
setAppState: React.Component<any, AppState>["setState"];
elements: readonly NonDeletedExcalidrawElement[];
onLockToggle: () => void;
@@ -73,7 +72,6 @@ interface LayerUIProps {
onExportImage: AppClassProperties["onExportImage"];
renderWelcomeScreen: boolean;
children?: React.ReactNode;
app: AppClassProperties;
}
const DefaultMainMenu: React.FC<{
@@ -118,7 +116,6 @@ const LayerUI = ({
setAppState,
elements,
canvas,
interactiveCanvas,
onLockToggle,
onHandToolToggle,
onPenModeToggle,
@@ -130,7 +127,6 @@ const LayerUI = ({
onExportImage,
renderWelcomeScreen,
children,
app,
}: LayerUIProps) => {
const device = useDevice();
const tunnels = useInitializeTunnels();
@@ -244,9 +240,9 @@ const LayerUI = ({
>
<HintViewer
appState={appState}
elements={elements}
isMobile={device.isMobile}
device={device}
app={app}
/>
{heading}
<Stack.Row gap={1}>
@@ -274,7 +270,7 @@ const LayerUI = ({
<ShapesSwitcher
appState={appState}
interactiveCanvas={interactiveCanvas}
canvas={canvas}
activeTool={appState.activeTool}
setAppState={setAppState}
onImageAction={({ pointerType }) => {
@@ -405,7 +401,6 @@ const LayerUI = ({
)}
{device.isMobile && (
<MobileMenu
app={app}
appState={appState}
elements={elements}
actionManager={actionManager}
@@ -415,7 +410,7 @@ const LayerUI = ({
onLockToggle={onLockToggle}
onHandToolToggle={onHandToolToggle}
onPenModeToggle={onPenModeToggle}
interactiveCanvas={interactiveCanvas}
canvas={canvas}
onImageAction={onImageAction}
renderTopRightUI={renderTopRightUI}
renderCustomStats={renderCustomStats}
@@ -466,7 +461,7 @@ const LayerUI = ({
className="scroll-back-to-content"
onClick={() => {
setAppState((appState) => ({
...calculateScrollCenter(elements, appState),
...calculateScrollCenter(elements, appState, canvas),
}));
}}
>
@@ -509,18 +504,8 @@ const areEqual = (prevProps: LayerUIProps, nextProps: LayerUIProps) => {
return false;
}
const {
canvas: _pC,
interactiveCanvas: _pIC,
appState: prevAppState,
...prev
} = prevProps;
const {
canvas: _nC,
interactiveCanvas: _nIC,
appState: nextAppState,
...next
} = nextProps;
const { canvas: _prevCanvas, appState: prevAppState, ...prev } = prevProps;
const { canvas: _nextCanvas, appState: nextAppState, ...next } = nextProps;
return (
isShallowEqual(

View File

@@ -29,7 +29,6 @@ import "./LibraryMenu.scss";
import { LibraryMenuControlButtons } from "./LibraryMenuControlButtons";
import { isShallowEqual } from "../utils";
import { NonDeletedExcalidrawElement } from "../element/types";
import { LIBRARY_DISABLED_TYPES } from "../constants";
export const isLibraryMenuOpenAtom = atom(false);
@@ -69,12 +68,11 @@ export const LibraryMenuContent = ({
libraryItems: LibraryItems,
) => {
trackEvent("element", "addToLibrary", "ui");
for (const type of LIBRARY_DISABLED_TYPES) {
if (processedElements.some((element) => element.type === type)) {
return setAppState({
errorMessage: t(`errors.libraryElementTypeError.${type}`),
});
}
if (processedElements.some((element) => element.type === "image")) {
return setAppState({
errorMessage:
"Support for adding images to the library coming soon!",
});
}
const nextItems: LibraryItems = [
{
@@ -199,7 +197,6 @@ export const LibraryMenu = () => {
setAppState({
selectedElementIds: {},
selectedGroupIds: {},
activeEmbeddable: null,
});
}, [setAppState]);

View File

@@ -16,7 +16,7 @@ const LibraryMenuBrowseButton = ({
return (
<a
className="library-menu-browse-button"
href={`${import.meta.env.VITE_APP_LIBRARY_URL}?target=${
href={`${process.env.REACT_APP_LIBRARY_URL}?target=${
window.name || "_blank"
}&referrer=${referrer}&useHash=true&token=${id}&theme=${theme}&version=${
VERSIONS.excalidrawLibrary

View File

@@ -12,11 +12,6 @@
box-sizing: border-box;
border-radius: var(--border-radius-lg);
svg {
// to prevent clicks on links and such
pointer-events: none;
}
&--hover {
border-color: var(--color-primary);
}

View File

@@ -1,11 +1,5 @@
import React from "react";
import {
AppClassProperties,
AppState,
Device,
ExcalidrawProps,
UIAppState,
} from "../types";
import { AppState, Device, ExcalidrawProps, UIAppState } from "../types";
import { ActionManager } from "../actions/manager";
import { t } from "../i18n";
import Stack from "./Stack";
@@ -36,7 +30,7 @@ type MobileMenuProps = {
onLockToggle: () => void;
onHandToolToggle: () => void;
onPenModeToggle: () => void;
interactiveCanvas: HTMLCanvasElement | null;
canvas: HTMLCanvasElement | null;
onImageAction: (data: { insertOnCanvasDirectly: boolean }) => void;
renderTopRightUI?: (
@@ -47,7 +41,6 @@ type MobileMenuProps = {
renderSidebars: () => JSX.Element | null;
device: Device;
renderWelcomeScreen: boolean;
app: AppClassProperties;
};
export const MobileMenu = ({
@@ -58,14 +51,13 @@ export const MobileMenu = ({
onLockToggle,
onHandToolToggle,
onPenModeToggle,
interactiveCanvas,
canvas,
onImageAction,
renderTopRightUI,
renderCustomStats,
renderSidebars,
device,
renderWelcomeScreen,
app,
}: MobileMenuProps) => {
const {
WelcomeScreenCenterTunnel,
@@ -85,7 +77,7 @@ export const MobileMenu = ({
<Stack.Row gap={1}>
<ShapesSwitcher
appState={appState}
interactiveCanvas={interactiveCanvas}
canvas={canvas}
activeTool={appState.activeTool}
setAppState={setAppState}
onImageAction={({ pointerType }) => {
@@ -127,9 +119,9 @@ export const MobileMenu = ({
</Section>
<HintViewer
appState={appState}
elements={elements}
isMobile={true}
device={device}
app={app}
/>
</FixedSideContainer>
);
@@ -202,7 +194,7 @@ export const MobileMenu = ({
className="scroll-back-to-content"
onClick={() => {
setAppState((appState) => ({
...calculateScrollCenter(elements, appState),
...calculateScrollCenter(elements, appState, canvas),
}));
}}
>

View File

@@ -319,7 +319,7 @@ const PublishLibrary = ({
formData.append("twitterHandle", libraryData.twitterHandle);
formData.append("website", libraryData.website);
fetch(`${import.meta.env.VITE_APP_LIBRARY_BACKEND}/submit`, {
fetch(`${process.env.REACT_APP_LIBRARY_BACKEND}/submit`, {
method: "post",
body: formData,
})

View File

@@ -3,7 +3,7 @@ import { t } from "../i18n";
import { useExcalidrawContainer } from "./App";
export const Section: React.FC<{
heading: "canvasActions" | "selectedShapeActions" | "shapes";
heading: string;
children?: React.ReactNode | ((heading: React.ReactNode) => React.ReactNode);
className?: string;
}> = ({ heading, children, ...props }) => {

View File

@@ -10,7 +10,6 @@ import {
waitFor,
withExcalidrawDimensions,
} from "../../tests/test-utils";
import { vi } from "vitest";
export const assertSidebarDockButton = async <T extends boolean>(
hasDockButton: T,
@@ -206,7 +205,7 @@ describe("Sidebar", () => {
});
it("<Sidebar.Header> should render close button", async () => {
const onStateChange = vi.fn();
const onStateChange = jest.fn();
const CustomExcalidraw = () => {
return (
<Excalidraw

View File

@@ -53,7 +53,7 @@ export const SidebarInner = forwardRef(
}: SidebarProps & Omit<React.RefAttributes<HTMLDivElement>, "onSelect">,
ref: React.ForwardedRef<HTMLDivElement>,
) => {
if (import.meta.env.DEV && onDock && docked == null) {
if (process.env.NODE_ENV === "development" && onDock && docked == null) {
console.warn(
"Sidebar: `docked` must be set when `onDock` is supplied for the sidebar to be user-dockable. To hide this message, either pass `docked` or remove `onDock`",
);

View File

@@ -25,6 +25,17 @@
white-space: pre-wrap;
}
.Toast__message--spinner {
padding: 0 3rem;
}
.Toast__spinner {
position: absolute;
left: 1.5rem;
top: 50%;
margin-top: -8px;
}
.close {
position: absolute;
top: 0;

View File

@@ -1,5 +1,7 @@
import clsx from "clsx";
import { useCallback, useEffect, useRef } from "react";
import { CloseIcon } from "./icons";
import Spinner from "./Spinner";
import "./Toast.scss";
import { ToolButton } from "./ToolButton";
@@ -9,12 +11,14 @@ export const Toast = ({
message,
onClose,
closable = false,
spinner = true,
// To prevent autoclose, pass duration as Infinity
duration = DEFAULT_TOAST_TIMEOUT,
}: {
message: string;
onClose: () => void;
closable?: boolean;
spinner?: boolean;
duration?: number;
}) => {
const timerRef = useRef<number>(0);
@@ -44,7 +48,18 @@ export const Toast = ({
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
>
<p className="Toast__message">{message}</p>
{spinner && (
<div className="Toast__spinner">
<Spinner />
</div>
)}
<p
className={clsx("Toast__message", {
"Toast__message--spinner": spinner,
})}
>
{message}
</p>
{closable && (
<ToolButton
icon={CloseIcon}

View File

@@ -3,7 +3,6 @@ import { render } from "@testing-library/react";
import fallbackLangData from "../locales/en.json";
import Trans from "./Trans";
import { TranslationKeys } from "../i18n";
describe("Test <Trans/>", () => {
it("should translate the the strings correctly", () => {
@@ -19,27 +18,24 @@ describe("Test <Trans/>", () => {
const { getByTestId } = render(
<>
<div data-testid="test1">
<Trans
i18nKey={"transTest.key1" as unknown as TranslationKeys}
audience="world"
/>
<Trans i18nKey="transTest.key1" audience="world" />
</div>
<div data-testid="test2">
<Trans
i18nKey={"transTest.key2" as unknown as TranslationKeys}
i18nKey="transTest.key2"
link={(el) => <a href="https://example.com">{el}</a>}
/>
</div>
<div data-testid="test3">
<Trans
i18nKey={"transTest.key3" as unknown as TranslationKeys}
i18nKey="transTest.key3"
link={(el) => <a href="https://example.com">{el}</a>}
location="the button"
/>
</div>
<div data-testid="test4">
<Trans
i18nKey={"transTest.key4" as unknown as TranslationKeys}
i18nKey="transTest.key4"
link={(el) => <a href="https://example.com">{el}</a>}
location="the button"
bold={(el) => <strong>{el}</strong>}
@@ -47,7 +43,7 @@ describe("Test <Trans/>", () => {
</div>
<div data-testid="test5">
<Trans
i18nKey={"transTest.key5" as unknown as TranslationKeys}
i18nKey="transTest.key5"
connect-link={(el) => <a href="https://example.com">{el}</a>}
/>
</div>

View File

@@ -1,6 +1,6 @@
import React from "react";
import { TranslationKeys, useI18n } from "../i18n";
import { useI18n } from "../i18n";
// Used for splitting i18nKey into tokens in Trans component
// Example:
@@ -153,7 +153,7 @@ const Trans = ({
children,
...props
}: {
i18nKey: TranslationKeys;
i18nKey: string;
[key: string]: React.ReactNode | ((el: React.ReactNode) => React.ReactNode);
}) => {
const { t } = useI18n();

View File

@@ -1,6 +1,6 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Test <App/> > should show error modal when using brave and measureText API is not working 1`] = `
exports[`Test <App/> should show error modal when using brave and measureText API is not working 1`] = `
<div
data-testid="brave-measure-text-error"
>

View File

@@ -1,226 +0,0 @@
import React, { useEffect, useRef } from "react";
import { renderInteractiveScene } from "../../renderer/renderScene";
import {
isRenderThrottlingEnabled,
isShallowEqual,
sceneCoordsToViewportCoords,
} from "../../utils";
import { CURSOR_TYPE } from "../../constants";
import { t } from "../../i18n";
import type { DOMAttributes } from "react";
import type { AppState, InteractiveCanvasAppState } from "../../types";
import type {
InteractiveCanvasRenderConfig,
RenderInteractiveSceneCallback,
} from "../../scene/types";
import type { NonDeletedExcalidrawElement } from "../../element/types";
type InteractiveCanvasProps = {
containerRef: React.RefObject<HTMLDivElement>;
canvas: HTMLCanvasElement | null;
elements: readonly NonDeletedExcalidrawElement[];
visibleElements: readonly NonDeletedExcalidrawElement[];
selectedElements: readonly NonDeletedExcalidrawElement[];
versionNonce: number | undefined;
selectionNonce: number | undefined;
scale: number;
appState: InteractiveCanvasAppState;
renderInteractiveSceneCallback: (
data: RenderInteractiveSceneCallback,
) => void;
handleCanvasRef: (canvas: HTMLCanvasElement | null) => void;
onContextMenu: Exclude<
DOMAttributes<HTMLCanvasElement | HTMLDivElement>["onContextMenu"],
undefined
>;
onPointerMove: Exclude<
DOMAttributes<HTMLCanvasElement>["onPointerMove"],
undefined
>;
onPointerUp: Exclude<
DOMAttributes<HTMLCanvasElement>["onPointerUp"],
undefined
>;
onPointerCancel: Exclude<
DOMAttributes<HTMLCanvasElement>["onPointerCancel"],
undefined
>;
onTouchMove: Exclude<
DOMAttributes<HTMLCanvasElement>["onTouchMove"],
undefined
>;
onPointerDown: Exclude<
DOMAttributes<HTMLCanvasElement>["onPointerDown"],
undefined
>;
onDoubleClick: Exclude<
DOMAttributes<HTMLCanvasElement>["onDoubleClick"],
undefined
>;
};
const InteractiveCanvas = (props: InteractiveCanvasProps) => {
const isComponentMounted = useRef(false);
useEffect(() => {
if (!isComponentMounted.current) {
isComponentMounted.current = true;
return;
}
const cursorButton: {
[id: string]: string | undefined;
} = {};
const pointerViewportCoords: InteractiveCanvasRenderConfig["remotePointerViewportCoords"] =
{};
const remoteSelectedElementIds: InteractiveCanvasRenderConfig["remoteSelectedElementIds"] =
{};
const pointerUsernames: { [id: string]: string } = {};
const pointerUserStates: { [id: string]: string } = {};
props.appState.collaborators.forEach((user, socketId) => {
if (user.selectedElementIds) {
for (const id of Object.keys(user.selectedElementIds)) {
if (!(id in remoteSelectedElementIds)) {
remoteSelectedElementIds[id] = [];
}
remoteSelectedElementIds[id].push(socketId);
}
}
if (!user.pointer) {
return;
}
if (user.username) {
pointerUsernames[socketId] = user.username;
}
if (user.userState) {
pointerUserStates[socketId] = user.userState;
}
pointerViewportCoords[socketId] = sceneCoordsToViewportCoords(
{
sceneX: user.pointer.x,
sceneY: user.pointer.y,
},
props.appState,
);
cursorButton[socketId] = user.button;
});
const selectionColor =
(props.containerRef?.current &&
getComputedStyle(props.containerRef.current).getPropertyValue(
"--color-selection",
)) ||
"#6965db";
renderInteractiveScene(
{
canvas: props.canvas,
elements: props.elements,
visibleElements: props.visibleElements,
selectedElements: props.selectedElements,
scale: window.devicePixelRatio,
appState: props.appState,
renderConfig: {
remotePointerViewportCoords: pointerViewportCoords,
remotePointerButton: cursorButton,
remoteSelectedElementIds,
remotePointerUsernames: pointerUsernames,
remotePointerUserStates: pointerUserStates,
selectionColor,
renderScrollbars: false,
},
callback: props.renderInteractiveSceneCallback,
},
isRenderThrottlingEnabled(),
);
});
return (
<canvas
className="excalidraw__canvas interactive"
style={{
width: props.appState.width,
height: props.appState.height,
cursor: props.appState.viewModeEnabled
? CURSOR_TYPE.GRAB
: CURSOR_TYPE.AUTO,
}}
width={props.appState.width * props.scale}
height={props.appState.height * props.scale}
ref={props.handleCanvasRef}
onContextMenu={props.onContextMenu}
onPointerMove={props.onPointerMove}
onPointerUp={props.onPointerUp}
onPointerCancel={props.onPointerCancel}
onTouchMove={props.onTouchMove}
onPointerDown={props.onPointerDown}
onDoubleClick={
props.appState.viewModeEnabled ? undefined : props.onDoubleClick
}
>
{t("labels.drawingCanvas")}
</canvas>
);
};
const getRelevantAppStateProps = (
appState: AppState,
): InteractiveCanvasAppState => ({
zoom: appState.zoom,
scrollX: appState.scrollX,
scrollY: appState.scrollY,
width: appState.width,
height: appState.height,
viewModeEnabled: appState.viewModeEnabled,
editingGroupId: appState.editingGroupId,
editingLinearElement: appState.editingLinearElement,
selectedElementIds: appState.selectedElementIds,
frameToHighlight: appState.frameToHighlight,
offsetLeft: appState.offsetLeft,
offsetTop: appState.offsetTop,
theme: appState.theme,
pendingImageElementId: appState.pendingImageElementId,
selectionElement: appState.selectionElement,
selectedGroupIds: appState.selectedGroupIds,
selectedLinearElement: appState.selectedLinearElement,
multiElement: appState.multiElement,
isBindingEnabled: appState.isBindingEnabled,
suggestedBindings: appState.suggestedBindings,
isRotating: appState.isRotating,
elementsToHighlight: appState.elementsToHighlight,
openSidebar: appState.openSidebar,
showHyperlinkPopup: appState.showHyperlinkPopup,
collaborators: appState.collaborators, // Necessary for collab. sessions
activeEmbeddable: appState.activeEmbeddable,
});
const areEqual = (
prevProps: InteractiveCanvasProps,
nextProps: InteractiveCanvasProps,
) => {
// This could be further optimised if needed, as we don't have to render interactive canvas on each scene mutation
if (
prevProps.selectionNonce !== nextProps.selectionNonce ||
prevProps.versionNonce !== nextProps.versionNonce ||
prevProps.scale !== nextProps.scale ||
// we need to memoize on element arrays because they may have renewed
// even if versionNonce didn't change (e.g. we filter elements out based
// on appState)
prevProps.elements !== nextProps.elements ||
prevProps.visibleElements !== nextProps.visibleElements ||
prevProps.selectedElements !== nextProps.selectedElements
) {
return false;
}
// Comparing the interactive appState for changes in case of some edge cases
return isShallowEqual(
// asserting AppState because we're being passed the whole AppState
// but resolve to only the InteractiveCanvas-relevant props
getRelevantAppStateProps(prevProps.appState as AppState),
getRelevantAppStateProps(nextProps.appState as AppState),
);
};
export default React.memo(InteractiveCanvas, areEqual);

View File

@@ -1,110 +0,0 @@
import React, { useEffect, useRef } from "react";
import { RoughCanvas } from "roughjs/bin/canvas";
import { renderStaticScene } from "../../renderer/renderScene";
import { isRenderThrottlingEnabled, isShallowEqual } from "../../utils";
import type { AppState, StaticCanvasAppState } from "../../types";
import type { StaticCanvasRenderConfig } from "../../scene/types";
import type { NonDeletedExcalidrawElement } from "../../element/types";
type StaticCanvasProps = {
canvas: HTMLCanvasElement;
rc: RoughCanvas;
elements: readonly NonDeletedExcalidrawElement[];
visibleElements: readonly NonDeletedExcalidrawElement[];
versionNonce: number | undefined;
selectionNonce: number | undefined;
scale: number;
appState: StaticCanvasAppState;
renderConfig: StaticCanvasRenderConfig;
};
const StaticCanvas = (props: StaticCanvasProps) => {
const wrapperRef = useRef<HTMLDivElement>(null);
const isComponentMounted = useRef(false);
useEffect(() => {
const wrapper = wrapperRef.current;
if (!wrapper) {
return;
}
const canvas = props.canvas;
if (!isComponentMounted.current) {
isComponentMounted.current = true;
wrapper.replaceChildren(canvas);
canvas.classList.add("excalidraw__canvas", "static");
}
canvas.style.width = `${props.appState.width}px`;
canvas.style.height = `${props.appState.height}px`;
canvas.width = props.appState.width * props.scale;
canvas.height = props.appState.height * props.scale;
renderStaticScene(
{
canvas,
rc: props.rc,
scale: props.scale,
elements: props.elements,
visibleElements: props.visibleElements,
appState: props.appState,
renderConfig: props.renderConfig,
},
isRenderThrottlingEnabled(),
);
});
return <div className="excalidraw__canvas-wrapper" ref={wrapperRef} />;
};
const getRelevantAppStateProps = (
appState: AppState,
): StaticCanvasAppState => ({
zoom: appState.zoom,
scrollX: appState.scrollX,
scrollY: appState.scrollY,
width: appState.width,
height: appState.height,
viewModeEnabled: appState.viewModeEnabled,
offsetLeft: appState.offsetLeft,
offsetTop: appState.offsetTop,
theme: appState.theme,
pendingImageElementId: appState.pendingImageElementId,
shouldCacheIgnoreZoom: appState.shouldCacheIgnoreZoom,
viewBackgroundColor: appState.viewBackgroundColor,
exportScale: appState.exportScale,
selectedElementsAreBeingDragged: appState.selectedElementsAreBeingDragged,
gridSize: appState.gridSize,
frameRendering: appState.frameRendering,
selectedElementIds: appState.selectedElementIds,
frameToHighlight: appState.frameToHighlight,
editingGroupId: appState.editingGroupId,
});
const areEqual = (
prevProps: StaticCanvasProps,
nextProps: StaticCanvasProps,
) => {
if (
prevProps.versionNonce !== nextProps.versionNonce ||
prevProps.scale !== nextProps.scale ||
// we need to memoize on element arrays because they may have renewed
// even if versionNonce didn't change (e.g. we filter elements out based
// on appState)
prevProps.elements !== nextProps.elements ||
prevProps.visibleElements !== nextProps.visibleElements
) {
return false;
}
return isShallowEqual(
// asserting AppState because we're being passed the whole AppState
// but resolve to only the StaticCanvas-relevant props
getRelevantAppStateProps(prevProps.appState as AppState),
getRelevantAppStateProps(nextProps.appState as AppState),
);
};
export default React.memo(StaticCanvas, areEqual);

View File

@@ -1,4 +0,0 @@
import InteractiveCanvas from "./InteractiveCanvas";
import StaticCanvas from "./StaticCanvas";
export { InteractiveCanvas, StaticCanvas };

Some files were not shown because too many files have changed in this diff Show More