mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-04-14 16:40:58 -04:00
Merge branch 'master' into aakansha-create-text-containers-programmatically
# Conflicts: # src/components/App.tsx
This commit is contained in:
commit
50fce7e5f2
152 changed files with 8967 additions and 10549 deletions
|
@ -1,30 +1,39 @@
|
||||||
REACT_APP_BACKEND_V2_GET_URL=https://json-dev.excalidraw.com/api/v2/
|
VITE_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_BACKEND_V2_POST_URL=https://json-dev.excalidraw.com/api/v2/post/
|
||||||
|
|
||||||
REACT_APP_LIBRARY_URL=https://libraries.excalidraw.com
|
VITE_APP_LIBRARY_URL=https://libraries.excalidraw.com
|
||||||
REACT_APP_LIBRARY_BACKEND=https://us-central1-excalidraw-room-persistence.cloudfunctions.net/libraries
|
VITE_APP_LIBRARY_BACKEND=https://us-central1-excalidraw-room-persistence.cloudfunctions.net/libraries
|
||||||
|
|
||||||
# collaboration WebSocket server (https://github.com/excalidraw/excalidraw-room)
|
# collaboration WebSocket server (https://github.com/excalidraw/excalidraw-room)
|
||||||
REACT_APP_WS_SERVER_URL=http://localhost:3002
|
VITE_APP_WS_SERVER_URL=http://localhost:3002
|
||||||
|
|
||||||
# set this only if using the collaboration workflow we use on excalidraw.com
|
# set this only if using the collaboration workflow we use on excalidraw.com
|
||||||
REACT_APP_PORTAL_URL=
|
VITE_APP_PORTAL_URL=
|
||||||
|
|
||||||
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"}'
|
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"}'
|
||||||
|
|
||||||
# put these in your .env.local, or make sure you don't commit!
|
# put these in your .env.local, or make sure you don't commit!
|
||||||
# must be lowercase `true` when turned on
|
# must be lowercase `true` when turned on
|
||||||
#
|
#
|
||||||
# whether to enable Service Workers in development
|
# whether to enable Service Workers in development
|
||||||
REACT_APP_DEV_ENABLE_SW=
|
VITE_APP_DEV_ENABLE_SW=
|
||||||
# whether to disable live reload / HMR. Usuaully what you want to do when
|
# whether to disable live reload / HMR. Usuaully what you want to do when
|
||||||
# debugging Service Workers.
|
# debugging Service Workers.
|
||||||
REACT_APP_DEV_DISABLE_LIVE_RELOAD=
|
VITE_APP_DEV_DISABLE_LIVE_RELOAD=
|
||||||
REACT_APP_DISABLE_TRACKING=true
|
VITE_APP_DISABLE_TRACKING=true
|
||||||
|
|
||||||
FAST_REFRESH=false
|
FAST_REFRESH=false
|
||||||
|
|
||||||
|
# The port the run the dev server
|
||||||
|
VITE_APP_PORT=3000
|
||||||
|
|
||||||
#Debug flags
|
#Debug flags
|
||||||
|
|
||||||
# To enable bounding box for text containers
|
# To enable bounding box for text containers
|
||||||
REACT_APP_DEBUG_ENABLE_TEXT_CONTAINER_BOUNDING_BOX=
|
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
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
REACT_APP_BACKEND_V2_GET_URL=https://json.excalidraw.com/api/v2/
|
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/
|
REACT_APP_BACKEND_V2_POST_URL=https://json.excalidraw.com/api/v2/post/
|
||||||
|
|
||||||
REACT_APP_LIBRARY_URL=https://libraries.excalidraw.com
|
VITE_APP_LIBRARY_URL=https://libraries.excalidraw.com
|
||||||
REACT_APP_LIBRARY_BACKEND=https://us-central1-excalidraw-room-persistence.cloudfunctions.net/libraries
|
VITE_APP_LIBRARY_BACKEND=https://us-central1-excalidraw-room-persistence.cloudfunctions.net/libraries
|
||||||
|
|
||||||
REACT_APP_PORTAL_URL=https://portal.excalidraw.com
|
VITE_APP_PORTAL_URL=https://portal.excalidraw.com
|
||||||
# Fill to set socket server URL used for collaboration.
|
# Fill to set socket server URL used for collaboration.
|
||||||
# Meant for forks only: excalidraw.com uses custom REACT_APP_PORTAL_URL flow
|
# Meant for forks only: excalidraw.com uses custom VITE_APP_PORTAL_URL flow
|
||||||
REACT_APP_WS_SERVER_URL=
|
VITE_APP_WS_SERVER_URL=
|
||||||
|
|
||||||
REACT_APP_FIREBASE_CONFIG='{"apiKey":"AIzaSyAd15pYlMci_xIp9ko6wkEsDzAAA0Dn0RU","authDomain":"excalidraw-room-persistence.firebaseapp.com","databaseURL":"https://excalidraw-room-persistence.firebaseio.com","projectId":"excalidraw-room-persistence","storageBucket":"excalidraw-room-persistence.appspot.com","messagingSenderId":"654800341332","appId":"1:654800341332:web:4a692de832b55bd57ce0c1"}'
|
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_PLUS_APP=https://app.excalidraw.com
|
VITE_APP_PLUS_APP=https://app.excalidraw.com
|
||||||
REACT_APP_DISABLE_TRACKING=
|
VITE_APP_DISABLE_TRACKING=
|
||||||
|
|
4
.github/workflows/autorelease-excalidraw.yml
vendored
4
.github/workflows/autorelease-excalidraw.yml
vendored
|
@ -12,10 +12,10 @@ jobs:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
with:
|
with:
|
||||||
fetch-depth: 2
|
fetch-depth: 2
|
||||||
- name: Setup Node.js 14.x
|
- name: Setup Node.js 18.x
|
||||||
uses: actions/setup-node@v2
|
uses: actions/setup-node@v2
|
||||||
with:
|
with:
|
||||||
node-version: 14.x
|
node-version: 18.x
|
||||||
- name: Set up publish access
|
- name: Set up publish access
|
||||||
run: |
|
run: |
|
||||||
npm config set //registry.npmjs.org/:_authToken ${NPM_TOKEN}
|
npm config set //registry.npmjs.org/:_authToken ${NPM_TOKEN}
|
||||||
|
|
4
.github/workflows/autorelease-preview.yml
vendored
4
.github/workflows/autorelease-preview.yml
vendored
|
@ -32,10 +32,10 @@ jobs:
|
||||||
with:
|
with:
|
||||||
ref: ${{ steps.sha.outputs.result }}
|
ref: ${{ steps.sha.outputs.result }}
|
||||||
fetch-depth: 2
|
fetch-depth: 2
|
||||||
- name: Setup Node.js 14.x
|
- name: Setup Node.js 18.x
|
||||||
uses: actions/setup-node@v2
|
uses: actions/setup-node@v2
|
||||||
with:
|
with:
|
||||||
node-version: 14.x
|
node-version: 18.x
|
||||||
- name: Set up publish access
|
- name: Set up publish access
|
||||||
run: |
|
run: |
|
||||||
npm config set //registry.npmjs.org/:_authToken ${NPM_TOKEN}
|
npm config set //registry.npmjs.org/:_authToken ${NPM_TOKEN}
|
||||||
|
|
4
.github/workflows/lint.yml
vendored
4
.github/workflows/lint.yml
vendored
|
@ -9,10 +9,10 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
- name: Setup Node.js 14.x
|
- name: Setup Node.js 18.x
|
||||||
uses: actions/setup-node@v2
|
uses: actions/setup-node@v2
|
||||||
with:
|
with:
|
||||||
node-version: 14.x
|
node-version: 18.x
|
||||||
|
|
||||||
- name: Install and lint
|
- name: Install and lint
|
||||||
run: |
|
run: |
|
||||||
|
|
4
.github/workflows/locales-coverage.yml
vendored
4
.github/workflows/locales-coverage.yml
vendored
|
@ -14,10 +14,10 @@ jobs:
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.PUSH_TRANSLATIONS_COVERAGE_PAT }}
|
token: ${{ secrets.PUSH_TRANSLATIONS_COVERAGE_PAT }}
|
||||||
|
|
||||||
- name: Setup Node.js 14.x
|
- name: Setup Node.js 18.x
|
||||||
uses: actions/setup-node@v2
|
uses: actions/setup-node@v2
|
||||||
with:
|
with:
|
||||||
node-version: 14.x
|
node-version: 18.x
|
||||||
|
|
||||||
- name: Create report file
|
- name: Create report file
|
||||||
run: |
|
run: |
|
||||||
|
|
2
.github/workflows/semantic-pr-title.yml
vendored
2
.github/workflows/semantic-pr-title.yml
vendored
|
@ -1,7 +1,7 @@
|
||||||
name: Semantic PR title
|
name: Semantic PR title
|
||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request_target:
|
pull_request:
|
||||||
types:
|
types:
|
||||||
- opened
|
- opened
|
||||||
- edited
|
- edited
|
||||||
|
|
4
.github/workflows/sentry-production.yml
vendored
4
.github/workflows/sentry-production.yml
vendored
|
@ -10,10 +10,10 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- name: Setup Node.js 14.x
|
- name: Setup Node.js 18.x
|
||||||
uses: actions/setup-node@v2
|
uses: actions/setup-node@v2
|
||||||
with:
|
with:
|
||||||
node-version: 14.x
|
node-version: 18.x
|
||||||
- name: Install and build
|
- name: Install and build
|
||||||
run: |
|
run: |
|
||||||
yarn --frozen-lockfile
|
yarn --frozen-lockfile
|
||||||
|
|
4
.github/workflows/test.yml
vendored
4
.github/workflows/test.yml
vendored
|
@ -7,10 +7,10 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- name: Setup Node.js 14.x
|
- name: Setup Node.js 18.x
|
||||||
uses: actions/setup-node@v2
|
uses: actions/setup-node@v2
|
||||||
with:
|
with:
|
||||||
node-version: 14.x
|
node-version: 18.x
|
||||||
- name: Install and test
|
- name: Install and test
|
||||||
run: |
|
run: |
|
||||||
yarn --frozen-lockfile
|
yarn --frozen-lockfile
|
||||||
|
|
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -26,3 +26,5 @@ src/packages/excalidraw/example/public/bundle.js
|
||||||
src/packages/excalidraw/example/public/excalidraw-assets-dev
|
src/packages/excalidraw/example/public/excalidraw-assets-dev
|
||||||
src/packages/excalidraw/example/public/excalidraw.development.js
|
src/packages/excalidraw/example/public/excalidraw.development.js
|
||||||
coverage
|
coverage
|
||||||
|
dev-dist
|
||||||
|
html
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
FROM node:14-alpine AS build
|
FROM node:18 AS build
|
||||||
|
|
||||||
WORKDIR /opt/node_app
|
WORKDIR /opt/node_app
|
||||||
|
|
||||||
|
|
|
@ -29,6 +29,8 @@ All `props` are *optional*.
|
||||||
| [`handleKeyboardGlobally`](#handlekeyboardglobally) | `boolean` | `false` | Indicates whether to bind the keyboard events to document. |
|
| [`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 |
|
| [`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 |
|
| [`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>` |
|
||||||
|
|
||||||
### Storing custom data on Excalidraw elements
|
### Storing custom data on Excalidraw elements
|
||||||
|
|
||||||
|
@ -215,7 +217,6 @@ 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).
|
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
|
### autoFocus
|
||||||
|
|
||||||
This prop indicates whether to `focus` the Excalidraw component on page load. Defaults to false.
|
This prop indicates whether to `focus` the Excalidraw component on page load. Defaults to false.
|
||||||
|
@ -228,3 +229,12 @@ Allows you to override `id` generation for files added on canvas (images). By de
|
||||||
(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.
|
|
@ -121,3 +121,16 @@ function App() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## renderEmbeddable
|
||||||
|
|
||||||
|
<pre>
|
||||||
|
(element: NonDeleted<ExcalidrawEmbeddableElement>, 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. |
|
||||||
|
|
|
@ -78,8 +78,7 @@
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<!------------------------------------------------------------------------->
|
<!------------------------------------------------------------------------->
|
||||||
|
<% if ("%PROD%" === "true") { %>
|
||||||
<% if (process.env.NODE_ENV === "production") { %>
|
|
||||||
<script>
|
<script>
|
||||||
// Redirect Excalidraw+ users which have auto-redirect enabled.
|
// Redirect Excalidraw+ users which have auto-redirect enabled.
|
||||||
//
|
//
|
||||||
|
@ -100,41 +99,35 @@
|
||||||
</script>
|
</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 -->
|
<!-- Excalidraw version -->
|
||||||
<meta name="version" content="{version}" />
|
<meta name="version" content="{version}" />
|
||||||
|
|
||||||
<link
|
<link
|
||||||
rel="preload"
|
rel="preload"
|
||||||
href="Virgil.woff2"
|
href="/Virgil.woff2"
|
||||||
as="font"
|
as="font"
|
||||||
type="font/woff2"
|
type="font/woff2"
|
||||||
crossorigin="anonymous"
|
crossorigin="anonymous"
|
||||||
/>
|
/>
|
||||||
<link
|
<link
|
||||||
rel="preload"
|
rel="preload"
|
||||||
href="Cascadia.woff2"
|
href="/Cascadia.woff2"
|
||||||
as="font"
|
as="font"
|
||||||
type="font/woff2"
|
type="font/woff2"
|
||||||
crossorigin="anonymous"
|
crossorigin="anonymous"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<link
|
<link rel="stylesheet" href="/fonts.css" type="text/css" />
|
||||||
rel="manifest"
|
<% if ("%VITE_APP_DEV_DISABLE_LIVE_RELOAD%"==="true" ) { %>
|
||||||
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>
|
<script>
|
||||||
{
|
{
|
||||||
const _WebSocket = window.WebSocket;
|
const _WebSocket = window.WebSocket;
|
||||||
window.WebSocket = function (url) {
|
window.WebSocket = function (url) {
|
||||||
if (/ws:\/\/localhost:.+?\/sockjs-node/.test(url)) {
|
if (/ws:\/\/localhost:.+?\/sockjs-node/.test(url)) {
|
||||||
console.info(
|
console.info(
|
||||||
"[!!!] Live reload is disabled via process.env.REACT_APP_DEV_DISABLE_LIVE_RELOAD [!!!]",
|
"[!!!] Live reload is disabled via VITE_APP_DEV_DISABLE_LIVE_RELOAD [!!!]",
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return new _WebSocket(url);
|
return new _WebSocket(url);
|
||||||
|
@ -200,7 +193,8 @@
|
||||||
<h1 class="visually-hidden">Excalidraw</h1>
|
<h1 class="visually-hidden">Excalidraw</h1>
|
||||||
</header>
|
</header>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
<% if (process.env.REACT_APP_DISABLE_TRACKING !== 'true') { %>
|
<script type="module" src="/src/index.tsx"></script>
|
||||||
|
<% if ("%VITE_APP_DEV_DISABLE_LIVE_RELOAD%" !== 'true') { %>
|
||||||
<!-- 100% privacy friendly analytics -->
|
<!-- 100% privacy friendly analytics -->
|
||||||
<script>
|
<script>
|
||||||
// need to load this script dynamically bcs. of iframe embed tracking
|
// need to load this script dynamically bcs. of iframe embed tracking
|
67
package.json
67
package.json
|
@ -32,6 +32,7 @@
|
||||||
"canvas-roundrect-polyfill": "0.0.1",
|
"canvas-roundrect-polyfill": "0.0.1",
|
||||||
"clsx": "1.1.1",
|
"clsx": "1.1.1",
|
||||||
"cross-env": "7.0.3",
|
"cross-env": "7.0.3",
|
||||||
|
"eslint-plugin-react": "7.32.2",
|
||||||
"fake-indexeddb": "3.1.7",
|
"fake-indexeddb": "3.1.7",
|
||||||
"firebase": "8.3.3",
|
"firebase": "8.3.3",
|
||||||
"i18next-browser-languagedetector": "6.1.4",
|
"i18next-browser-languagedetector": "6.1.4",
|
||||||
|
@ -51,28 +52,14 @@
|
||||||
"pwacompat": "2.0.17",
|
"pwacompat": "2.0.17",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
"react-scripts": "5.0.1",
|
|
||||||
"roughjs": "4.5.2",
|
"roughjs": "4.5.2",
|
||||||
"sass": "1.51.0",
|
"sass": "1.51.0",
|
||||||
"socket.io-client": "2.3.1",
|
"socket.io-client": "2.3.1",
|
||||||
"tunnel-rat": "0.1.2",
|
"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": {
|
"devDependencies": {
|
||||||
"@excalidraw/eslint-config": "1.0.0",
|
"@excalidraw/eslint-config": "1.0.3",
|
||||||
"@excalidraw/prettier-config": "1.0.2",
|
"@excalidraw/prettier-config": "1.0.2",
|
||||||
"@size-limit/preset-big-lib": "8.2.4",
|
|
||||||
"@types/chai": "4.3.0",
|
"@types/chai": "4.3.0",
|
||||||
"@types/jest": "27.4.0",
|
"@types/jest": "27.4.0",
|
||||||
"@types/lodash.throttle": "4.1.7",
|
"@types/lodash.throttle": "4.1.7",
|
||||||
|
@ -82,49 +69,42 @@
|
||||||
"@types/react-dom": "18.0.6",
|
"@types/react-dom": "18.0.6",
|
||||||
"@types/resize-observer-browser": "0.1.7",
|
"@types/resize-observer-browser": "0.1.7",
|
||||||
"@types/socket.io-client": "1.4.36",
|
"@types/socket.io-client": "1.4.36",
|
||||||
|
"@vitejs/plugin-react": "3.1.0",
|
||||||
|
"@vitest/ui": "0.32.2",
|
||||||
"chai": "4.3.6",
|
"chai": "4.3.6",
|
||||||
"dotenv": "16.0.1",
|
"dotenv": "16.0.1",
|
||||||
"eslint-config-prettier": "8.5.0",
|
"eslint-config-prettier": "8.5.0",
|
||||||
|
"eslint-config-react-app": "7.0.1",
|
||||||
"eslint-plugin-prettier": "3.3.1",
|
"eslint-plugin-prettier": "3.3.1",
|
||||||
"http-server": "14.1.1",
|
"http-server": "14.1.1",
|
||||||
"husky": "7.0.4",
|
"husky": "7.0.4",
|
||||||
"jest-canvas-mock": "2.4.0",
|
"jsdom": "22.1.0",
|
||||||
"lint-staged": "12.3.7",
|
"lint-staged": "12.3.7",
|
||||||
"pepjs": "0.5.3",
|
"pepjs": "0.5.3",
|
||||||
"prettier": "2.6.2",
|
"prettier": "2.6.2",
|
||||||
"rewire": "6.0.0",
|
"rewire": "6.0.0",
|
||||||
"size-limit": "8.2.4",
|
"typescript": "4.9.4",
|
||||||
"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.32.2",
|
||||||
|
"vitest-canvas-mock": "0.3.2"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14.0.0"
|
"node": ">=18.0.0"
|
||||||
},
|
},
|
||||||
"homepage": ".",
|
"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",
|
"name": "excalidraw",
|
||||||
"prettier": "@excalidraw/prettier-config",
|
"prettier": "@excalidraw/prettier-config",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build-node": "node ./scripts/build-node.js",
|
"build-node": "node ./scripts/build-node.js",
|
||||||
"build:app:docker": "cross-env REACT_APP_DISABLE_SENTRY=true REACT_APP_DISABLE_TRACKING=true react-scripts build",
|
"build:app:docker": "cross-env REACT_APP_DISABLE_SENTRY=true REACT_APP_DISABLE_TRACKING=true vite build",
|
||||||
"build:app": "cross-env REACT_APP_GIT_SHA=$VERCEL_GIT_COMMIT_SHA react-scripts build",
|
"build:app": "cross-env REACT_APP_GIT_SHA=$VERCEL_GIT_COMMIT_SHA vite build",
|
||||||
"build:version": "node ./scripts/build-version.js",
|
"build:version": "node ./scripts/build-version.js",
|
||||||
"build": "yarn build:app && yarn build:version",
|
"build": "yarn build:app && yarn build:version",
|
||||||
"eject": "react-scripts eject",
|
|
||||||
"fix:code": "yarn test:code --fix",
|
"fix:code": "yarn test:code --fix",
|
||||||
"fix:other": "yarn prettier --write",
|
"fix:other": "yarn prettier --write",
|
||||||
"fix": "yarn fix:other && yarn fix:code",
|
"fix": "yarn fix:other && yarn fix:code",
|
||||||
|
@ -132,19 +112,20 @@
|
||||||
"locales-coverage:description": "node scripts/locales-coverage-description.js",
|
"locales-coverage:description": "node scripts/locales-coverage-description.js",
|
||||||
"prepare": "husky install",
|
"prepare": "husky install",
|
||||||
"prettier": "prettier \"**/*.{css,scss,json,md,html,yml}\" --ignore-path=.eslintignore",
|
"prettier": "prettier \"**/*.{css,scss,json,md,html,yml}\" --ignore-path=.eslintignore",
|
||||||
"start": "react-scripts start",
|
"start": "vite",
|
||||||
"start:production": "npm run build && npx http-server build -a localhost -p 5001 -o",
|
"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 --watchAll=false",
|
"test:all": "yarn test:typecheck && yarn test:code && yarn test:other && yarn test:app --watchAll=false",
|
||||||
"test:app": "react-scripts test --passWithNoTests",
|
"test:app": "vitest --config vitest.config.ts",
|
||||||
"test:code": "eslint --max-warnings=0 --ext .js,.ts,.tsx .",
|
"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:other": "yarn prettier --list-different",
|
||||||
"test:typecheck": "tsc",
|
"test:typecheck": "tsc",
|
||||||
"test:update": "yarn test:app --updateSnapshot --watchAll=false",
|
"test:update": "yarn test:app --update --watch=false",
|
||||||
"test": "yarn test:app",
|
"test": "yarn test:app",
|
||||||
"test:coverage": "react-scripts test --passWithNoTests --coverage --watchAll",
|
"test:coverage": "vitest --coverage --watchAll",
|
||||||
|
"test:ui": "yarn test --ui",
|
||||||
"autorelease": "node scripts/autorelease.js",
|
"autorelease": "node scripts/autorelease.js",
|
||||||
"prerelease": "node scripts/prerelease.js",
|
"prerelease": "node scripts/prerelease.js",
|
||||||
|
"build:preview": "yarn build && vite preview --port 5000",
|
||||||
"release": "node scripts/release.js"
|
"release": "node scripts/release.js"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,2 +0,0 @@
|
||||||
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
|
|
|
@ -1,2 +0,0 @@
|
||||||
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
|
|
|
@ -1,2 +0,0 @@
|
||||||
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
|
@ -1,2 +0,0 @@
|
||||||
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
|
|
|
@ -1,2 +0,0 @@
|
||||||
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
|
|
|
@ -1,2 +0,0 @@
|
||||||
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
|
|
|
@ -1,2 +0,0 @@
|
||||||
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
|
|
|
@ -1,2 +0,0 @@
|
||||||
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
|
|
|
@ -1,2 +0,0 @@
|
||||||
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
|
|
|
@ -1,2 +0,0 @@
|
||||||
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
|
|
|
@ -1,2 +0,0 @@
|
||||||
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
|
|
|
@ -1,2 +0,0 @@
|
||||||
!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
|
|
|
@ -1,2 +0,0 @@
|
||||||
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
|
|
|
@ -1,2 +0,0 @@
|
||||||
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
|
|
|
@ -1,2 +0,0 @@
|
||||||
!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
|
|
|
@ -2,6 +2,7 @@ import { register } from "./register";
|
||||||
import { deepCopyElement } from "../element/newElement";
|
import { deepCopyElement } from "../element/newElement";
|
||||||
import { randomId } from "../random";
|
import { randomId } from "../random";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
|
import { LIBRARY_DISABLED_TYPES } from "../constants";
|
||||||
|
|
||||||
export const actionAddToLibrary = register({
|
export const actionAddToLibrary = register({
|
||||||
name: "addToLibrary",
|
name: "addToLibrary",
|
||||||
|
@ -12,14 +13,17 @@ export const actionAddToLibrary = register({
|
||||||
includeBoundTextElement: true,
|
includeBoundTextElement: true,
|
||||||
includeElementsInFrames: true,
|
includeElementsInFrames: true,
|
||||||
});
|
});
|
||||||
if (selectedElements.some((element) => element.type === "image")) {
|
|
||||||
return {
|
for (const type of LIBRARY_DISABLED_TYPES) {
|
||||||
commitToHistory: false,
|
if (selectedElements.some((element) => element.type === type)) {
|
||||||
appState: {
|
return {
|
||||||
...appState,
|
commitToHistory: false,
|
||||||
errorMessage: "Support for adding images to the library coming soon!",
|
appState: {
|
||||||
},
|
...appState,
|
||||||
};
|
errorMessage: t(`errors.libraryElementTypeError.${type}`),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return app.library
|
return app.library
|
||||||
|
|
|
@ -396,6 +396,7 @@ export const actionToggleEraserTool = register({
|
||||||
...appState,
|
...appState,
|
||||||
selectedElementIds: {},
|
selectedElementIds: {},
|
||||||
selectedGroupIds: {},
|
selectedGroupIds: {},
|
||||||
|
activeEmbeddable: null,
|
||||||
activeTool,
|
activeTool,
|
||||||
},
|
},
|
||||||
commitToHistory: true,
|
commitToHistory: true,
|
||||||
|
@ -430,6 +431,7 @@ export const actionToggleHandTool = register({
|
||||||
...appState,
|
...appState,
|
||||||
selectedElementIds: {},
|
selectedElementIds: {},
|
||||||
selectedGroupIds: {},
|
selectedGroupIds: {},
|
||||||
|
activeEmbeddable: null,
|
||||||
activeTool,
|
activeTool,
|
||||||
},
|
},
|
||||||
commitToHistory: true,
|
commitToHistory: true,
|
||||||
|
|
|
@ -158,6 +158,7 @@ export const actionDeleteSelected = register({
|
||||||
...nextAppState,
|
...nextAppState,
|
||||||
activeTool: updateActiveTool(appState, { type: "selection" }),
|
activeTool: updateActiveTool(appState, { type: "selection" }),
|
||||||
multiElement: null,
|
multiElement: null,
|
||||||
|
activeEmbeddable: null,
|
||||||
},
|
},
|
||||||
commitToHistory: isSomeElementSelected(
|
commitToHistory: isSomeElementSelected(
|
||||||
getNonDeletedElements(elements),
|
getNonDeletedElements(elements),
|
||||||
|
|
|
@ -65,7 +65,7 @@ export const actionChangeExportScale = register({
|
||||||
);
|
);
|
||||||
|
|
||||||
const scaleButtonTitle = `${t(
|
const scaleButtonTitle = `${t(
|
||||||
"buttons.scale",
|
"imageExportDialog.label.scale",
|
||||||
)} ${s}x (${width}x${height})`;
|
)} ${s}x (${width}x${height})`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -102,7 +102,7 @@ export const actionChangeExportBackground = register({
|
||||||
checked={appState.exportBackground}
|
checked={appState.exportBackground}
|
||||||
onChange={(checked) => updateData(checked)}
|
onChange={(checked) => updateData(checked)}
|
||||||
>
|
>
|
||||||
{t("labels.withBackground")}
|
{t("imageExportDialog.label.withBackground")}
|
||||||
</CheckboxItem>
|
</CheckboxItem>
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
@ -121,8 +121,8 @@ export const actionChangeExportEmbedScene = register({
|
||||||
checked={appState.exportEmbedScene}
|
checked={appState.exportEmbedScene}
|
||||||
onChange={(checked) => updateData(checked)}
|
onChange={(checked) => updateData(checked)}
|
||||||
>
|
>
|
||||||
{t("labels.exportEmbedScene")}
|
{t("imageExportDialog.label.embedScene")}
|
||||||
<Tooltip label={t("labels.exportEmbedScene_details")} long={true}>
|
<Tooltip label={t("imageExportDialog.tooltip.embedScene")} long={true}>
|
||||||
<div className="excalidraw-tooltip-icon">{questionCircle}</div>
|
<div className="excalidraw-tooltip-icon">{questionCircle}</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</CheckboxItem>
|
</CheckboxItem>
|
||||||
|
@ -277,7 +277,7 @@ export const actionExportWithDarkMode = register({
|
||||||
onChange={(theme: Theme) => {
|
onChange={(theme: Theme) => {
|
||||||
updateData(theme === THEME.DARK);
|
updateData(theme === THEME.DARK);
|
||||||
}}
|
}}
|
||||||
title={t("labels.toggleExportColorScheme")}
|
title={t("imageExportDialog.label.darkMode")}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
|
|
|
@ -160,6 +160,7 @@ export const actionFinalize = register({
|
||||||
multiPointElement
|
multiPointElement
|
||||||
? appState.activeTool
|
? appState.activeTool
|
||||||
: activeTool,
|
: activeTool,
|
||||||
|
activeEmbeddable: null,
|
||||||
draggingElement: null,
|
draggingElement: null,
|
||||||
multiElement: null,
|
multiElement: null,
|
||||||
editingElement: null,
|
editingElement: null,
|
||||||
|
|
|
@ -121,6 +121,7 @@ export type ActionName =
|
||||||
| "removeAllElementsFromFrame"
|
| "removeAllElementsFromFrame"
|
||||||
| "updateFrameRendering"
|
| "updateFrameRendering"
|
||||||
| "setFrameAsActiveTool"
|
| "setFrameAsActiveTool"
|
||||||
|
| "setEmbeddableAsActiveTool"
|
||||||
| "createContainerFromText"
|
| "createContainerFromText"
|
||||||
| "wrapTextInContainer";
|
| "wrapTextInContainer";
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,7 @@ export const trackEvent = (
|
||||||
// Uncomment the next line to track locally
|
// Uncomment the next line to track locally
|
||||||
// console.log("Track Event", { category, action, label, value });
|
// console.log("Track Event", { category, action, label, value });
|
||||||
|
|
||||||
if (typeof window === "undefined" || process.env.JEST_WORKER_ID) {
|
if (typeof window === "undefined" || import.meta.env.VITE_WORKER_ID) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -38,6 +38,7 @@ export const getDefaultAppState = (): Omit<
|
||||||
currentItemStrokeWidth: DEFAULT_ELEMENT_PROPS.strokeWidth,
|
currentItemStrokeWidth: DEFAULT_ELEMENT_PROPS.strokeWidth,
|
||||||
currentItemTextAlign: DEFAULT_TEXT_ALIGN,
|
currentItemTextAlign: DEFAULT_TEXT_ALIGN,
|
||||||
cursorButton: "up",
|
cursorButton: "up",
|
||||||
|
activeEmbeddable: null,
|
||||||
draggingElement: null,
|
draggingElement: null,
|
||||||
editingElement: null,
|
editingElement: null,
|
||||||
editingGroupId: null,
|
editingGroupId: null,
|
||||||
|
@ -139,6 +140,7 @@ const APP_STATE_STORAGE_CONF = (<
|
||||||
currentItemStrokeWidth: { browser: true, export: false, server: false },
|
currentItemStrokeWidth: { browser: true, export: false, server: false },
|
||||||
currentItemTextAlign: { browser: true, export: false, server: false },
|
currentItemTextAlign: { browser: true, export: false, server: false },
|
||||||
cursorButton: { 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 },
|
draggingElement: { browser: false, export: false, server: false },
|
||||||
editingElement: { browser: false, export: false, server: false },
|
editingElement: { browser: false, export: false, server: false },
|
||||||
editingGroupId: { browser: true, export: false, server: false },
|
editingGroupId: { browser: true, export: false, server: false },
|
||||||
|
|
|
@ -6,7 +6,6 @@ import {
|
||||||
import {
|
import {
|
||||||
DEFAULT_FONT_FAMILY,
|
DEFAULT_FONT_FAMILY,
|
||||||
DEFAULT_FONT_SIZE,
|
DEFAULT_FONT_SIZE,
|
||||||
ENV,
|
|
||||||
VERTICAL_ALIGN,
|
VERTICAL_ALIGN,
|
||||||
} from "./constants";
|
} from "./constants";
|
||||||
import { newElement, newLinearElement, newTextElement } from "./element";
|
import { newElement, newLinearElement, newTextElement } from "./element";
|
||||||
|
@ -384,7 +383,7 @@ const chartTypeBar = (
|
||||||
y,
|
y,
|
||||||
groupId,
|
groupId,
|
||||||
backgroundColor,
|
backgroundColor,
|
||||||
process.env.NODE_ENV === ENV.DEVELOPMENT,
|
import.meta.env.DEV,
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
@ -473,7 +472,7 @@ const chartTypeLine = (
|
||||||
y,
|
y,
|
||||||
groupId,
|
groupId,
|
||||||
backgroundColor,
|
backgroundColor,
|
||||||
process.env.NODE_ENV === ENV.DEVELOPMENT,
|
import.meta.env.DEV,
|
||||||
),
|
),
|
||||||
line,
|
line,
|
||||||
...lines,
|
...lines,
|
||||||
|
|
|
@ -21,7 +21,7 @@ export type ColorPickerColor =
|
||||||
export type ColorTuple = readonly [string, string, string, string, string];
|
export type ColorTuple = readonly [string, string, string, string, string];
|
||||||
export type ColorPalette = Merge<
|
export type ColorPalette = Merge<
|
||||||
Record<ColorPickerColor, ColorTuple>,
|
Record<ColorPickerColor, ColorTuple>,
|
||||||
{ black: string; white: string; transparent: string }
|
{ black: "#1e1e1e"; white: "#ffffff"; transparent: "transparent" }
|
||||||
>;
|
>;
|
||||||
|
|
||||||
// used general type instead of specific type (ColorPalette) to support custom colors
|
// used general type instead of specific type (ColorPalette) to support custom colors
|
||||||
|
|
|
@ -36,7 +36,7 @@ import {
|
||||||
|
|
||||||
import "./Actions.scss";
|
import "./Actions.scss";
|
||||||
import DropdownMenu from "./dropdownMenu/DropdownMenu";
|
import DropdownMenu from "./dropdownMenu/DropdownMenu";
|
||||||
import { extraToolsIcon, frameToolIcon } from "./icons";
|
import { EmbedIcon, extraToolsIcon, frameToolIcon } from "./icons";
|
||||||
import { KEYS } from "../keys";
|
import { KEYS } from "../keys";
|
||||||
|
|
||||||
export const SelectedShapeActions = ({
|
export const SelectedShapeActions = ({
|
||||||
|
@ -266,6 +266,7 @@ export const ShapesSwitcher = ({
|
||||||
});
|
});
|
||||||
setAppState({
|
setAppState({
|
||||||
activeTool: nextActiveTool,
|
activeTool: nextActiveTool,
|
||||||
|
activeEmbeddable: null,
|
||||||
multiElement: null,
|
multiElement: null,
|
||||||
selectedElementIds: {},
|
selectedElementIds: {},
|
||||||
});
|
});
|
||||||
|
@ -283,39 +284,72 @@ export const ShapesSwitcher = ({
|
||||||
<div className="App-toolbar__divider" />
|
<div className="App-toolbar__divider" />
|
||||||
{/* TEMP HACK because dropdown doesn't work well inside mobile toolbar */}
|
{/* TEMP HACK because dropdown doesn't work well inside mobile toolbar */}
|
||||||
{device.isMobile ? (
|
{device.isMobile ? (
|
||||||
<ToolButton
|
<>
|
||||||
className={clsx("Shape", { fillable: false })}
|
<ToolButton
|
||||||
type="radio"
|
className={clsx("Shape", { fillable: false })}
|
||||||
icon={frameToolIcon}
|
type="radio"
|
||||||
checked={activeTool.type === "frame"}
|
icon={frameToolIcon}
|
||||||
name="editor-current-shape"
|
checked={activeTool.type === "frame"}
|
||||||
title={`${capitalizeString(
|
name="editor-current-shape"
|
||||||
t("toolBar.frame"),
|
title={`${capitalizeString(
|
||||||
)} — ${KEYS.F.toLocaleUpperCase()}`}
|
t("toolBar.frame"),
|
||||||
keyBindingLabel={KEYS.F.toLocaleUpperCase()}
|
)} — ${KEYS.F.toLocaleUpperCase()}`}
|
||||||
aria-label={capitalizeString(t("toolBar.frame"))}
|
keyBindingLabel={KEYS.F.toLocaleUpperCase()}
|
||||||
aria-keyshortcuts={KEYS.F.toLocaleUpperCase()}
|
aria-label={capitalizeString(t("toolBar.frame"))}
|
||||||
data-testid={`toolbar-frame`}
|
aria-keyshortcuts={KEYS.F.toLocaleUpperCase()}
|
||||||
onPointerDown={({ pointerType }) => {
|
data-testid={`toolbar-frame`}
|
||||||
if (!appState.penDetected && pointerType === "pen") {
|
onPointerDown={({ pointerType }) => {
|
||||||
setAppState({
|
if (!appState.penDetected && pointerType === "pen") {
|
||||||
penDetected: true,
|
setAppState({
|
||||||
penMode: true,
|
penDetected: true,
|
||||||
|
penMode: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onChange={({ pointerType }) => {
|
||||||
|
trackEvent("toolbar", "frame", "ui");
|
||||||
|
const nextActiveTool = updateActiveTool(appState, {
|
||||||
|
type: "frame",
|
||||||
});
|
});
|
||||||
}
|
setAppState({
|
||||||
}}
|
activeTool: nextActiveTool,
|
||||||
onChange={({ pointerType }) => {
|
multiElement: null,
|
||||||
trackEvent("toolbar", "frame", "ui");
|
selectedElementIds: {},
|
||||||
const nextActiveTool = updateActiveTool(appState, {
|
activeEmbeddable: null,
|
||||||
type: "frame",
|
});
|
||||||
});
|
}}
|
||||||
setAppState({
|
/>
|
||||||
activeTool: nextActiveTool,
|
<ToolButton
|
||||||
multiElement: null,
|
className={clsx("Shape", { fillable: false })}
|
||||||
selectedElementIds: {},
|
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,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
) : (
|
) : (
|
||||||
<DropdownMenu open={isExtraToolsMenuOpen}>
|
<DropdownMenu open={isExtraToolsMenuOpen}>
|
||||||
<DropdownMenu.Trigger
|
<DropdownMenu.Trigger
|
||||||
|
@ -347,6 +381,22 @@ export const ShapesSwitcher = ({
|
||||||
>
|
>
|
||||||
{t("toolBar.frame")}
|
{t("toolBar.frame")}
|
||||||
</DropdownMenu.Item>
|
</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.Content>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -4,8 +4,9 @@ import { reseed } from "../random";
|
||||||
import { render, queryByTestId } from "../tests/test-utils";
|
import { render, queryByTestId } from "../tests/test-utils";
|
||||||
|
|
||||||
import ExcalidrawApp from "../excalidraw-app";
|
import ExcalidrawApp from "../excalidraw-app";
|
||||||
|
import { vi } from "vitest";
|
||||||
|
|
||||||
const renderScene = jest.spyOn(Renderer, "renderScene");
|
const renderScene = vi.spyOn(Renderer, "renderScene");
|
||||||
|
|
||||||
describe("Test <App/>", () => {
|
describe("Test <App/>", () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -8,7 +8,7 @@ import {
|
||||||
} from "./colorPickerUtils";
|
} from "./colorPickerUtils";
|
||||||
import HotkeyLabel from "./HotkeyLabel";
|
import HotkeyLabel from "./HotkeyLabel";
|
||||||
import { ColorPaletteCustom } from "../../colors";
|
import { ColorPaletteCustom } from "../../colors";
|
||||||
import { t } from "../../i18n";
|
import { TranslationKeys, t } from "../../i18n";
|
||||||
|
|
||||||
interface PickerColorListProps {
|
interface PickerColorListProps {
|
||||||
palette: ColorPaletteCustom;
|
palette: ColorPaletteCustom;
|
||||||
|
@ -48,7 +48,11 @@ const PickerColorList = ({
|
||||||
(Array.isArray(value) ? value[activeShade] : value) || "transparent";
|
(Array.isArray(value) ? value[activeShade] : value) || "transparent";
|
||||||
|
|
||||||
const keybinding = colorPickerHotkeyBindings[index];
|
const keybinding = colorPickerHotkeyBindings[index];
|
||||||
const label = t(`colors.${key.replace(/\d+/, "")}`, null, "");
|
const label = t(
|
||||||
|
`colors.${key.replace(/\d+/, "")}` as unknown as TranslationKeys,
|
||||||
|
null,
|
||||||
|
"",
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { Popover } from "./Popover";
|
import { Popover } from "./Popover";
|
||||||
import { t } from "../i18n";
|
import { t, TranslationKeys } from "../i18n";
|
||||||
|
|
||||||
import "./ContextMenu.scss";
|
import "./ContextMenu.scss";
|
||||||
import {
|
import {
|
||||||
|
@ -83,10 +83,14 @@ export const ContextMenu = React.memo(
|
||||||
if (item.contextItemLabel) {
|
if (item.contextItemLabel) {
|
||||||
if (typeof item.contextItemLabel === "function") {
|
if (typeof item.contextItemLabel === "function") {
|
||||||
label = t(
|
label = t(
|
||||||
item.contextItemLabel(elements, appState, actionManager.app),
|
item.contextItemLabel(
|
||||||
|
elements,
|
||||||
|
appState,
|
||||||
|
actionManager.app,
|
||||||
|
) as unknown as TranslationKeys,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
label = t(item.contextItemLabel);
|
label = t(item.contextItemLabel as unknown as TranslationKeys);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -58,7 +58,7 @@ export const EyeDropper: React.FC<{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let currentColor = COLOR_PALETTE.black;
|
let currentColor: string = COLOR_PALETTE.black;
|
||||||
let isHoldingPointerDown = false;
|
let isHoldingPointerDown = false;
|
||||||
|
|
||||||
const ctx = app.canvas.getContext("2d")!;
|
const ctx = app.canvas.getContext("2d")!;
|
||||||
|
|
|
@ -44,6 +44,10 @@ const getHints = ({ appState, isMobile, device, app }: HintViewerProps) => {
|
||||||
return t("hints.text");
|
return t("hints.text");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (activeTool.type === "embeddable") {
|
||||||
|
return t("hints.embeddable");
|
||||||
|
}
|
||||||
|
|
||||||
if (appState.activeTool.type === "image" && appState.pendingImageElementId) {
|
if (appState.activeTool.type === "image" && appState.pendingImageElementId) {
|
||||||
return t("hints.placeImage");
|
return t("hints.placeImage");
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,6 +29,7 @@ import "./LibraryMenu.scss";
|
||||||
import { LibraryMenuControlButtons } from "./LibraryMenuControlButtons";
|
import { LibraryMenuControlButtons } from "./LibraryMenuControlButtons";
|
||||||
import { isShallowEqual } from "../utils";
|
import { isShallowEqual } from "../utils";
|
||||||
import { NonDeletedExcalidrawElement } from "../element/types";
|
import { NonDeletedExcalidrawElement } from "../element/types";
|
||||||
|
import { LIBRARY_DISABLED_TYPES } from "../constants";
|
||||||
|
|
||||||
export const isLibraryMenuOpenAtom = atom(false);
|
export const isLibraryMenuOpenAtom = atom(false);
|
||||||
|
|
||||||
|
@ -68,11 +69,12 @@ export const LibraryMenuContent = ({
|
||||||
libraryItems: LibraryItems,
|
libraryItems: LibraryItems,
|
||||||
) => {
|
) => {
|
||||||
trackEvent("element", "addToLibrary", "ui");
|
trackEvent("element", "addToLibrary", "ui");
|
||||||
if (processedElements.some((element) => element.type === "image")) {
|
for (const type of LIBRARY_DISABLED_TYPES) {
|
||||||
return setAppState({
|
if (processedElements.some((element) => element.type === type)) {
|
||||||
errorMessage:
|
return setAppState({
|
||||||
"Support for adding images to the library coming soon!",
|
errorMessage: t(`errors.libraryElementTypeError.${type}`),
|
||||||
});
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const nextItems: LibraryItems = [
|
const nextItems: LibraryItems = [
|
||||||
{
|
{
|
||||||
|
@ -197,6 +199,7 @@ export const LibraryMenu = () => {
|
||||||
setAppState({
|
setAppState({
|
||||||
selectedElementIds: {},
|
selectedElementIds: {},
|
||||||
selectedGroupIds: {},
|
selectedGroupIds: {},
|
||||||
|
activeEmbeddable: null,
|
||||||
});
|
});
|
||||||
}, [setAppState]);
|
}, [setAppState]);
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,7 @@ const LibraryMenuBrowseButton = ({
|
||||||
return (
|
return (
|
||||||
<a
|
<a
|
||||||
className="library-menu-browse-button"
|
className="library-menu-browse-button"
|
||||||
href={`${process.env.REACT_APP_LIBRARY_URL}?target=${
|
href={`${import.meta.env.VITE_APP_LIBRARY_URL}?target=${
|
||||||
window.name || "_blank"
|
window.name || "_blank"
|
||||||
}&referrer=${referrer}&useHash=true&token=${id}&theme=${theme}&version=${
|
}&referrer=${referrer}&useHash=true&token=${id}&theme=${theme}&version=${
|
||||||
VERSIONS.excalidrawLibrary
|
VERSIONS.excalidrawLibrary
|
||||||
|
|
|
@ -12,6 +12,11 @@
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
border-radius: var(--border-radius-lg);
|
border-radius: var(--border-radius-lg);
|
||||||
|
|
||||||
|
svg {
|
||||||
|
// to prevent clicks on links and such
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
&--hover {
|
&--hover {
|
||||||
border-color: var(--color-primary);
|
border-color: var(--color-primary);
|
||||||
}
|
}
|
||||||
|
|
|
@ -319,7 +319,7 @@ const PublishLibrary = ({
|
||||||
formData.append("twitterHandle", libraryData.twitterHandle);
|
formData.append("twitterHandle", libraryData.twitterHandle);
|
||||||
formData.append("website", libraryData.website);
|
formData.append("website", libraryData.website);
|
||||||
|
|
||||||
fetch(`${process.env.REACT_APP_LIBRARY_BACKEND}/submit`, {
|
fetch(`${import.meta.env.VITE_APP_LIBRARY_BACKEND}/submit`, {
|
||||||
method: "post",
|
method: "post",
|
||||||
body: formData,
|
body: formData,
|
||||||
})
|
})
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { t } from "../i18n";
|
||||||
import { useExcalidrawContainer } from "./App";
|
import { useExcalidrawContainer } from "./App";
|
||||||
|
|
||||||
export const Section: React.FC<{
|
export const Section: React.FC<{
|
||||||
heading: string;
|
heading: "canvasActions" | "selectedShapeActions" | "shapes";
|
||||||
children?: React.ReactNode | ((heading: React.ReactNode) => React.ReactNode);
|
children?: React.ReactNode | ((heading: React.ReactNode) => React.ReactNode);
|
||||||
className?: string;
|
className?: string;
|
||||||
}> = ({ heading, children, ...props }) => {
|
}> = ({ heading, children, ...props }) => {
|
||||||
|
|
|
@ -10,6 +10,7 @@ import {
|
||||||
waitFor,
|
waitFor,
|
||||||
withExcalidrawDimensions,
|
withExcalidrawDimensions,
|
||||||
} from "../../tests/test-utils";
|
} from "../../tests/test-utils";
|
||||||
|
import { vi } from "vitest";
|
||||||
|
|
||||||
export const assertSidebarDockButton = async <T extends boolean>(
|
export const assertSidebarDockButton = async <T extends boolean>(
|
||||||
hasDockButton: T,
|
hasDockButton: T,
|
||||||
|
@ -205,7 +206,7 @@ describe("Sidebar", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("<Sidebar.Header> should render close button", async () => {
|
it("<Sidebar.Header> should render close button", async () => {
|
||||||
const onStateChange = jest.fn();
|
const onStateChange = vi.fn();
|
||||||
const CustomExcalidraw = () => {
|
const CustomExcalidraw = () => {
|
||||||
return (
|
return (
|
||||||
<Excalidraw
|
<Excalidraw
|
||||||
|
|
|
@ -53,7 +53,7 @@ export const SidebarInner = forwardRef(
|
||||||
}: SidebarProps & Omit<React.RefAttributes<HTMLDivElement>, "onSelect">,
|
}: SidebarProps & Omit<React.RefAttributes<HTMLDivElement>, "onSelect">,
|
||||||
ref: React.ForwardedRef<HTMLDivElement>,
|
ref: React.ForwardedRef<HTMLDivElement>,
|
||||||
) => {
|
) => {
|
||||||
if (process.env.NODE_ENV === "development" && onDock && docked == null) {
|
if (import.meta.env.DEV && onDock && docked == null) {
|
||||||
console.warn(
|
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`",
|
"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`",
|
||||||
);
|
);
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { render } from "@testing-library/react";
|
||||||
import fallbackLangData from "../locales/en.json";
|
import fallbackLangData from "../locales/en.json";
|
||||||
|
|
||||||
import Trans from "./Trans";
|
import Trans from "./Trans";
|
||||||
|
import { TranslationKeys } from "../i18n";
|
||||||
|
|
||||||
describe("Test <Trans/>", () => {
|
describe("Test <Trans/>", () => {
|
||||||
it("should translate the the strings correctly", () => {
|
it("should translate the the strings correctly", () => {
|
||||||
|
@ -18,24 +19,27 @@ describe("Test <Trans/>", () => {
|
||||||
const { getByTestId } = render(
|
const { getByTestId } = render(
|
||||||
<>
|
<>
|
||||||
<div data-testid="test1">
|
<div data-testid="test1">
|
||||||
<Trans i18nKey="transTest.key1" audience="world" />
|
<Trans
|
||||||
|
i18nKey={"transTest.key1" as unknown as TranslationKeys}
|
||||||
|
audience="world"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div data-testid="test2">
|
<div data-testid="test2">
|
||||||
<Trans
|
<Trans
|
||||||
i18nKey="transTest.key2"
|
i18nKey={"transTest.key2" as unknown as TranslationKeys}
|
||||||
link={(el) => <a href="https://example.com">{el}</a>}
|
link={(el) => <a href="https://example.com">{el}</a>}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div data-testid="test3">
|
<div data-testid="test3">
|
||||||
<Trans
|
<Trans
|
||||||
i18nKey="transTest.key3"
|
i18nKey={"transTest.key3" as unknown as TranslationKeys}
|
||||||
link={(el) => <a href="https://example.com">{el}</a>}
|
link={(el) => <a href="https://example.com">{el}</a>}
|
||||||
location="the button"
|
location="the button"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div data-testid="test4">
|
<div data-testid="test4">
|
||||||
<Trans
|
<Trans
|
||||||
i18nKey="transTest.key4"
|
i18nKey={"transTest.key4" as unknown as TranslationKeys}
|
||||||
link={(el) => <a href="https://example.com">{el}</a>}
|
link={(el) => <a href="https://example.com">{el}</a>}
|
||||||
location="the button"
|
location="the button"
|
||||||
bold={(el) => <strong>{el}</strong>}
|
bold={(el) => <strong>{el}</strong>}
|
||||||
|
@ -43,7 +47,7 @@ describe("Test <Trans/>", () => {
|
||||||
</div>
|
</div>
|
||||||
<div data-testid="test5">
|
<div data-testid="test5">
|
||||||
<Trans
|
<Trans
|
||||||
i18nKey="transTest.key5"
|
i18nKey={"transTest.key5" as unknown as TranslationKeys}
|
||||||
connect-link={(el) => <a href="https://example.com">{el}</a>}
|
connect-link={(el) => <a href="https://example.com">{el}</a>}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { useI18n } from "../i18n";
|
import { TranslationKeys, useI18n } from "../i18n";
|
||||||
|
|
||||||
// Used for splitting i18nKey into tokens in Trans component
|
// Used for splitting i18nKey into tokens in Trans component
|
||||||
// Example:
|
// Example:
|
||||||
|
@ -153,7 +153,7 @@ const Trans = ({
|
||||||
children,
|
children,
|
||||||
...props
|
...props
|
||||||
}: {
|
}: {
|
||||||
i18nKey: string;
|
i18nKey: TranslationKeys;
|
||||||
[key: string]: React.ReactNode | ((el: React.ReactNode) => React.ReactNode);
|
[key: string]: React.ReactNode | ((el: React.ReactNode) => React.ReactNode);
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||||
|
|
||||||
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
|
<div
|
||||||
data-testid="brave-measure-text-error"
|
data-testid="brave-measure-text-error"
|
||||||
>
|
>
|
||||||
|
|
|
@ -396,6 +396,14 @@ export const TrashIcon = createIcon(
|
||||||
modifiedTablerIconProps,
|
modifiedTablerIconProps,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const EmbedIcon = createIcon(
|
||||||
|
<g strokeWidth="1.25">
|
||||||
|
<polyline points="12 16 18 10 12 4" />
|
||||||
|
<polyline points="8 4 2 10 8 16" />
|
||||||
|
</g>,
|
||||||
|
modifiedTablerIconProps,
|
||||||
|
);
|
||||||
|
|
||||||
export const DuplicateIcon = createIcon(
|
export const DuplicateIcon = createIcon(
|
||||||
<g strokeWidth="1.25">
|
<g strokeWidth="1.25">
|
||||||
<path d="M14.375 6.458H8.958a2.5 2.5 0 0 0-2.5 2.5v5.417a2.5 2.5 0 0 0 2.5 2.5h5.417a2.5 2.5 0 0 0 2.5-2.5V8.958a2.5 2.5 0 0 0-2.5-2.5Z" />
|
<path d="M14.375 6.458H8.958a2.5 2.5 0 0 0-2.5 2.5v5.417a2.5 2.5 0 0 0 2.5 2.5h5.417a2.5 2.5 0 0 0 2.5-2.5V8.958a2.5 2.5 0 0 0-2.5-2.5Z" />
|
||||||
|
|
|
@ -4,6 +4,7 @@ import {
|
||||||
useExcalidrawSetAppState,
|
useExcalidrawSetAppState,
|
||||||
useExcalidrawActionManager,
|
useExcalidrawActionManager,
|
||||||
useExcalidrawElements,
|
useExcalidrawElements,
|
||||||
|
useAppProps,
|
||||||
} from "../App";
|
} from "../App";
|
||||||
import {
|
import {
|
||||||
ExportIcon,
|
ExportIcon,
|
||||||
|
@ -198,13 +199,20 @@ export const ChangeCanvasBackground = () => {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const appState = useUIAppState();
|
const appState = useUIAppState();
|
||||||
const actionManager = useExcalidrawActionManager();
|
const actionManager = useExcalidrawActionManager();
|
||||||
|
const appProps = useAppProps();
|
||||||
|
|
||||||
if (appState.viewModeEnabled) {
|
if (
|
||||||
|
appState.viewModeEnabled ||
|
||||||
|
!appProps.UIOptions.canvasActions.changeViewBackgroundColor
|
||||||
|
) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div style={{ marginTop: "0.5rem" }}>
|
<div style={{ marginTop: "0.5rem" }}>
|
||||||
<div style={{ fontSize: ".75rem", marginBottom: ".5rem" }}>
|
<div
|
||||||
|
data-testid="canvas-background-label"
|
||||||
|
style={{ fontSize: ".75rem", marginBottom: ".5rem" }}
|
||||||
|
>
|
||||||
{t("labels.canvasBackground")}
|
{t("labels.canvasBackground")}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ padding: "0 0.625rem" }}>
|
<div style={{ padding: "0 0.625rem" }}>
|
||||||
|
|
|
@ -71,8 +71,18 @@ export enum EVENT {
|
||||||
// custom events
|
// custom events
|
||||||
EXCALIDRAW_LINK = "excalidraw-link",
|
EXCALIDRAW_LINK = "excalidraw-link",
|
||||||
MENU_ITEM_SELECT = "menu.itemSelect",
|
MENU_ITEM_SELECT = "menu.itemSelect",
|
||||||
|
MESSAGE = "message",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const YOUTUBE_STATES = {
|
||||||
|
UNSTARTED: -1,
|
||||||
|
ENDED: 0,
|
||||||
|
PLAYING: 1,
|
||||||
|
PAUSED: 2,
|
||||||
|
BUFFERING: 3,
|
||||||
|
CUED: 5,
|
||||||
|
} as const;
|
||||||
|
|
||||||
export const ENV = {
|
export const ENV = {
|
||||||
TEST: "test",
|
TEST: "test",
|
||||||
DEVELOPMENT: "development",
|
DEVELOPMENT: "development",
|
||||||
|
@ -92,7 +102,7 @@ export const FONT_FAMILY = {
|
||||||
export const THEME = {
|
export const THEME = {
|
||||||
LIGHT: "light",
|
LIGHT: "light",
|
||||||
DARK: "dark",
|
DARK: "dark",
|
||||||
};
|
} as const;
|
||||||
|
|
||||||
export const FRAME_STYLE = {
|
export const FRAME_STYLE = {
|
||||||
strokeColor: "#bbb" as ExcalidrawElement["strokeColor"],
|
strokeColor: "#bbb" as ExcalidrawElement["strokeColor"],
|
||||||
|
@ -301,3 +311,5 @@ export const DEFAULT_SIDEBAR = {
|
||||||
name: "default",
|
name: "default",
|
||||||
defaultTab: LIBRARY_SIDEBAR_TAB,
|
defaultTab: LIBRARY_SIDEBAR_TAB,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
export const LIBRARY_DISABLED_TYPES = new Set(["embeddable", "image"] as const);
|
||||||
|
|
|
@ -77,6 +77,19 @@
|
||||||
position: absolute;
|
position: absolute;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__embeddable {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__embeddable-container {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 2;
|
||||||
|
transform-origin: top left;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
&.theme--dark {
|
&.theme--dark {
|
||||||
// The percentage is inspired by
|
// The percentage is inspired by
|
||||||
// https://material.io/design/color/dark-theme.html#properties, which
|
// https://material.io/design/color/dark-theme.html#properties, which
|
||||||
|
@ -661,3 +674,33 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.excalidraw__embeddable-container {
|
||||||
|
.excalidraw__embeddable-container__inner {
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: var(--embeddable-radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
.excalidraw__embeddable__outer {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
& > * {
|
||||||
|
border-radius: var(--embeddable-radius);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.excalidraw__embeddable-hint {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 1;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
padding: 1rem 1.6rem;
|
||||||
|
border-radius: 12px;
|
||||||
|
color: #fff;
|
||||||
|
font-weight: bold;
|
||||||
|
letter-spacing: 0.6px;
|
||||||
|
font-family: "Assistant";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ import {
|
||||||
ExcalidrawSelectionElement,
|
ExcalidrawSelectionElement,
|
||||||
ExcalidrawTextElement,
|
ExcalidrawTextElement,
|
||||||
FontFamilyValues,
|
FontFamilyValues,
|
||||||
|
PointBinding,
|
||||||
StrokeRoundness,
|
StrokeRoundness,
|
||||||
} from "../element/types";
|
} from "../element/types";
|
||||||
import {
|
import {
|
||||||
|
@ -65,6 +66,7 @@ export const AllowedExcalidrawActiveTools: Record<
|
||||||
eraser: false,
|
eraser: false,
|
||||||
custom: true,
|
custom: true,
|
||||||
frame: true,
|
frame: true,
|
||||||
|
embeddable: true,
|
||||||
hand: true,
|
hand: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -83,6 +85,13 @@ const getFontFamilyByName = (fontFamilyName: string): FontFamilyValues => {
|
||||||
return DEFAULT_FONT_FAMILY;
|
return DEFAULT_FONT_FAMILY;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const repairBinding = (binding: PointBinding | null) => {
|
||||||
|
if (!binding) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return { ...binding, focus: binding.focus || 0 };
|
||||||
|
};
|
||||||
|
|
||||||
const restoreElementWithProperties = <
|
const restoreElementWithProperties = <
|
||||||
T extends Required<Omit<ExcalidrawElement, "customData">> & {
|
T extends Required<Omit<ExcalidrawElement, "customData">> & {
|
||||||
customData?: ExcalidrawElement["customData"];
|
customData?: ExcalidrawElement["customData"];
|
||||||
|
@ -258,8 +267,8 @@ const restoreElement = (
|
||||||
(element.type as ExcalidrawElement["type"] | "draw") === "draw"
|
(element.type as ExcalidrawElement["type"] | "draw") === "draw"
|
||||||
? "line"
|
? "line"
|
||||||
: element.type,
|
: element.type,
|
||||||
startBinding: element.startBinding,
|
startBinding: repairBinding(element.startBinding),
|
||||||
endBinding: element.endBinding,
|
endBinding: repairBinding(element.endBinding),
|
||||||
lastCommittedPoint: null,
|
lastCommittedPoint: null,
|
||||||
startArrowhead,
|
startArrowhead,
|
||||||
endArrowhead,
|
endArrowhead,
|
||||||
|
@ -276,6 +285,10 @@ const restoreElement = (
|
||||||
return restoreElementWithProperties(element, {});
|
return restoreElementWithProperties(element, {});
|
||||||
case "diamond":
|
case "diamond":
|
||||||
return restoreElementWithProperties(element, {});
|
return restoreElementWithProperties(element, {});
|
||||||
|
case "embeddable":
|
||||||
|
return restoreElementWithProperties(element, {
|
||||||
|
validated: undefined,
|
||||||
|
});
|
||||||
case "frame":
|
case "frame":
|
||||||
return restoreElementWithProperties(element, {
|
return restoreElementWithProperties(element, {
|
||||||
name: element.name ?? null,
|
name: element.name ?? null,
|
||||||
|
|
|
@ -1,9 +1,35 @@
|
||||||
import { sanitizeUrl } from "@braintree/sanitize-url";
|
import { sanitizeUrl } from "@braintree/sanitize-url";
|
||||||
|
|
||||||
export const normalizeLink = (link: string) => {
|
export const normalizeLink = (link: string) => {
|
||||||
|
link = link.trim();
|
||||||
|
if (!link) {
|
||||||
|
return link;
|
||||||
|
}
|
||||||
return sanitizeUrl(link);
|
return sanitizeUrl(link);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isLocalLink = (link: string | null) => {
|
export const isLocalLink = (link: string | null) => {
|
||||||
return !!(link?.includes(location.origin) || link?.startsWith("/"));
|
return !!(link?.includes(location.origin) || link?.startsWith("/"));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns URL sanitized and safe for usage in places such as
|
||||||
|
* iframe's src attribute or <a> href attributes.
|
||||||
|
*/
|
||||||
|
export const toValidURL = (link: string) => {
|
||||||
|
link = normalizeLink(link);
|
||||||
|
|
||||||
|
// make relative links into fully-qualified urls
|
||||||
|
if (link.startsWith("/")) {
|
||||||
|
return `${location.origin}${link}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
new URL(link);
|
||||||
|
} catch {
|
||||||
|
// if link does not parse as URL, assume invalid and return blank page
|
||||||
|
return "about:blank";
|
||||||
|
}
|
||||||
|
|
||||||
|
return link;
|
||||||
|
};
|
||||||
|
|
|
@ -55,10 +55,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.d-none {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
&--remove .ToolIcon__icon svg {
|
&--remove .ToolIcon__icon svg {
|
||||||
color: $oc-red-6;
|
color: $oc-red-6;
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,8 +5,12 @@ import {
|
||||||
viewportCoordsToSceneCoords,
|
viewportCoordsToSceneCoords,
|
||||||
wrapEvent,
|
wrapEvent,
|
||||||
} from "../utils";
|
} from "../utils";
|
||||||
|
import { getEmbedLink, embeddableURLValidator } from "./embeddable";
|
||||||
import { mutateElement } from "./mutateElement";
|
import { mutateElement } from "./mutateElement";
|
||||||
import { NonDeletedExcalidrawElement } from "./types";
|
import {
|
||||||
|
ExcalidrawEmbeddableElement,
|
||||||
|
NonDeletedExcalidrawElement,
|
||||||
|
} from "./types";
|
||||||
|
|
||||||
import { register } from "../actions/register";
|
import { register } from "../actions/register";
|
||||||
import { ToolButton } from "../components/ToolButton";
|
import { ToolButton } from "../components/ToolButton";
|
||||||
|
@ -21,7 +25,10 @@ import {
|
||||||
} from "react";
|
} from "react";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { KEYS } from "../keys";
|
import { KEYS } from "../keys";
|
||||||
import { DEFAULT_LINK_SIZE } from "../renderer/renderElement";
|
import {
|
||||||
|
DEFAULT_LINK_SIZE,
|
||||||
|
invalidateShapeForElement,
|
||||||
|
} from "../renderer/renderElement";
|
||||||
import { rotate } from "../math";
|
import { rotate } from "../math";
|
||||||
import { EVENT, HYPERLINK_TOOLTIP_DELAY, MIME_TYPES } from "../constants";
|
import { EVENT, HYPERLINK_TOOLTIP_DELAY, MIME_TYPES } from "../constants";
|
||||||
import { Bounds } from "./bounds";
|
import { Bounds } from "./bounds";
|
||||||
|
@ -33,7 +40,8 @@ import { isLocalLink, normalizeLink } from "../data/url";
|
||||||
|
|
||||||
import "./Hyperlink.scss";
|
import "./Hyperlink.scss";
|
||||||
import { trackEvent } from "../analytics";
|
import { trackEvent } from "../analytics";
|
||||||
import { useExcalidrawAppState } from "../components/App";
|
import { useAppProps, useExcalidrawAppState } from "../components/App";
|
||||||
|
import { isEmbeddableElement } from "./typeChecks";
|
||||||
|
|
||||||
const CONTAINER_WIDTH = 320;
|
const CONTAINER_WIDTH = 320;
|
||||||
const SPACE_BOTTOM = 85;
|
const SPACE_BOTTOM = 85;
|
||||||
|
@ -48,37 +56,112 @@ EXTERNAL_LINK_IMG.src = `data:${MIME_TYPES.svg}, ${encodeURIComponent(
|
||||||
|
|
||||||
let IS_HYPERLINK_TOOLTIP_VISIBLE = false;
|
let IS_HYPERLINK_TOOLTIP_VISIBLE = false;
|
||||||
|
|
||||||
|
const embeddableLinkCache = new Map<
|
||||||
|
ExcalidrawEmbeddableElement["id"],
|
||||||
|
string
|
||||||
|
>();
|
||||||
|
|
||||||
export const Hyperlink = ({
|
export const Hyperlink = ({
|
||||||
element,
|
element,
|
||||||
setAppState,
|
setAppState,
|
||||||
onLinkOpen,
|
onLinkOpen,
|
||||||
|
setToast,
|
||||||
}: {
|
}: {
|
||||||
element: NonDeletedExcalidrawElement;
|
element: NonDeletedExcalidrawElement;
|
||||||
setAppState: React.Component<any, AppState>["setState"];
|
setAppState: React.Component<any, AppState>["setState"];
|
||||||
onLinkOpen: ExcalidrawProps["onLinkOpen"];
|
onLinkOpen: ExcalidrawProps["onLinkOpen"];
|
||||||
|
setToast: (
|
||||||
|
toast: { message: string; closable?: boolean; duration?: number } | null,
|
||||||
|
) => void;
|
||||||
}) => {
|
}) => {
|
||||||
const appState = useExcalidrawAppState();
|
const appState = useExcalidrawAppState();
|
||||||
|
const appProps = useAppProps();
|
||||||
|
|
||||||
const linkVal = element.link || "";
|
const linkVal = element.link || "";
|
||||||
|
|
||||||
const [inputVal, setInputVal] = useState(linkVal);
|
const [inputVal, setInputVal] = useState(linkVal);
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
const isEditing = appState.showHyperlinkPopup === "editor" || !linkVal;
|
const isEditing = appState.showHyperlinkPopup === "editor";
|
||||||
|
|
||||||
const handleSubmit = useCallback(() => {
|
const handleSubmit = useCallback(() => {
|
||||||
if (!inputRef.current) {
|
if (!inputRef.current) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const link = normalizeLink(inputRef.current.value);
|
const link = normalizeLink(inputRef.current.value) || null;
|
||||||
|
|
||||||
if (!element.link && link) {
|
if (!element.link && link) {
|
||||||
trackEvent("hyperlink", "create");
|
trackEvent("hyperlink", "create");
|
||||||
}
|
}
|
||||||
|
|
||||||
mutateElement(element, { link });
|
if (isEmbeddableElement(element)) {
|
||||||
setAppState({ showHyperlinkPopup: "info" });
|
if (appState.activeEmbeddable?.element === element) {
|
||||||
}, [element, setAppState]);
|
setAppState({ activeEmbeddable: null });
|
||||||
|
}
|
||||||
|
if (!link) {
|
||||||
|
mutateElement(element, {
|
||||||
|
validated: false,
|
||||||
|
link: null,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!embeddableURLValidator(link, appProps.validateEmbeddable)) {
|
||||||
|
if (link) {
|
||||||
|
setToast({ message: t("toast.unableToEmbed"), closable: true });
|
||||||
|
}
|
||||||
|
element.link && embeddableLinkCache.set(element.id, element.link);
|
||||||
|
mutateElement(element, {
|
||||||
|
validated: false,
|
||||||
|
link,
|
||||||
|
});
|
||||||
|
invalidateShapeForElement(element);
|
||||||
|
} else {
|
||||||
|
const { width, height } = element;
|
||||||
|
const embedLink = getEmbedLink(link);
|
||||||
|
if (embedLink?.warning) {
|
||||||
|
setToast({ message: embedLink.warning, closable: true });
|
||||||
|
}
|
||||||
|
const ar = embedLink
|
||||||
|
? embedLink.aspectRatio.w / embedLink.aspectRatio.h
|
||||||
|
: 1;
|
||||||
|
const hasLinkChanged =
|
||||||
|
embeddableLinkCache.get(element.id) !== element.link;
|
||||||
|
mutateElement(element, {
|
||||||
|
...(hasLinkChanged
|
||||||
|
? {
|
||||||
|
width:
|
||||||
|
embedLink?.type === "video"
|
||||||
|
? width > height
|
||||||
|
? width
|
||||||
|
: height * ar
|
||||||
|
: width,
|
||||||
|
height:
|
||||||
|
embedLink?.type === "video"
|
||||||
|
? width > height
|
||||||
|
? width / ar
|
||||||
|
: height
|
||||||
|
: height,
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
validated: true,
|
||||||
|
link,
|
||||||
|
});
|
||||||
|
invalidateShapeForElement(element);
|
||||||
|
if (embeddableLinkCache.has(element.id)) {
|
||||||
|
embeddableLinkCache.delete(element.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
mutateElement(element, { link });
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
element,
|
||||||
|
setToast,
|
||||||
|
appProps.validateEmbeddable,
|
||||||
|
appState.activeEmbeddable,
|
||||||
|
setAppState,
|
||||||
|
]);
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
|
@ -132,10 +215,12 @@ export const Hyperlink = ({
|
||||||
appState.draggingElement ||
|
appState.draggingElement ||
|
||||||
appState.resizingElement ||
|
appState.resizingElement ||
|
||||||
appState.isRotating ||
|
appState.isRotating ||
|
||||||
appState.openMenu
|
appState.openMenu ||
|
||||||
|
appState.viewModeEnabled
|
||||||
) {
|
) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="excalidraw-hyperlinkContainer"
|
className="excalidraw-hyperlinkContainer"
|
||||||
|
@ -145,6 +230,11 @@ export const Hyperlink = ({
|
||||||
width: CONTAINER_WIDTH,
|
width: CONTAINER_WIDTH,
|
||||||
padding: CONTAINER_PADDING,
|
padding: CONTAINER_PADDING,
|
||||||
}}
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
if (!element.link && !isEditing) {
|
||||||
|
setAppState({ showHyperlinkPopup: "editor" });
|
||||||
|
}
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{isEditing ? (
|
{isEditing ? (
|
||||||
<input
|
<input
|
||||||
|
@ -162,15 +252,14 @@ export const Hyperlink = ({
|
||||||
}
|
}
|
||||||
if (event.key === KEYS.ENTER || event.key === KEYS.ESCAPE) {
|
if (event.key === KEYS.ENTER || event.key === KEYS.ESCAPE) {
|
||||||
handleSubmit();
|
handleSubmit();
|
||||||
|
setAppState({ showHyperlinkPopup: "info" });
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : element.link ? (
|
||||||
<a
|
<a
|
||||||
href={normalizeLink(element.link || "")}
|
href={normalizeLink(element.link || "")}
|
||||||
className={clsx("excalidraw-hyperlinkContainer-link", {
|
className="excalidraw-hyperlinkContainer-link"
|
||||||
"d-none": isEditing,
|
|
||||||
})}
|
|
||||||
target={isLocalLink(element.link) ? "_self" : "_blank"}
|
target={isLocalLink(element.link) ? "_self" : "_blank"}
|
||||||
onClick={(event) => {
|
onClick={(event) => {
|
||||||
if (element.link && onLinkOpen) {
|
if (element.link && onLinkOpen) {
|
||||||
|
@ -194,6 +283,10 @@ export const Hyperlink = ({
|
||||||
>
|
>
|
||||||
{element.link}
|
{element.link}
|
||||||
</a>
|
</a>
|
||||||
|
) : (
|
||||||
|
<div className="excalidraw-hyperlinkContainer-link">
|
||||||
|
{t("labels.link.empty")}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="excalidraw-hyperlinkContainer__buttons">
|
<div className="excalidraw-hyperlinkContainer__buttons">
|
||||||
{!isEditing && (
|
{!isEditing && (
|
||||||
|
@ -207,8 +300,7 @@ export const Hyperlink = ({
|
||||||
icon={FreedrawIcon}
|
icon={FreedrawIcon}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{linkVal && !isEmbeddableElement(element) && (
|
||||||
{linkVal && (
|
|
||||||
<ToolButton
|
<ToolButton
|
||||||
type="button"
|
type="button"
|
||||||
title={t("buttons.remove")}
|
title={t("buttons.remove")}
|
||||||
|
@ -271,7 +363,11 @@ export const actionLink = register({
|
||||||
type="button"
|
type="button"
|
||||||
icon={LinkIcon}
|
icon={LinkIcon}
|
||||||
aria-label={t(getContextMenuLabel(elements, appState))}
|
aria-label={t(getContextMenuLabel(elements, appState))}
|
||||||
title={`${t("labels.link.label")} - ${getShortcutKey("CtrlOrCmd+K")}`}
|
title={`${
|
||||||
|
isEmbeddableElement(elements[0])
|
||||||
|
? t("labels.link.labelEmbed")
|
||||||
|
: t("labels.link.label")
|
||||||
|
} - ${getShortcutKey("CtrlOrCmd+K")}`}
|
||||||
onClick={() => updateData(null)}
|
onClick={() => updateData(null)}
|
||||||
selected={selectedElements.length === 1 && !!selectedElements[0].link}
|
selected={selectedElements.length === 1 && !!selectedElements[0].link}
|
||||||
/>
|
/>
|
||||||
|
@ -285,7 +381,11 @@ export const getContextMenuLabel = (
|
||||||
) => {
|
) => {
|
||||||
const selectedElements = getSelectedElements(elements, appState);
|
const selectedElements = getSelectedElements(elements, appState);
|
||||||
const label = selectedElements[0]!.link
|
const label = selectedElements[0]!.link
|
||||||
? "labels.link.edit"
|
? isEmbeddableElement(selectedElements[0])
|
||||||
|
? "labels.link.editEmbed"
|
||||||
|
: "labels.link.edit"
|
||||||
|
: isEmbeddableElement(selectedElements[0])
|
||||||
|
? "labels.link.createEmbed"
|
||||||
: "labels.link.create";
|
: "labels.link.create";
|
||||||
return label;
|
return label;
|
||||||
};
|
};
|
||||||
|
@ -327,6 +427,26 @@ export const isPointHittingLinkIcon = (
|
||||||
element: NonDeletedExcalidrawElement,
|
element: NonDeletedExcalidrawElement,
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
[x, y]: Point,
|
[x, y]: Point,
|
||||||
|
) => {
|
||||||
|
const threshold = 4 / appState.zoom.value;
|
||||||
|
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
|
||||||
|
const [linkX, linkY, linkWidth, linkHeight] = getLinkHandleFromCoords(
|
||||||
|
[x1, y1, x2, y2],
|
||||||
|
element.angle,
|
||||||
|
appState,
|
||||||
|
);
|
||||||
|
const hitLink =
|
||||||
|
x > linkX - threshold &&
|
||||||
|
x < linkX + threshold + linkWidth &&
|
||||||
|
y > linkY - threshold &&
|
||||||
|
y < linkY + linkHeight + threshold;
|
||||||
|
return hitLink;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isPointHittingLink = (
|
||||||
|
element: NonDeletedExcalidrawElement,
|
||||||
|
appState: AppState,
|
||||||
|
[x, y]: Point,
|
||||||
isMobile: boolean,
|
isMobile: boolean,
|
||||||
) => {
|
) => {
|
||||||
if (!element.link || appState.selectedElementIds[element.id]) {
|
if (!element.link || appState.selectedElementIds[element.id]) {
|
||||||
|
@ -340,19 +460,7 @@ export const isPointHittingLinkIcon = (
|
||||||
) {
|
) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
|
return isPointHittingLinkIcon(element, appState, [x, y]);
|
||||||
|
|
||||||
const [linkX, linkY, linkWidth, linkHeight] = getLinkHandleFromCoords(
|
|
||||||
[x1, y1, x2, y2],
|
|
||||||
element.angle,
|
|
||||||
appState,
|
|
||||||
);
|
|
||||||
const hitLink =
|
|
||||||
x > linkX - threshold &&
|
|
||||||
x < linkX + threshold + linkWidth &&
|
|
||||||
y > linkY - threshold &&
|
|
||||||
y < linkY + linkHeight + threshold;
|
|
||||||
return hitLink;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let HYPERLINK_TOOLTIP_TIMEOUT_ID: number | null = null;
|
let HYPERLINK_TOOLTIP_TIMEOUT_ID: number | null = null;
|
||||||
|
|
|
@ -18,6 +18,7 @@ import {
|
||||||
ExcalidrawBindableElement,
|
ExcalidrawBindableElement,
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
ExcalidrawRectangleElement,
|
ExcalidrawRectangleElement,
|
||||||
|
ExcalidrawEmbeddableElement,
|
||||||
ExcalidrawDiamondElement,
|
ExcalidrawDiamondElement,
|
||||||
ExcalidrawTextElement,
|
ExcalidrawTextElement,
|
||||||
ExcalidrawEllipseElement,
|
ExcalidrawEllipseElement,
|
||||||
|
@ -39,7 +40,11 @@ import { FrameNameBoundsCache, Point } from "../types";
|
||||||
import { Drawable } from "roughjs/bin/core";
|
import { Drawable } from "roughjs/bin/core";
|
||||||
import { AppState } from "../types";
|
import { AppState } from "../types";
|
||||||
import { getShapeForElement } from "../renderer/renderElement";
|
import { getShapeForElement } from "../renderer/renderElement";
|
||||||
import { hasBoundTextElement, isImageElement } from "./typeChecks";
|
import {
|
||||||
|
hasBoundTextElement,
|
||||||
|
isEmbeddableElement,
|
||||||
|
isImageElement,
|
||||||
|
} from "./typeChecks";
|
||||||
import { isTextElement } from ".";
|
import { isTextElement } from ".";
|
||||||
import { isTransparent } from "../utils";
|
import { isTransparent } from "../utils";
|
||||||
import { shouldShowBoundingBox } from "./transformHandles";
|
import { shouldShowBoundingBox } from "./transformHandles";
|
||||||
|
@ -57,7 +62,9 @@ const isElementDraggableFromInside = (
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
const isDraggableFromInside =
|
const isDraggableFromInside =
|
||||||
!isTransparent(element.backgroundColor) || hasBoundTextElement(element);
|
!isTransparent(element.backgroundColor) ||
|
||||||
|
hasBoundTextElement(element) ||
|
||||||
|
isEmbeddableElement(element);
|
||||||
if (element.type === "line") {
|
if (element.type === "line") {
|
||||||
return isDraggableFromInside && isPathALoop(element.points);
|
return isDraggableFromInside && isPathALoop(element.points);
|
||||||
}
|
}
|
||||||
|
@ -248,6 +255,7 @@ type HitTestArgs = {
|
||||||
const hitTestPointAgainstElement = (args: HitTestArgs): boolean => {
|
const hitTestPointAgainstElement = (args: HitTestArgs): boolean => {
|
||||||
switch (args.element.type) {
|
switch (args.element.type) {
|
||||||
case "rectangle":
|
case "rectangle":
|
||||||
|
case "embeddable":
|
||||||
case "image":
|
case "image":
|
||||||
case "text":
|
case "text":
|
||||||
case "diamond":
|
case "diamond":
|
||||||
|
@ -306,6 +314,7 @@ export const distanceToBindableElement = (
|
||||||
case "rectangle":
|
case "rectangle":
|
||||||
case "image":
|
case "image":
|
||||||
case "text":
|
case "text":
|
||||||
|
case "embeddable":
|
||||||
case "frame":
|
case "frame":
|
||||||
return distanceToRectangle(element, point);
|
return distanceToRectangle(element, point);
|
||||||
case "diamond":
|
case "diamond":
|
||||||
|
@ -337,6 +346,7 @@ const distanceToRectangle = (
|
||||||
| ExcalidrawTextElement
|
| ExcalidrawTextElement
|
||||||
| ExcalidrawFreeDrawElement
|
| ExcalidrawFreeDrawElement
|
||||||
| ExcalidrawImageElement
|
| ExcalidrawImageElement
|
||||||
|
| ExcalidrawEmbeddableElement
|
||||||
| ExcalidrawFrameElement,
|
| ExcalidrawFrameElement,
|
||||||
point: Point,
|
point: Point,
|
||||||
): number => {
|
): number => {
|
||||||
|
@ -645,17 +655,23 @@ export const determineFocusDistance = (
|
||||||
const c = line[1];
|
const c = line[1];
|
||||||
const mabs = Math.abs(m);
|
const mabs = Math.abs(m);
|
||||||
const nabs = Math.abs(n);
|
const nabs = Math.abs(n);
|
||||||
|
let ret;
|
||||||
switch (element.type) {
|
switch (element.type) {
|
||||||
case "rectangle":
|
case "rectangle":
|
||||||
case "image":
|
case "image":
|
||||||
case "text":
|
case "text":
|
||||||
|
case "embeddable":
|
||||||
case "frame":
|
case "frame":
|
||||||
return c / (hwidth * (nabs + q * mabs));
|
ret = c / (hwidth * (nabs + q * mabs));
|
||||||
|
break;
|
||||||
case "diamond":
|
case "diamond":
|
||||||
return mabs < nabs ? c / (nabs * hwidth) : c / (mabs * hheight);
|
ret = mabs < nabs ? c / (nabs * hwidth) : c / (mabs * hheight);
|
||||||
|
break;
|
||||||
case "ellipse":
|
case "ellipse":
|
||||||
return c / (hwidth * Math.sqrt(n ** 2 + q ** 2 * m ** 2));
|
ret = c / (hwidth * Math.sqrt(n ** 2 + q ** 2 * m ** 2));
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
return ret || 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const determineFocusPoint = (
|
export const determineFocusPoint = (
|
||||||
|
@ -682,6 +698,7 @@ export const determineFocusPoint = (
|
||||||
case "image":
|
case "image":
|
||||||
case "text":
|
case "text":
|
||||||
case "diamond":
|
case "diamond":
|
||||||
|
case "embeddable":
|
||||||
case "frame":
|
case "frame":
|
||||||
point = findFocusPointForRectangulars(element, focus, adjecentPointRel);
|
point = findFocusPointForRectangulars(element, focus, adjecentPointRel);
|
||||||
break;
|
break;
|
||||||
|
@ -733,6 +750,7 @@ const getSortedElementLineIntersections = (
|
||||||
case "image":
|
case "image":
|
||||||
case "text":
|
case "text":
|
||||||
case "diamond":
|
case "diamond":
|
||||||
|
case "embeddable":
|
||||||
case "frame":
|
case "frame":
|
||||||
const corners = getCorners(element);
|
const corners = getCorners(element);
|
||||||
intersections = corners
|
intersections = corners
|
||||||
|
@ -768,6 +786,7 @@ const getCorners = (
|
||||||
| ExcalidrawImageElement
|
| ExcalidrawImageElement
|
||||||
| ExcalidrawDiamondElement
|
| ExcalidrawDiamondElement
|
||||||
| ExcalidrawTextElement
|
| ExcalidrawTextElement
|
||||||
|
| ExcalidrawEmbeddableElement
|
||||||
| ExcalidrawFrameElement,
|
| ExcalidrawFrameElement,
|
||||||
scale: number = 1,
|
scale: number = 1,
|
||||||
): GA.Point[] => {
|
): GA.Point[] => {
|
||||||
|
@ -777,6 +796,7 @@ const getCorners = (
|
||||||
case "rectangle":
|
case "rectangle":
|
||||||
case "image":
|
case "image":
|
||||||
case "text":
|
case "text":
|
||||||
|
case "embeddable":
|
||||||
case "frame":
|
case "frame":
|
||||||
return [
|
return [
|
||||||
GA.point(hx, hy),
|
GA.point(hx, hy),
|
||||||
|
@ -926,6 +946,7 @@ export const findFocusPointForRectangulars = (
|
||||||
| ExcalidrawImageElement
|
| ExcalidrawImageElement
|
||||||
| ExcalidrawDiamondElement
|
| ExcalidrawDiamondElement
|
||||||
| ExcalidrawTextElement
|
| ExcalidrawTextElement
|
||||||
|
| ExcalidrawEmbeddableElement
|
||||||
| ExcalidrawFrameElement,
|
| ExcalidrawFrameElement,
|
||||||
// Between -1 and 1 for how far away should the focus point be relative
|
// Between -1 and 1 for how far away should the focus point be relative
|
||||||
// to the size of the element. Sign determines orientation.
|
// to the size of the element. Sign determines orientation.
|
||||||
|
|
350
src/element/embeddable.ts
Normal file
350
src/element/embeddable.ts
Normal file
|
@ -0,0 +1,350 @@
|
||||||
|
import { register } from "../actions/register";
|
||||||
|
import { FONT_FAMILY, VERTICAL_ALIGN } from "../constants";
|
||||||
|
import { t } from "../i18n";
|
||||||
|
import { ExcalidrawProps } from "../types";
|
||||||
|
import { getFontString, setCursorForShape, updateActiveTool } from "../utils";
|
||||||
|
import { newTextElement } from "./newElement";
|
||||||
|
import { getContainerElement, wrapText } from "./textElement";
|
||||||
|
import { isEmbeddableElement } from "./typeChecks";
|
||||||
|
import {
|
||||||
|
ExcalidrawElement,
|
||||||
|
ExcalidrawEmbeddableElement,
|
||||||
|
NonDeletedExcalidrawElement,
|
||||||
|
Theme,
|
||||||
|
} from "./types";
|
||||||
|
|
||||||
|
type EmbeddedLink =
|
||||||
|
| ({
|
||||||
|
aspectRatio: { w: number; h: number };
|
||||||
|
warning?: string;
|
||||||
|
} & (
|
||||||
|
| { type: "video" | "generic"; link: string }
|
||||||
|
| { type: "document"; srcdoc: (theme: Theme) => string }
|
||||||
|
))
|
||||||
|
| null;
|
||||||
|
|
||||||
|
const embeddedLinkCache = new Map<string, EmbeddedLink>();
|
||||||
|
|
||||||
|
const RE_YOUTUBE =
|
||||||
|
/^(?:http(?:s)?:\/\/)?(?:www\.)?youtu(?:be\.com|\.be)\/(embed\/|watch\?v=|shorts\/|playlist\?list=|embed\/videoseries\?list=)?([a-zA-Z0-9_-]+)(?:\?t=|&t=|\?start=|&start=)?([a-zA-Z0-9_-]+)?[^\s]*$/;
|
||||||
|
const RE_VIMEO =
|
||||||
|
/^(?:http(?:s)?:\/\/)?(?:(?:w){3}.)?(?:player\.)?vimeo\.com\/(?:video\/)?([^?\s]+)(?:\?.*)?$/;
|
||||||
|
const RE_FIGMA = /^https:\/\/(?:www\.)?figma\.com/;
|
||||||
|
|
||||||
|
const RE_GH_GIST = /^https:\/\/gist\.github\.com/;
|
||||||
|
const RE_GH_GIST_EMBED =
|
||||||
|
/^<script[\s\S]*?\ssrc=["'](https:\/\/gist.github.com\/.*?)\.js["']/i;
|
||||||
|
|
||||||
|
// not anchored to start to allow <blockquote> twitter embeds
|
||||||
|
const RE_TWITTER = /(?:http(?:s)?:\/\/)?(?:(?:w){3}.)?twitter.com/;
|
||||||
|
const RE_TWITTER_EMBED =
|
||||||
|
/^<blockquote[\s\S]*?\shref=["'](https:\/\/twitter.com\/[^"']*)/i;
|
||||||
|
|
||||||
|
const RE_VALTOWN =
|
||||||
|
/^https:\/\/(?:www\.)?val.town\/(v|embed)\/[a-zA-Z_$][0-9a-zA-Z_$]+\.[a-zA-Z_$][0-9a-zA-Z_$]+/;
|
||||||
|
|
||||||
|
const RE_GENERIC_EMBED =
|
||||||
|
/^<(?:iframe|blockquote)[\s\S]*?\s(?:src|href)=["']([^"']*)["'][\s\S]*?>$/i;
|
||||||
|
|
||||||
|
const ALLOWED_DOMAINS = new Set([
|
||||||
|
"youtube.com",
|
||||||
|
"youtu.be",
|
||||||
|
"vimeo.com",
|
||||||
|
"player.vimeo.com",
|
||||||
|
"figma.com",
|
||||||
|
"link.excalidraw.com",
|
||||||
|
"gist.github.com",
|
||||||
|
"twitter.com",
|
||||||
|
"*.simplepdf.eu",
|
||||||
|
"stackblitz.com",
|
||||||
|
"val.town",
|
||||||
|
]);
|
||||||
|
|
||||||
|
const createSrcDoc = (body: string) => {
|
||||||
|
return `<html><body>${body}</body></html>`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getEmbedLink = (link: string | null | undefined): EmbeddedLink => {
|
||||||
|
if (!link) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (embeddedLinkCache.has(link)) {
|
||||||
|
return embeddedLinkCache.get(link)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
const originalLink = link;
|
||||||
|
|
||||||
|
let type: "video" | "generic" = "generic";
|
||||||
|
let aspectRatio = { w: 560, h: 840 };
|
||||||
|
const ytLink = link.match(RE_YOUTUBE);
|
||||||
|
if (ytLink?.[2]) {
|
||||||
|
const time = ytLink[3] ? `&start=${ytLink[3]}` : ``;
|
||||||
|
const isPortrait = link.includes("shorts");
|
||||||
|
type = "video";
|
||||||
|
switch (ytLink[1]) {
|
||||||
|
case "embed/":
|
||||||
|
case "watch?v=":
|
||||||
|
case "shorts/":
|
||||||
|
link = `https://www.youtube.com/embed/${ytLink[2]}?enablejsapi=1${time}`;
|
||||||
|
break;
|
||||||
|
case "playlist?list=":
|
||||||
|
case "embed/videoseries?list=":
|
||||||
|
link = `https://www.youtube.com/embed/videoseries?list=${ytLink[2]}&enablejsapi=1${time}`;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
link = `https://www.youtube.com/embed/${ytLink[2]}?enablejsapi=1${time}`;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
aspectRatio = isPortrait ? { w: 315, h: 560 } : { w: 560, h: 315 };
|
||||||
|
embeddedLinkCache.set(originalLink, { link, aspectRatio, type });
|
||||||
|
return { link, aspectRatio, type };
|
||||||
|
}
|
||||||
|
|
||||||
|
const vimeoLink = link.match(RE_VIMEO);
|
||||||
|
if (vimeoLink?.[1]) {
|
||||||
|
const target = vimeoLink?.[1];
|
||||||
|
const warning = !/^\d+$/.test(target)
|
||||||
|
? t("toast.unrecognizedLinkFormat")
|
||||||
|
: undefined;
|
||||||
|
type = "video";
|
||||||
|
link = `https://player.vimeo.com/video/${target}?api=1`;
|
||||||
|
aspectRatio = { w: 560, h: 315 };
|
||||||
|
//warning deliberately ommited so it is displayed only once per link
|
||||||
|
//same link next time will be served from cache
|
||||||
|
embeddedLinkCache.set(originalLink, { link, aspectRatio, type });
|
||||||
|
return { link, aspectRatio, type, warning };
|
||||||
|
}
|
||||||
|
|
||||||
|
const figmaLink = link.match(RE_FIGMA);
|
||||||
|
if (figmaLink) {
|
||||||
|
type = "generic";
|
||||||
|
link = `https://www.figma.com/embed?embed_host=share&url=${encodeURIComponent(
|
||||||
|
link,
|
||||||
|
)}`;
|
||||||
|
aspectRatio = { w: 550, h: 550 };
|
||||||
|
embeddedLinkCache.set(originalLink, { link, aspectRatio, type });
|
||||||
|
return { link, aspectRatio, type };
|
||||||
|
}
|
||||||
|
|
||||||
|
const valLink = link.match(RE_VALTOWN);
|
||||||
|
if (valLink) {
|
||||||
|
link =
|
||||||
|
valLink[1] === "embed" ? valLink[0] : valLink[0].replace("/v", "/embed");
|
||||||
|
embeddedLinkCache.set(originalLink, { link, aspectRatio, type });
|
||||||
|
return { link, aspectRatio, type };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (RE_TWITTER.test(link)) {
|
||||||
|
let ret: EmbeddedLink;
|
||||||
|
// assume embed code
|
||||||
|
if (/<blockquote/.test(link)) {
|
||||||
|
const srcDoc = createSrcDoc(link);
|
||||||
|
ret = {
|
||||||
|
type: "document",
|
||||||
|
srcdoc: () => srcDoc,
|
||||||
|
aspectRatio: { w: 480, h: 480 },
|
||||||
|
};
|
||||||
|
// assume regular tweet url
|
||||||
|
} else {
|
||||||
|
ret = {
|
||||||
|
type: "document",
|
||||||
|
srcdoc: (theme: string) =>
|
||||||
|
createSrcDoc(
|
||||||
|
`<blockquote class="twitter-tweet" data-dnt="true" data-theme="${theme}"><a href="${link}"></a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>`,
|
||||||
|
),
|
||||||
|
aspectRatio: { w: 480, h: 480 },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
embeddedLinkCache.set(originalLink, ret);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (RE_GH_GIST.test(link)) {
|
||||||
|
let ret: EmbeddedLink;
|
||||||
|
// assume embed code
|
||||||
|
if (/<script>/.test(link)) {
|
||||||
|
const srcDoc = createSrcDoc(link);
|
||||||
|
ret = {
|
||||||
|
type: "document",
|
||||||
|
srcdoc: () => srcDoc,
|
||||||
|
aspectRatio: { w: 550, h: 720 },
|
||||||
|
};
|
||||||
|
// assume regular url
|
||||||
|
} else {
|
||||||
|
ret = {
|
||||||
|
type: "document",
|
||||||
|
srcdoc: () =>
|
||||||
|
createSrcDoc(`
|
||||||
|
<script src="${link}.js"></script>
|
||||||
|
<style type="text/css">
|
||||||
|
* { margin: 0px; }
|
||||||
|
table, .gist { height: 100%; }
|
||||||
|
.gist .gist-file { height: calc(100vh - 2px); padding: 0px; display: grid; grid-template-rows: 1fr auto; }
|
||||||
|
</style>
|
||||||
|
`),
|
||||||
|
aspectRatio: { w: 550, h: 720 },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
embeddedLinkCache.set(link, ret);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
embeddedLinkCache.set(link, { link, aspectRatio, type });
|
||||||
|
return { link, aspectRatio, type };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isEmbeddableOrFrameLabel = (
|
||||||
|
element: NonDeletedExcalidrawElement,
|
||||||
|
): Boolean => {
|
||||||
|
if (isEmbeddableElement(element)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (element.type === "text") {
|
||||||
|
const container = getContainerElement(element);
|
||||||
|
if (container && isEmbeddableElement(container)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createPlaceholderEmbeddableLabel = (
|
||||||
|
element: ExcalidrawEmbeddableElement,
|
||||||
|
): ExcalidrawElement => {
|
||||||
|
const text =
|
||||||
|
!element.link || element?.link === "" ? "Empty Web-Embed" : element.link;
|
||||||
|
const fontSize = Math.max(
|
||||||
|
Math.min(element.width / 2, element.width / text.length),
|
||||||
|
element.width / 30,
|
||||||
|
);
|
||||||
|
const fontFamily = FONT_FAMILY.Helvetica;
|
||||||
|
|
||||||
|
const fontString = getFontString({
|
||||||
|
fontSize,
|
||||||
|
fontFamily,
|
||||||
|
});
|
||||||
|
|
||||||
|
return newTextElement({
|
||||||
|
x: element.x + element.width / 2,
|
||||||
|
y: element.y + element.height / 2,
|
||||||
|
strokeColor:
|
||||||
|
element.strokeColor !== "transparent" ? element.strokeColor : "black",
|
||||||
|
backgroundColor: "transparent",
|
||||||
|
fontFamily,
|
||||||
|
fontSize,
|
||||||
|
text: wrapText(text, fontString, element.width - 20),
|
||||||
|
textAlign: "center",
|
||||||
|
verticalAlign: VERTICAL_ALIGN.MIDDLE,
|
||||||
|
angle: element.angle ?? 0,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const actionSetEmbeddableAsActiveTool = register({
|
||||||
|
name: "setEmbeddableAsActiveTool",
|
||||||
|
trackEvent: { category: "toolbar" },
|
||||||
|
perform: (elements, appState, _, app) => {
|
||||||
|
const nextActiveTool = updateActiveTool(appState, {
|
||||||
|
type: "embeddable",
|
||||||
|
});
|
||||||
|
|
||||||
|
setCursorForShape(app.canvas, {
|
||||||
|
...appState,
|
||||||
|
activeTool: nextActiveTool,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
elements,
|
||||||
|
appState: {
|
||||||
|
...appState,
|
||||||
|
activeTool: updateActiveTool(appState, {
|
||||||
|
type: "embeddable",
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
commitToHistory: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const validateHostname = (
|
||||||
|
url: string,
|
||||||
|
/** using a Set assumes it already contains normalized bare domains */
|
||||||
|
allowedHostnames: Set<string> | string,
|
||||||
|
): boolean => {
|
||||||
|
try {
|
||||||
|
const { hostname } = new URL(url);
|
||||||
|
|
||||||
|
const bareDomain = hostname.replace(/^www\./, "");
|
||||||
|
const bareDomainWithFirstSubdomainWildcarded = bareDomain.replace(
|
||||||
|
/^([^.]+)/,
|
||||||
|
"*",
|
||||||
|
);
|
||||||
|
|
||||||
|
if (allowedHostnames instanceof Set) {
|
||||||
|
return (
|
||||||
|
ALLOWED_DOMAINS.has(bareDomain) ||
|
||||||
|
ALLOWED_DOMAINS.has(bareDomainWithFirstSubdomainWildcarded)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bareDomain === allowedHostnames.replace(/^www\./, "")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const extractSrc = (htmlString: string): string => {
|
||||||
|
const twitterMatch = htmlString.match(RE_TWITTER_EMBED);
|
||||||
|
if (twitterMatch && twitterMatch.length === 2) {
|
||||||
|
return twitterMatch[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
const gistMatch = htmlString.match(RE_GH_GIST_EMBED);
|
||||||
|
if (gistMatch && gistMatch.length === 2) {
|
||||||
|
return gistMatch[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
const match = htmlString.match(RE_GENERIC_EMBED);
|
||||||
|
if (match && match.length === 2) {
|
||||||
|
return match[1];
|
||||||
|
}
|
||||||
|
return htmlString;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const embeddableURLValidator = (
|
||||||
|
url: string | null | undefined,
|
||||||
|
validateEmbeddable: ExcalidrawProps["validateEmbeddable"],
|
||||||
|
): boolean => {
|
||||||
|
if (!url) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (validateEmbeddable != null) {
|
||||||
|
if (typeof validateEmbeddable === "function") {
|
||||||
|
const ret = validateEmbeddable(url);
|
||||||
|
// if return value is undefined, leave validation to default
|
||||||
|
if (typeof ret === "boolean") {
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
} else if (typeof validateEmbeddable === "boolean") {
|
||||||
|
return validateEmbeddable;
|
||||||
|
} else if (validateEmbeddable instanceof RegExp) {
|
||||||
|
return validateEmbeddable.test(url);
|
||||||
|
} else if (Array.isArray(validateEmbeddable)) {
|
||||||
|
for (const domain of validateEmbeddable) {
|
||||||
|
if (domain instanceof RegExp) {
|
||||||
|
if (url.match(domain)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} else if (validateHostname(url, domain)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return validateHostname(url, ALLOWED_DOMAINS);
|
||||||
|
};
|
|
@ -13,6 +13,7 @@ import {
|
||||||
FontFamilyValues,
|
FontFamilyValues,
|
||||||
ExcalidrawTextContainer,
|
ExcalidrawTextContainer,
|
||||||
ExcalidrawFrameElement,
|
ExcalidrawFrameElement,
|
||||||
|
ExcalidrawEmbeddableElement,
|
||||||
} from "../element/types";
|
} from "../element/types";
|
||||||
import {
|
import {
|
||||||
arrayToMap,
|
arrayToMap,
|
||||||
|
@ -130,6 +131,18 @@ export const newElement = (
|
||||||
): NonDeleted<ExcalidrawGenericElement> =>
|
): NonDeleted<ExcalidrawGenericElement> =>
|
||||||
_newElementBase<ExcalidrawGenericElement>(opts.type, opts);
|
_newElementBase<ExcalidrawGenericElement>(opts.type, opts);
|
||||||
|
|
||||||
|
export const newEmbeddableElement = (
|
||||||
|
opts: {
|
||||||
|
type: "embeddable";
|
||||||
|
validated: boolean | undefined;
|
||||||
|
} & ElementConstructorOpts,
|
||||||
|
): NonDeleted<ExcalidrawEmbeddableElement> => {
|
||||||
|
return {
|
||||||
|
..._newElementBase<ExcalidrawEmbeddableElement>("embeddable", opts),
|
||||||
|
validated: opts.validated,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export const newFrameElement = (
|
export const newFrameElement = (
|
||||||
opts: ElementConstructorOpts,
|
opts: ElementConstructorOpts,
|
||||||
): NonDeleted<ExcalidrawFrameElement> => {
|
): NonDeleted<ExcalidrawFrameElement> => {
|
||||||
|
@ -177,7 +190,6 @@ export const newTextElement = (
|
||||||
containerId?: ExcalidrawTextContainer["id"] | null;
|
containerId?: ExcalidrawTextContainer["id"] | null;
|
||||||
lineHeight?: ExcalidrawTextElement["lineHeight"];
|
lineHeight?: ExcalidrawTextElement["lineHeight"];
|
||||||
strokeWidth?: ExcalidrawTextElement["strokeWidth"];
|
strokeWidth?: ExcalidrawTextElement["strokeWidth"];
|
||||||
isFrameName?: boolean;
|
|
||||||
} & ElementConstructorOpts,
|
} & ElementConstructorOpts,
|
||||||
): NonDeleted<ExcalidrawTextElement> => {
|
): NonDeleted<ExcalidrawTextElement> => {
|
||||||
const fontFamily = opts.fontFamily || DEFAULT_FONT_FAMILY;
|
const fontFamily = opts.fontFamily || DEFAULT_FONT_FAMILY;
|
||||||
|
@ -212,7 +224,6 @@ export const newTextElement = (
|
||||||
containerId: opts.containerId || null,
|
containerId: opts.containerId || null,
|
||||||
originalText: text,
|
originalText: text,
|
||||||
lineHeight,
|
lineHeight,
|
||||||
isFrameName: opts.isFrameName || false,
|
|
||||||
},
|
},
|
||||||
{},
|
{},
|
||||||
);
|
);
|
||||||
|
@ -432,7 +443,7 @@ const _deepCopyElement = (val: any, depth: number = 0) => {
|
||||||
// we're not cloning non-array & non-plain-object objects because we
|
// we're not cloning non-array & non-plain-object objects because we
|
||||||
// don't support them on excalidraw elements yet. If we do, we need to make
|
// don't support them on excalidraw elements yet. If we do, we need to make
|
||||||
// sure we start cloning them, so let's warn about it.
|
// sure we start cloning them, so let's warn about it.
|
||||||
if (process.env.NODE_ENV === "development") {
|
if (import.meta.env.DEV) {
|
||||||
if (
|
if (
|
||||||
objectType !== "[object Object]" &&
|
objectType !== "[object Object]" &&
|
||||||
objectType !== "[object Array]" &&
|
objectType !== "[object Array]" &&
|
||||||
|
|
|
@ -7,11 +7,11 @@ export const showSelectedShapeActions = (
|
||||||
elements: readonly NonDeletedExcalidrawElement[],
|
elements: readonly NonDeletedExcalidrawElement[],
|
||||||
) =>
|
) =>
|
||||||
Boolean(
|
Boolean(
|
||||||
(!appState.viewModeEnabled &&
|
!appState.viewModeEnabled &&
|
||||||
appState.activeTool.type !== "custom" &&
|
((appState.activeTool.type !== "custom" &&
|
||||||
(appState.editingElement ||
|
(appState.editingElement ||
|
||||||
(appState.activeTool.type !== "selection" &&
|
(appState.activeTool.type !== "selection" &&
|
||||||
appState.activeTool.type !== "eraser" &&
|
appState.activeTool.type !== "eraser" &&
|
||||||
appState.activeTool.type !== "hand"))) ||
|
appState.activeTool.type !== "hand"))) ||
|
||||||
getSelectedElements(elements, appState).length,
|
getSelectedElements(elements, appState).length),
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,19 +1,32 @@
|
||||||
|
import { vi } from "vitest";
|
||||||
import { getPerfectElementSize } from "./sizeHelpers";
|
import { getPerfectElementSize } from "./sizeHelpers";
|
||||||
import * as constants from "../constants";
|
import * as constants from "../constants";
|
||||||
|
|
||||||
const EPSILON_DIGITS = 3;
|
const EPSILON_DIGITS = 3;
|
||||||
|
// Needed so that we can mock the value of constants which is done in
|
||||||
|
// below tests. In Jest this wasn't needed as global override was possible
|
||||||
|
// but vite doesn't allow that hence we need to mock
|
||||||
|
vi.mock(
|
||||||
|
"../constants.ts",
|
||||||
|
//@ts-ignore
|
||||||
|
async (importOriginal) => {
|
||||||
|
const module: any = await importOriginal();
|
||||||
|
return { ...module };
|
||||||
|
},
|
||||||
|
);
|
||||||
describe("getPerfectElementSize", () => {
|
describe("getPerfectElementSize", () => {
|
||||||
it("should return height:0 if `elementType` is line and locked angle is 0", () => {
|
it("should return height:0 if `elementType` is line and locked angle is 0", () => {
|
||||||
const { height, width } = getPerfectElementSize("line", 149, 10);
|
const { height, width } = getPerfectElementSize("line", 149, 10);
|
||||||
expect(width).toBeCloseTo(149, EPSILON_DIGITS);
|
expect(width).toBeCloseTo(149, EPSILON_DIGITS);
|
||||||
expect(height).toBeCloseTo(0, EPSILON_DIGITS);
|
expect(height).toBeCloseTo(0, EPSILON_DIGITS);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return width:0 if `elementType` is line and locked angle is 90 deg (Math.PI/2)", () => {
|
it("should return width:0 if `elementType` is line and locked angle is 90 deg (Math.PI/2)", () => {
|
||||||
const { height, width } = getPerfectElementSize("line", 10, 140);
|
const { height, width } = getPerfectElementSize("line", 10, 140);
|
||||||
expect(width).toBeCloseTo(0, EPSILON_DIGITS);
|
expect(width).toBeCloseTo(0, EPSILON_DIGITS);
|
||||||
expect(height).toBeCloseTo(140, EPSILON_DIGITS);
|
expect(height).toBeCloseTo(140, EPSILON_DIGITS);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return height:0 if `elementType` is arrow and locked angle is 0", () => {
|
it("should return height:0 if `elementType` is arrow and locked angle is 0", () => {
|
||||||
const { height, width } = getPerfectElementSize("arrow", 200, 20);
|
const { height, width } = getPerfectElementSize("arrow", 200, 20);
|
||||||
expect(width).toBeCloseTo(200, EPSILON_DIGITS);
|
expect(width).toBeCloseTo(200, EPSILON_DIGITS);
|
||||||
|
@ -24,16 +37,19 @@ describe("getPerfectElementSize", () => {
|
||||||
expect(width).toBeCloseTo(0, EPSILON_DIGITS);
|
expect(width).toBeCloseTo(0, EPSILON_DIGITS);
|
||||||
expect(height).toBeCloseTo(100, EPSILON_DIGITS);
|
expect(height).toBeCloseTo(100, EPSILON_DIGITS);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return adjust height to be width * tan(locked angle)", () => {
|
it("should return adjust height to be width * tan(locked angle)", () => {
|
||||||
const { height, width } = getPerfectElementSize("arrow", 120, 185);
|
const { height, width } = getPerfectElementSize("arrow", 120, 185);
|
||||||
expect(width).toBeCloseTo(120, EPSILON_DIGITS);
|
expect(width).toBeCloseTo(120, EPSILON_DIGITS);
|
||||||
expect(height).toBeCloseTo(207.846, EPSILON_DIGITS);
|
expect(height).toBeCloseTo(207.846, EPSILON_DIGITS);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return height equals to width if locked angle is 45 deg", () => {
|
it("should return height equals to width if locked angle is 45 deg", () => {
|
||||||
const { height, width } = getPerfectElementSize("arrow", 135, 145);
|
const { height, width } = getPerfectElementSize("arrow", 135, 145);
|
||||||
expect(width).toBeCloseTo(135, EPSILON_DIGITS);
|
expect(width).toBeCloseTo(135, EPSILON_DIGITS);
|
||||||
expect(height).toBeCloseTo(135, EPSILON_DIGITS);
|
expect(height).toBeCloseTo(135, EPSILON_DIGITS);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return height:0 and width:0 when width and height are 0", () => {
|
it("should return height:0 and width:0 when width and height are 0", () => {
|
||||||
const { height, width } = getPerfectElementSize("arrow", 0, 0);
|
const { height, width } = getPerfectElementSize("arrow", 0, 0);
|
||||||
expect(width).toBeCloseTo(0, EPSILON_DIGITS);
|
expect(width).toBeCloseTo(0, EPSILON_DIGITS);
|
||||||
|
|
|
@ -955,7 +955,7 @@ describe("textWysiwyg", () => {
|
||||||
// should center align horizontally and vertically by default
|
// should center align horizontally and vertically by default
|
||||||
resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 100]);
|
resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 100]);
|
||||||
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
||||||
Array [
|
[
|
||||||
85,
|
85,
|
||||||
4.5,
|
4.5,
|
||||||
]
|
]
|
||||||
|
@ -979,7 +979,7 @@ describe("textWysiwyg", () => {
|
||||||
// should left align horizontally and bottom vertically after resize
|
// should left align horizontally and bottom vertically after resize
|
||||||
resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 100]);
|
resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 100]);
|
||||||
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
||||||
Array [
|
[
|
||||||
15,
|
15,
|
||||||
65,
|
65,
|
||||||
]
|
]
|
||||||
|
@ -1001,7 +1001,7 @@ describe("textWysiwyg", () => {
|
||||||
// should right align horizontally and top vertically after resize
|
// should right align horizontally and top vertically after resize
|
||||||
resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 100]);
|
resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 100]);
|
||||||
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
||||||
Array [
|
[
|
||||||
375,
|
375,
|
||||||
-539,
|
-539,
|
||||||
]
|
]
|
||||||
|
@ -1279,7 +1279,7 @@ describe("textWysiwyg", () => {
|
||||||
fireEvent.click(screen.getByTitle("Left"));
|
fireEvent.click(screen.getByTitle("Left"));
|
||||||
fireEvent.click(screen.getByTitle("Align top"));
|
fireEvent.click(screen.getByTitle("Align top"));
|
||||||
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
||||||
Array [
|
[
|
||||||
15,
|
15,
|
||||||
25,
|
25,
|
||||||
]
|
]
|
||||||
|
@ -1290,7 +1290,7 @@ describe("textWysiwyg", () => {
|
||||||
fireEvent.click(screen.getByTitle("Center"));
|
fireEvent.click(screen.getByTitle("Center"));
|
||||||
fireEvent.click(screen.getByTitle("Align top"));
|
fireEvent.click(screen.getByTitle("Align top"));
|
||||||
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
||||||
Array [
|
[
|
||||||
30,
|
30,
|
||||||
25,
|
25,
|
||||||
]
|
]
|
||||||
|
@ -1302,7 +1302,7 @@ describe("textWysiwyg", () => {
|
||||||
fireEvent.click(screen.getByTitle("Align top"));
|
fireEvent.click(screen.getByTitle("Align top"));
|
||||||
|
|
||||||
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
||||||
Array [
|
[
|
||||||
45,
|
45,
|
||||||
25,
|
25,
|
||||||
]
|
]
|
||||||
|
@ -1313,7 +1313,7 @@ describe("textWysiwyg", () => {
|
||||||
fireEvent.click(screen.getByTitle("Center vertically"));
|
fireEvent.click(screen.getByTitle("Center vertically"));
|
||||||
fireEvent.click(screen.getByTitle("Left"));
|
fireEvent.click(screen.getByTitle("Left"));
|
||||||
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
||||||
Array [
|
[
|
||||||
15,
|
15,
|
||||||
45,
|
45,
|
||||||
]
|
]
|
||||||
|
@ -1325,7 +1325,7 @@ describe("textWysiwyg", () => {
|
||||||
fireEvent.click(screen.getByTitle("Center vertically"));
|
fireEvent.click(screen.getByTitle("Center vertically"));
|
||||||
|
|
||||||
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
||||||
Array [
|
[
|
||||||
30,
|
30,
|
||||||
45,
|
45,
|
||||||
]
|
]
|
||||||
|
@ -1337,7 +1337,7 @@ describe("textWysiwyg", () => {
|
||||||
fireEvent.click(screen.getByTitle("Center vertically"));
|
fireEvent.click(screen.getByTitle("Center vertically"));
|
||||||
|
|
||||||
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
||||||
Array [
|
[
|
||||||
45,
|
45,
|
||||||
45,
|
45,
|
||||||
]
|
]
|
||||||
|
@ -1349,7 +1349,7 @@ describe("textWysiwyg", () => {
|
||||||
fireEvent.click(screen.getByTitle("Align bottom"));
|
fireEvent.click(screen.getByTitle("Align bottom"));
|
||||||
|
|
||||||
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
||||||
Array [
|
[
|
||||||
15,
|
15,
|
||||||
65,
|
65,
|
||||||
]
|
]
|
||||||
|
@ -1360,7 +1360,7 @@ describe("textWysiwyg", () => {
|
||||||
fireEvent.click(screen.getByTitle("Center"));
|
fireEvent.click(screen.getByTitle("Center"));
|
||||||
fireEvent.click(screen.getByTitle("Align bottom"));
|
fireEvent.click(screen.getByTitle("Align bottom"));
|
||||||
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
||||||
Array [
|
[
|
||||||
30,
|
30,
|
||||||
65,
|
65,
|
||||||
]
|
]
|
||||||
|
@ -1371,7 +1371,7 @@ describe("textWysiwyg", () => {
|
||||||
fireEvent.click(screen.getByTitle("Right"));
|
fireEvent.click(screen.getByTitle("Right"));
|
||||||
fireEvent.click(screen.getByTitle("Align bottom"));
|
fireEvent.click(screen.getByTitle("Align bottom"));
|
||||||
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
||||||
Array [
|
[
|
||||||
45,
|
45,
|
||||||
65,
|
65,
|
||||||
]
|
]
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { MarkNonNullable } from "../utility-types";
|
||||||
import {
|
import {
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
ExcalidrawTextElement,
|
ExcalidrawTextElement,
|
||||||
|
ExcalidrawEmbeddableElement,
|
||||||
ExcalidrawLinearElement,
|
ExcalidrawLinearElement,
|
||||||
ExcalidrawBindableElement,
|
ExcalidrawBindableElement,
|
||||||
ExcalidrawGenericElement,
|
ExcalidrawGenericElement,
|
||||||
|
@ -24,7 +25,8 @@ export const isGenericElement = (
|
||||||
(element.type === "selection" ||
|
(element.type === "selection" ||
|
||||||
element.type === "rectangle" ||
|
element.type === "rectangle" ||
|
||||||
element.type === "diamond" ||
|
element.type === "diamond" ||
|
||||||
element.type === "ellipse")
|
element.type === "ellipse" ||
|
||||||
|
element.type === "embeddable")
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -40,6 +42,12 @@ export const isImageElement = (
|
||||||
return !!element && element.type === "image";
|
return !!element && element.type === "image";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const isEmbeddableElement = (
|
||||||
|
element: ExcalidrawElement | null | undefined,
|
||||||
|
): element is ExcalidrawEmbeddableElement => {
|
||||||
|
return !!element && element.type === "embeddable";
|
||||||
|
};
|
||||||
|
|
||||||
export const isTextElement = (
|
export const isTextElement = (
|
||||||
element: ExcalidrawElement | null,
|
element: ExcalidrawElement | null,
|
||||||
): element is ExcalidrawTextElement => {
|
): element is ExcalidrawTextElement => {
|
||||||
|
@ -112,6 +120,7 @@ export const isBindableElement = (
|
||||||
element.type === "diamond" ||
|
element.type === "diamond" ||
|
||||||
element.type === "ellipse" ||
|
element.type === "ellipse" ||
|
||||||
element.type === "image" ||
|
element.type === "image" ||
|
||||||
|
element.type === "embeddable" ||
|
||||||
(element.type === "text" && !element.containerId))
|
(element.type === "text" && !element.containerId))
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -135,6 +144,7 @@ export const isExcalidrawElement = (element: any): boolean => {
|
||||||
element?.type === "text" ||
|
element?.type === "text" ||
|
||||||
element?.type === "diamond" ||
|
element?.type === "diamond" ||
|
||||||
element?.type === "rectangle" ||
|
element?.type === "rectangle" ||
|
||||||
|
element?.type === "embeddable" ||
|
||||||
element?.type === "ellipse" ||
|
element?.type === "ellipse" ||
|
||||||
element?.type === "arrow" ||
|
element?.type === "arrow" ||
|
||||||
element?.type === "freedraw" ||
|
element?.type === "freedraw" ||
|
||||||
|
@ -162,7 +172,8 @@ export const isBoundToContainer = (
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isUsingAdaptiveRadius = (type: string) => type === "rectangle";
|
export const isUsingAdaptiveRadius = (type: string) =>
|
||||||
|
type === "rectangle" || type === "embeddable";
|
||||||
|
|
||||||
export const isUsingProportionalRadius = (type: string) =>
|
export const isUsingProportionalRadius = (type: string) =>
|
||||||
type === "line" || type === "arrow" || type === "diamond";
|
type === "line" || type === "arrow" || type === "diamond";
|
||||||
|
@ -193,17 +204,13 @@ export const canApplyRoundnessTypeToElement = (
|
||||||
export const getDefaultRoundnessTypeForElement = (
|
export const getDefaultRoundnessTypeForElement = (
|
||||||
element: ExcalidrawElement,
|
element: ExcalidrawElement,
|
||||||
) => {
|
) => {
|
||||||
if (
|
if (isUsingProportionalRadius(element.type)) {
|
||||||
element.type === "arrow" ||
|
|
||||||
element.type === "line" ||
|
|
||||||
element.type === "diamond"
|
|
||||||
) {
|
|
||||||
return {
|
return {
|
||||||
type: ROUNDNESS.PROPORTIONAL_RADIUS,
|
type: ROUNDNESS.PROPORTIONAL_RADIUS,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (element.type === "rectangle") {
|
if (isUsingAdaptiveRadius(element.type)) {
|
||||||
return {
|
return {
|
||||||
type: ROUNDNESS.ADAPTIVE_RADIUS,
|
type: ROUNDNESS.ADAPTIVE_RADIUS,
|
||||||
};
|
};
|
||||||
|
|
|
@ -84,6 +84,19 @@ export type ExcalidrawEllipseElement = _ExcalidrawElementBase & {
|
||||||
type: "ellipse";
|
type: "ellipse";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ExcalidrawEmbeddableElement = _ExcalidrawElementBase &
|
||||||
|
Readonly<{
|
||||||
|
/**
|
||||||
|
* indicates whether the embeddable src (url) has been validated for rendering.
|
||||||
|
* nullish value indicates that the validation is pending. We reset the
|
||||||
|
* value on each restore (or url change) so that we can guarantee
|
||||||
|
* the validation came from a trusted source (the editor). Also because we
|
||||||
|
* may not have access to host-app supplied url validator during restore.
|
||||||
|
*/
|
||||||
|
validated?: boolean;
|
||||||
|
type: "embeddable";
|
||||||
|
}>;
|
||||||
|
|
||||||
export type ExcalidrawImageElement = _ExcalidrawElementBase &
|
export type ExcalidrawImageElement = _ExcalidrawElementBase &
|
||||||
Readonly<{
|
Readonly<{
|
||||||
type: "image";
|
type: "image";
|
||||||
|
@ -110,6 +123,7 @@ export type ExcalidrawFrameElement = _ExcalidrawElementBase & {
|
||||||
export type ExcalidrawGenericElement =
|
export type ExcalidrawGenericElement =
|
||||||
| ExcalidrawSelectionElement
|
| ExcalidrawSelectionElement
|
||||||
| ExcalidrawRectangleElement
|
| ExcalidrawRectangleElement
|
||||||
|
| ExcalidrawEmbeddableElement
|
||||||
| ExcalidrawDiamondElement
|
| ExcalidrawDiamondElement
|
||||||
| ExcalidrawEllipseElement;
|
| ExcalidrawEllipseElement;
|
||||||
|
|
||||||
|
@ -156,6 +170,7 @@ export type ExcalidrawBindableElement =
|
||||||
| ExcalidrawEllipseElement
|
| ExcalidrawEllipseElement
|
||||||
| ExcalidrawTextElement
|
| ExcalidrawTextElement
|
||||||
| ExcalidrawImageElement
|
| ExcalidrawImageElement
|
||||||
|
| ExcalidrawEmbeddableElement
|
||||||
| ExcalidrawFrameElement;
|
| ExcalidrawFrameElement;
|
||||||
|
|
||||||
export type ExcalidrawTextContainer =
|
export type ExcalidrawTextContainer =
|
||||||
|
|
|
@ -171,10 +171,7 @@ class Collab extends PureComponent<Props, CollabState> {
|
||||||
|
|
||||||
appJotaiStore.set(collabAPIAtom, collabAPI);
|
appJotaiStore.set(collabAPIAtom, collabAPI);
|
||||||
|
|
||||||
if (
|
if (import.meta.env.MODE === ENV.TEST || import.meta.env.DEV) {
|
||||||
process.env.NODE_ENV === ENV.TEST ||
|
|
||||||
process.env.NODE_ENV === ENV.DEVELOPMENT
|
|
||||||
) {
|
|
||||||
window.collab = window.collab || ({} as Window["collab"]);
|
window.collab = window.collab || ({} as Window["collab"]);
|
||||||
Object.defineProperties(window, {
|
Object.defineProperties(window, {
|
||||||
collab: {
|
collab: {
|
||||||
|
@ -333,7 +330,7 @@ class Collab extends PureComponent<Props, CollabState> {
|
||||||
* Indicates whether to fetch files that are errored or pending and older
|
* Indicates whether to fetch files that are errored or pending and older
|
||||||
* than 10 seconds.
|
* than 10 seconds.
|
||||||
*
|
*
|
||||||
* Use this as a machanism to fetch files which may be ok but for some
|
* Use this as a mechanism to fetch files which may be ok but for some
|
||||||
* reason their status was not updated correctly.
|
* reason their status was not updated correctly.
|
||||||
*/
|
*/
|
||||||
forceFetchFiles?: boolean;
|
forceFetchFiles?: boolean;
|
||||||
|
@ -860,10 +857,7 @@ declare global {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (import.meta.env.MODE === ENV.TEST || import.meta.env.DEV) {
|
||||||
process.env.NODE_ENV === ENV.TEST ||
|
|
||||||
process.env.NODE_ENV === ENV.DEVELOPMENT
|
|
||||||
) {
|
|
||||||
window.collab = window.collab || ({} as Window["collab"]);
|
window.collab = window.collab || ({} as Window["collab"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -19,7 +19,9 @@ export const AppWelcomeScreen: React.FC<{
|
||||||
return (
|
return (
|
||||||
<a
|
<a
|
||||||
style={{ pointerEvents: "all" }}
|
style={{ pointerEvents: "all" }}
|
||||||
href={`${process.env.REACT_APP_PLUS_APP}?utm_source=excalidraw&utm_medium=app&utm_content=welcomeScreenSignedInUser`}
|
href={`${
|
||||||
|
import.meta.env.VITE_APP_PLUS_APP
|
||||||
|
}?utm_source=excalidraw&utm_medium=app&utm_content=welcomeScreenSignedInUser`}
|
||||||
key={idx}
|
key={idx}
|
||||||
>
|
>
|
||||||
Excalidraw+
|
Excalidraw+
|
||||||
|
|
|
@ -6,7 +6,9 @@ export const ExcalidrawPlusAppLink = () => {
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<a
|
<a
|
||||||
href={`${process.env.REACT_APP_PLUS_APP}?utm_source=excalidraw&utm_medium=app&utm_content=signedInUserRedirectButton#excalidraw-redirect`}
|
href={`${
|
||||||
|
import.meta.env.VITE_APP_PLUS_APP
|
||||||
|
}?utm_source=excalidraw&utm_medium=app&utm_content=signedInUserRedirectButton#excalidraw-redirect`}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
className="plus-button"
|
className="plus-button"
|
||||||
|
|
|
@ -21,10 +21,12 @@ import { ResolutionType } from "../../utility-types";
|
||||||
|
|
||||||
let FIREBASE_CONFIG: Record<string, any>;
|
let FIREBASE_CONFIG: Record<string, any>;
|
||||||
try {
|
try {
|
||||||
FIREBASE_CONFIG = JSON.parse(process.env.REACT_APP_FIREBASE_CONFIG);
|
FIREBASE_CONFIG = JSON.parse(import.meta.env.VITE_APP_FIREBASE_CONFIG);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.warn(
|
console.warn(
|
||||||
`Error JSON parsing firebase config. Supplied value: ${process.env.REACT_APP_FIREBASE_CONFIG}`,
|
`Error JSON parsing firebase config. Supplied value: ${
|
||||||
|
import.meta.env.VITE_APP_FIREBASE_CONFIG
|
||||||
|
}`,
|
||||||
);
|
);
|
||||||
FIREBASE_CONFIG = {};
|
FIREBASE_CONFIG = {};
|
||||||
}
|
}
|
||||||
|
|
|
@ -47,8 +47,8 @@ export const getSyncableElements = (elements: readonly ExcalidrawElement[]) =>
|
||||||
isSyncableElement(element),
|
isSyncableElement(element),
|
||||||
) as SyncableExcalidrawElement[];
|
) as SyncableExcalidrawElement[];
|
||||||
|
|
||||||
const BACKEND_V2_GET = process.env.REACT_APP_BACKEND_V2_GET_URL;
|
const BACKEND_V2_GET = import.meta.env.VITE_APP_BACKEND_V2_GET_URL;
|
||||||
const BACKEND_V2_POST = process.env.REACT_APP_BACKEND_V2_POST_URL;
|
const BACKEND_V2_POST = import.meta.env.VITE_APP_BACKEND_V2_POST_URL;
|
||||||
|
|
||||||
const generateRoomId = async () => {
|
const generateRoomId = async () => {
|
||||||
const buffer = new Uint8Array(ROOM_ID_BYTES);
|
const buffer = new Uint8Array(ROOM_ID_BYTES);
|
||||||
|
@ -67,16 +67,16 @@ export const getCollabServer = async (): Promise<{
|
||||||
url: string;
|
url: string;
|
||||||
polling: boolean;
|
polling: boolean;
|
||||||
}> => {
|
}> => {
|
||||||
if (process.env.REACT_APP_WS_SERVER_URL) {
|
if (import.meta.env.VITE_APP_WS_SERVER_URL) {
|
||||||
return {
|
return {
|
||||||
url: process.env.REACT_APP_WS_SERVER_URL,
|
url: import.meta.env.VITE_APP_WS_SERVER_URL,
|
||||||
polling: true,
|
polling: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const resp = await fetch(
|
const resp = await fetch(
|
||||||
`${process.env.REACT_APP_PORTAL_URL}/collab-server`,
|
`${import.meta.env.VITE_APP_PORTAL_URL}/collab-server`,
|
||||||
);
|
);
|
||||||
return await resp.json();
|
return await resp.json();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
@ -100,6 +100,20 @@ polyfill();
|
||||||
|
|
||||||
window.EXCALIDRAW_THROTTLE_RENDER = true;
|
window.EXCALIDRAW_THROTTLE_RENDER = true;
|
||||||
|
|
||||||
|
let isSelfEmbedding = false;
|
||||||
|
|
||||||
|
if (window.self !== window.top) {
|
||||||
|
try {
|
||||||
|
const parentUrl = new URL(document.referrer);
|
||||||
|
const currentUrl = new URL(window.location.href);
|
||||||
|
if (parentUrl.origin === currentUrl.origin) {
|
||||||
|
isSelfEmbedding = true;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const languageDetector = new LanguageDetector();
|
const languageDetector = new LanguageDetector();
|
||||||
languageDetector.init({
|
languageDetector.init({
|
||||||
languageUtils: {},
|
languageUtils: {},
|
||||||
|
@ -518,7 +532,9 @@ const ExcalidrawWrapper = () => {
|
||||||
|
|
||||||
const [theme, setTheme] = useState<Theme>(
|
const [theme, setTheme] = useState<Theme>(
|
||||||
() =>
|
() =>
|
||||||
localStorage.getItem(STORAGE_KEYS.LOCAL_STORAGE_THEME) ||
|
(localStorage.getItem(
|
||||||
|
STORAGE_KEYS.LOCAL_STORAGE_THEME,
|
||||||
|
) as Theme | null) ||
|
||||||
// FIXME migration from old LS scheme. Can be removed later. #5660
|
// FIXME migration from old LS scheme. Can be removed later. #5660
|
||||||
importFromLocalStorage().appState?.theme ||
|
importFromLocalStorage().appState?.theme ||
|
||||||
THEME.LIGHT,
|
THEME.LIGHT,
|
||||||
|
@ -641,6 +657,25 @@ const ExcalidrawWrapper = () => {
|
||||||
|
|
||||||
const isOffline = useAtomValue(isOfflineAtom);
|
const isOffline = useAtomValue(isOfflineAtom);
|
||||||
|
|
||||||
|
// browsers generally prevent infinite self-embedding, there are
|
||||||
|
// cases where it still happens, and while we disallow self-embedding
|
||||||
|
// by not whitelisting our own origin, this serves as an additional guard
|
||||||
|
if (isSelfEmbedding) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
textAlign: "center",
|
||||||
|
height: "100%",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<h1>I'm not a pretzel!</h1>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{ height: "100%" }}
|
style={{ height: "100%" }}
|
||||||
|
|
|
@ -1,31 +0,0 @@
|
||||||
import { register as registerServiceWorker } from "../serviceWorkerRegistration";
|
|
||||||
import { EVENT } from "../constants";
|
|
||||||
|
|
||||||
// On Apple mobile devices add the proprietary app icon and splashscreen markup.
|
|
||||||
// No one should have to do this manually, and eventually this annoyance will
|
|
||||||
// go away once https://bugs.webkit.org/show_bug.cgi?id=183937 is fixed.
|
|
||||||
if (
|
|
||||||
/\b(iPad|iPhone|iPod|Safari)\b/.test(navigator.userAgent) &&
|
|
||||||
!matchMedia("(display-mode: standalone)").matches
|
|
||||||
) {
|
|
||||||
import(/* webpackChunkName: "pwacompat" */ "pwacompat");
|
|
||||||
}
|
|
||||||
|
|
||||||
registerServiceWorker({
|
|
||||||
onUpdate: (registration) => {
|
|
||||||
const waitingServiceWorker = registration.waiting;
|
|
||||||
if (waitingServiceWorker) {
|
|
||||||
waitingServiceWorker.addEventListener(
|
|
||||||
EVENT.STATE_CHANGE,
|
|
||||||
(event: Event) => {
|
|
||||||
const target = event.target as ServiceWorker;
|
|
||||||
const state = target.state as ServiceWorkerState;
|
|
||||||
if (state === "activated") {
|
|
||||||
window.location.reload();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
waitingServiceWorker.postMessage({ type: "SKIP_WAITING" });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
|
@ -7,7 +7,7 @@ const SentryEnvHostnameMap: { [key: string]: string } = {
|
||||||
};
|
};
|
||||||
|
|
||||||
const REACT_APP_DISABLE_SENTRY =
|
const REACT_APP_DISABLE_SENTRY =
|
||||||
process.env.REACT_APP_DISABLE_SENTRY === "true";
|
import.meta.env.VITE_APP_DISABLE_SENTRY === "true";
|
||||||
|
|
||||||
// Disable Sentry locally or inside the Docker to avoid noise/respect privacy
|
// Disable Sentry locally or inside the Docker to avoid noise/respect privacy
|
||||||
const onlineEnv =
|
const onlineEnv =
|
||||||
|
@ -21,7 +21,7 @@ Sentry.init({
|
||||||
? "https://7bfc596a5bf945eda6b660d3015a5460@sentry.io/5179260"
|
? "https://7bfc596a5bf945eda6b660d3015a5460@sentry.io/5179260"
|
||||||
: undefined,
|
: undefined,
|
||||||
environment: onlineEnv ? SentryEnvHostnameMap[onlineEnv] : undefined,
|
environment: onlineEnv ? SentryEnvHostnameMap[onlineEnv] : undefined,
|
||||||
release: process.env.REACT_APP_GIT_SHA,
|
release: import.meta.env.VITE_APP_GIT_SHA,
|
||||||
ignoreErrors: [
|
ignoreErrors: [
|
||||||
"undefined is not an object (evaluating 'window.__pad.performLoop')", // Only happens on Safari, but spams our servers. Doesn't break anything
|
"undefined is not an object (evaluating 'window.__pad.performLoop')", // Only happens on Safari, but spams our servers. Doesn't break anything
|
||||||
],
|
],
|
||||||
|
|
16
src/global.d.ts
vendored
16
src/global.d.ts
vendored
|
@ -38,16 +38,6 @@ interface CanvasRenderingContext2D {
|
||||||
) => void;
|
) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://github.com/facebook/create-react-app/blob/ddcb7d5/packages/react-scripts/lib/react-app.d.ts
|
|
||||||
declare namespace NodeJS {
|
|
||||||
interface ProcessEnv {
|
|
||||||
readonly REACT_APP_BACKEND_V2_GET_URL: string;
|
|
||||||
readonly REACT_APP_BACKEND_V2_POST_URL: string;
|
|
||||||
readonly REACT_APP_PORTAL_URL: string;
|
|
||||||
readonly REACT_APP_FIREBASE_CONFIG: string;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Clipboard extends EventTarget {
|
interface Clipboard extends EventTarget {
|
||||||
write(data: any[]): Promise<void>;
|
write(data: any[]): Promise<void>;
|
||||||
}
|
}
|
||||||
|
@ -120,3 +110,9 @@ declare module "image-blob-reduce" {
|
||||||
const reduce: ImageBlobReduce.ImageBlobReduceStatic;
|
const reduce: ImageBlobReduce.ImageBlobReduceStatic;
|
||||||
export = reduce;
|
export = reduce;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
declare namespace jest {
|
||||||
|
interface Expect {
|
||||||
|
toBeNonNaNNumber(): void;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -17,6 +17,7 @@ const exportLibraryItemToSvg = async (elements: LibraryItem["elements"]) => {
|
||||||
viewBackgroundColor: COLOR_PALETTE.white,
|
viewBackgroundColor: COLOR_PALETTE.white,
|
||||||
},
|
},
|
||||||
files: null,
|
files: null,
|
||||||
|
renderEmbeddables: false,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
10
src/i18n.ts
10
src/i18n.ts
|
@ -1,8 +1,8 @@
|
||||||
import fallbackLangData from "./locales/en.json";
|
import fallbackLangData from "./locales/en.json";
|
||||||
import percentages from "./locales/percentages.json";
|
import percentages from "./locales/percentages.json";
|
||||||
import { ENV } from "./constants";
|
|
||||||
import { jotaiScope, jotaiStore } from "./jotai";
|
import { jotaiScope, jotaiStore } from "./jotai";
|
||||||
import { atom, useAtomValue } from "jotai";
|
import { atom, useAtomValue } from "jotai";
|
||||||
|
import { NestedKeyOf } from "./utility-types";
|
||||||
|
|
||||||
const COMPLETION_THRESHOLD = 85;
|
const COMPLETION_THRESHOLD = 85;
|
||||||
|
|
||||||
|
@ -12,6 +12,8 @@ export interface Language {
|
||||||
rtl?: boolean;
|
rtl?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type TranslationKeys = NestedKeyOf<typeof fallbackLangData>;
|
||||||
|
|
||||||
export const defaultLang = { code: "en", label: "English" };
|
export const defaultLang = { code: "en", label: "English" };
|
||||||
|
|
||||||
export const languages: Language[] = [
|
export const languages: Language[] = [
|
||||||
|
@ -71,7 +73,7 @@ export const languages: Language[] = [
|
||||||
];
|
];
|
||||||
|
|
||||||
const TEST_LANG_CODE = "__test__";
|
const TEST_LANG_CODE = "__test__";
|
||||||
if (process.env.NODE_ENV === ENV.DEVELOPMENT) {
|
if (import.meta.env.DEV) {
|
||||||
languages.unshift(
|
languages.unshift(
|
||||||
{ code: TEST_LANG_CODE, label: "test language" },
|
{ code: TEST_LANG_CODE, label: "test language" },
|
||||||
{
|
{
|
||||||
|
@ -123,7 +125,7 @@ const findPartsForData = (data: any, parts: string[]) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const t = (
|
export const t = (
|
||||||
path: string,
|
path: NestedKeyOf<typeof fallbackLangData>,
|
||||||
replacement?: { [key: string]: string | number } | null,
|
replacement?: { [key: string]: string | number } | null,
|
||||||
fallback?: string,
|
fallback?: string,
|
||||||
) => {
|
) => {
|
||||||
|
@ -142,7 +144,7 @@ export const t = (
|
||||||
if (translation === undefined) {
|
if (translation === undefined) {
|
||||||
const errorMessage = `Can't find translation for ${path}`;
|
const errorMessage = `Can't find translation for ${path}`;
|
||||||
// in production, don't blow up the app on a missing translation key
|
// in production, don't blow up the app on a missing translation key
|
||||||
if (process.env.NODE_ENV === "production") {
|
if (import.meta.env.PROD) {
|
||||||
console.warn(errorMessage);
|
console.warn(errorMessage);
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
import { StrictMode } from "react";
|
import { StrictMode } from "react";
|
||||||
import { createRoot } from "react-dom/client";
|
import { createRoot } from "react-dom/client";
|
||||||
import ExcalidrawApp from "./excalidraw-app";
|
import ExcalidrawApp from "./excalidraw-app";
|
||||||
|
import { registerSW } from "virtual:pwa-register";
|
||||||
|
|
||||||
import "./excalidraw-app/pwa";
|
|
||||||
import "./excalidraw-app/sentry";
|
import "./excalidraw-app/sentry";
|
||||||
window.__EXCALIDRAW_SHA__ = process.env.REACT_APP_GIT_SHA;
|
window.__EXCALIDRAW_SHA__ = import.meta.env.VITE_APP_GIT_SHA;
|
||||||
const rootElement = document.getElementById("root")!;
|
const rootElement = document.getElementById("root")!;
|
||||||
const root = createRoot(rootElement);
|
const root = createRoot(rootElement);
|
||||||
|
registerSW();
|
||||||
|
|
||||||
root.render(
|
root.render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
<ExcalidrawApp />
|
<ExcalidrawApp />
|
||||||
|
|
|
@ -65,6 +65,7 @@ export const KEYS = {
|
||||||
Y: "y",
|
Y: "y",
|
||||||
Z: "z",
|
Z: "z",
|
||||||
K: "k",
|
K: "k",
|
||||||
|
W: "w",
|
||||||
|
|
||||||
0: "0",
|
0: "0",
|
||||||
1: "1",
|
1: "1",
|
||||||
|
|
|
@ -109,8 +109,12 @@
|
||||||
"createContainerFromText": "Wrap text in a container",
|
"createContainerFromText": "Wrap text in a container",
|
||||||
"link": {
|
"link": {
|
||||||
"edit": "Edit link",
|
"edit": "Edit link",
|
||||||
|
"editEmbed": "Edit link & embed",
|
||||||
"create": "Create link",
|
"create": "Create link",
|
||||||
"label": "Link"
|
"createEmbed": "Create link & embed",
|
||||||
|
"label": "Link",
|
||||||
|
"labelEmbed": "Link & embed",
|
||||||
|
"empty": "No link is set"
|
||||||
},
|
},
|
||||||
"lineEditor": {
|
"lineEditor": {
|
||||||
"edit": "Edit line",
|
"edit": "Edit line",
|
||||||
|
@ -164,9 +168,11 @@
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"clear": "Clear",
|
"clear": "Clear",
|
||||||
"remove": "Remove",
|
"remove": "Remove",
|
||||||
|
"embed": "Toggle embedding",
|
||||||
"publishLibrary": "Publish",
|
"publishLibrary": "Publish",
|
||||||
"submit": "Submit",
|
"submit": "Submit",
|
||||||
"confirm": "Confirm"
|
"confirm": "Confirm",
|
||||||
|
"embeddableInteractionButton": "Click to interact"
|
||||||
},
|
},
|
||||||
"alerts": {
|
"alerts": {
|
||||||
"clearReset": "This will clear the whole canvas. Are you sure?",
|
"clearReset": "This will clear the whole canvas. Are you sure?",
|
||||||
|
@ -206,6 +212,10 @@
|
||||||
"line2": "This could result in breaking the <bold>Text Elements</bold> in your drawings.",
|
"line2": "This could result in breaking the <bold>Text Elements</bold> in your drawings.",
|
||||||
"line3": "We strongly recommend disabling this setting. You can follow <link>these steps</link> on how to do so.",
|
"line3": "We strongly recommend disabling this setting. You can follow <link>these steps</link> on how to do so.",
|
||||||
"line4": "If disabling this setting doesn't fix the display of text elements, please open an <issueLink>issue</issueLink> on our GitHub, or write us on <discordLink>Discord</discordLink>"
|
"line4": "If disabling this setting doesn't fix the display of text elements, please open an <issueLink>issue</issueLink> on our GitHub, or write us on <discordLink>Discord</discordLink>"
|
||||||
|
},
|
||||||
|
"libraryElementTypeError": {
|
||||||
|
"embeddable": "Embeddable elements cannot be added to the library.",
|
||||||
|
"image": "Support for adding images to the library coming soon!"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
|
@ -224,6 +234,7 @@
|
||||||
"link": "Add/ Update link for a selected shape",
|
"link": "Add/ Update link for a selected shape",
|
||||||
"eraser": "Eraser",
|
"eraser": "Eraser",
|
||||||
"frame": "Frame tool",
|
"frame": "Frame tool",
|
||||||
|
"embeddable": "Web Embed",
|
||||||
"hand": "Hand (panning tool)",
|
"hand": "Hand (panning tool)",
|
||||||
"extraTools": "More tools"
|
"extraTools": "More tools"
|
||||||
},
|
},
|
||||||
|
@ -237,6 +248,7 @@
|
||||||
"linearElement": "Click to start multiple points, drag for single line",
|
"linearElement": "Click to start multiple points, drag for single line",
|
||||||
"freeDraw": "Click and drag, release when you're finished",
|
"freeDraw": "Click and drag, release when you're finished",
|
||||||
"text": "Tip: you can also add text by double-clicking anywhere with the selection tool",
|
"text": "Tip: you can also add text by double-clicking anywhere with the selection tool",
|
||||||
|
"embeddable": "Click-drag to create a website embed",
|
||||||
"text_selected": "Double-click or press ENTER to edit text",
|
"text_selected": "Double-click or press ENTER to edit text",
|
||||||
"text_editing": "Press Escape or CtrlOrCmd+ENTER to finish editing",
|
"text_editing": "Press Escape or CtrlOrCmd+ENTER to finish editing",
|
||||||
"linearElementMulti": "Click on last point or press Escape or Enter to finish",
|
"linearElementMulti": "Click on last point or press Escape or Enter to finish",
|
||||||
|
@ -411,7 +423,9 @@
|
||||||
"fileSavedToFilename": "Saved to {filename}",
|
"fileSavedToFilename": "Saved to {filename}",
|
||||||
"canvas": "canvas",
|
"canvas": "canvas",
|
||||||
"selection": "selection",
|
"selection": "selection",
|
||||||
"pasteAsSingleElement": "Use {{shortcut}} to paste as a single element,\nor paste into an existing text editor"
|
"pasteAsSingleElement": "Use {{shortcut}} to paste as a single element,\nor paste into an existing text editor",
|
||||||
|
"unableToEmbed": "Embedding this url is currently not allowed. Raise an issue on GitHub to request the url whitelisted",
|
||||||
|
"unrecognizedLinkFormat": "The link you embedded does not match the expected format. Please try to paste the 'embed' string provided by the source site"
|
||||||
},
|
},
|
||||||
"colors": {
|
"colors": {
|
||||||
"transparent": "Transparent",
|
"transparent": "Transparent",
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"path": "dist/excalidraw.production.min.js",
|
"path": "dist/excalidraw.production.min.js",
|
||||||
"limit": "285 kB"
|
"limit": "290 kB"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "dist/excalidraw-assets/locales",
|
"path": "dist/excalidraw-assets/locales",
|
||||||
|
|
|
@ -13,8 +13,27 @@ Please add the latest change on the top under the correct section.
|
||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
|
### renderEmbeddable
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
(element: NonDeletedExcalidrawElement, radius: number, appState: UIAppState) => JSX.Element | null;`
|
||||||
|
```
|
||||||
|
|
||||||
|
The renderEmbeddable function allows you to customize the rendering of a JSX component instead of using the default `<iframe>`. By setting props.renderEmbeddable, you can provide a custom implementation for rendering the element.
|
||||||
|
|
||||||
|
#### Parameters:
|
||||||
|
|
||||||
|
- element (NonDeletedExcalidrawElement): The element to be rendered.
|
||||||
|
- radius (number): The calculated border radius in pixels.
|
||||||
|
- appState (UIAppState): The current state of the UI.
|
||||||
|
|
||||||
|
#### Return value:
|
||||||
|
|
||||||
|
JSX.Element | null: The JSX component representing the custom rendering, or null if the default `<iframe>` should be rendered.
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|
||||||
|
- Added [`props.validateEmbeddable`](https://docs.excalidraw.com/docs/@excalidraw/excalidraw/api/props#validateEmbeddable) to customize embeddable src url validation. [#6691](https://github.com/excalidraw/excalidraw/pull/6691)
|
||||||
- Add support for `opts.fitToViewport` and `opts.viewportZoomFactor` in the [`ExcalidrawAPI.scrollToContent`](https://docs.excalidraw.com/docs/@excalidraw/excalidraw/api/props/ref#scrolltocontent) API. [#6581](https://github.com/excalidraw/excalidraw/pull/6581).
|
- Add support for `opts.fitToViewport` and `opts.viewportZoomFactor` in the [`ExcalidrawAPI.scrollToContent`](https://docs.excalidraw.com/docs/@excalidraw/excalidraw/api/props/ref#scrolltocontent) API. [#6581](https://github.com/excalidraw/excalidraw/pull/6581).
|
||||||
- Properly sanitize element `link` urls. [#6728](https://github.com/excalidraw/excalidraw/pull/6728).
|
- Properly sanitize element `link` urls. [#6728](https://github.com/excalidraw/excalidraw/pull/6728).
|
||||||
- Sidebar component now supports tabs — for more detailed description of new behavior and breaking changes, see the linked PR. [#6213](https://github.com/excalidraw/excalidraw/pull/6213)
|
- Sidebar component now supports tabs — for more detailed description of new behavior and breaking changes, see the linked PR. [#6213](https://github.com/excalidraw/excalidraw/pull/6213)
|
||||||
|
|
|
@ -9,9 +9,9 @@ const parseEnvVariables = (filepath) => {
|
||||||
},
|
},
|
||||||
{},
|
{},
|
||||||
);
|
);
|
||||||
envVars.PKG_NAME = JSON.stringify(pkg.name);
|
envVars.VITE_PKG_NAME = JSON.stringify(pkg.name);
|
||||||
envVars.PKG_VERSION = JSON.stringify(pkg.version);
|
envVars.VITE_PKG_VERSION = JSON.stringify(pkg.version);
|
||||||
envVars.IS_EXCALIDRAW_NPM_PACKAGE = JSON.stringify(true);
|
envVars.VITE_IS_EXCALIDRAW_NPM_PACKAGE = JSON.stringify(true);
|
||||||
return envVars;
|
return envVars;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -26,7 +26,7 @@ import {
|
||||||
LibraryItems,
|
LibraryItems,
|
||||||
PointerDownState as ExcalidrawPointerDownState,
|
PointerDownState as ExcalidrawPointerDownState,
|
||||||
} from "../../../types";
|
} from "../../../types";
|
||||||
import { NonDeletedExcalidrawElement } from "../../../element/types";
|
import { NonDeletedExcalidrawElement, Theme } from "../../../element/types";
|
||||||
import { ImportedLibraryData } from "../../../data/types";
|
import { ImportedLibraryData } from "../../../data/types";
|
||||||
import CustomFooter from "./CustomFooter";
|
import CustomFooter from "./CustomFooter";
|
||||||
import MobileFooter from "./MobileFooter";
|
import MobileFooter from "./MobileFooter";
|
||||||
|
@ -97,7 +97,7 @@ export default function App({ appTitle, useCustom, customArgs }: AppProps) {
|
||||||
const [canvasUrl, setCanvasUrl] = useState<string>("");
|
const [canvasUrl, setCanvasUrl] = useState<string>("");
|
||||||
const [exportWithDarkMode, setExportWithDarkMode] = useState(false);
|
const [exportWithDarkMode, setExportWithDarkMode] = useState(false);
|
||||||
const [exportEmbedScene, setExportEmbedScene] = useState(false);
|
const [exportEmbedScene, setExportEmbedScene] = useState(false);
|
||||||
const [theme, setTheme] = useState("light");
|
const [theme, setTheme] = useState<Theme>("light");
|
||||||
const [isCollaborating, setIsCollaborating] = useState(false);
|
const [isCollaborating, setIsCollaborating] = useState(false);
|
||||||
const [commentIcons, setCommentIcons] = useState<{ [id: string]: Comment }>(
|
const [commentIcons, setCommentIcons] = useState<{ [id: string]: Comment }>(
|
||||||
{},
|
{},
|
||||||
|
@ -601,11 +601,7 @@ export default function App({ appTitle, useCustom, customArgs }: AppProps) {
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={theme === "dark"}
|
checked={theme === "dark"}
|
||||||
onChange={() => {
|
onChange={() => {
|
||||||
let newTheme = "light";
|
setTheme(theme === "light" ? "dark" : "light");
|
||||||
if (theme === "light") {
|
|
||||||
newTheme = "dark";
|
|
||||||
}
|
|
||||||
setTheme(newTheme);
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
Switch to Dark Theme
|
Switch to Dark Theme
|
||||||
|
@ -684,11 +680,17 @@ export default function App({ appTitle, useCustom, customArgs }: AppProps) {
|
||||||
gridModeEnabled={gridModeEnabled}
|
gridModeEnabled={gridModeEnabled}
|
||||||
theme={theme}
|
theme={theme}
|
||||||
name="Custom name of drawing"
|
name="Custom name of drawing"
|
||||||
UIOptions={{ canvasActions: { loadScene: false } }}
|
UIOptions={{
|
||||||
|
canvasActions: {
|
||||||
|
loadScene: false,
|
||||||
|
},
|
||||||
|
}}
|
||||||
renderTopRightUI={renderTopRightUI}
|
renderTopRightUI={renderTopRightUI}
|
||||||
onLinkOpen={onLinkOpen}
|
onLinkOpen={onLinkOpen}
|
||||||
onPointerDown={onPointerDown}
|
onPointerDown={onPointerDown}
|
||||||
onScrollChange={rerenderCommentIcons}
|
onScrollChange={rerenderCommentIcons}
|
||||||
|
// allow all urls
|
||||||
|
validateEmbeddable={true}
|
||||||
>
|
>
|
||||||
{excalidrawAPI && (
|
{excalidrawAPI && (
|
||||||
<Footer>
|
<Footer>
|
||||||
|
|
|
@ -42,6 +42,8 @@ const ExcalidrawBase = (props: ExcalidrawProps) => {
|
||||||
onPointerDown,
|
onPointerDown,
|
||||||
onScrollChange,
|
onScrollChange,
|
||||||
children,
|
children,
|
||||||
|
validateEmbeddable,
|
||||||
|
renderEmbeddable,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const canvasActions = props.UIOptions?.canvasActions;
|
const canvasActions = props.UIOptions?.canvasActions;
|
||||||
|
@ -115,6 +117,8 @@ const ExcalidrawBase = (props: ExcalidrawProps) => {
|
||||||
onLinkOpen={onLinkOpen}
|
onLinkOpen={onLinkOpen}
|
||||||
onPointerDown={onPointerDown}
|
onPointerDown={onPointerDown}
|
||||||
onScrollChange={onScrollChange}
|
onScrollChange={onScrollChange}
|
||||||
|
validateEmbeddable={validateEmbeddable}
|
||||||
|
renderEmbeddable={renderEmbeddable}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</App>
|
</App>
|
||||||
|
|
|
@ -52,16 +52,19 @@
|
||||||
"@babel/preset-env": "7.18.6",
|
"@babel/preset-env": "7.18.6",
|
||||||
"@babel/preset-react": "7.18.6",
|
"@babel/preset-react": "7.18.6",
|
||||||
"@babel/preset-typescript": "7.18.6",
|
"@babel/preset-typescript": "7.18.6",
|
||||||
|
"@size-limit/preset-big-lib": "8.2.6",
|
||||||
"autoprefixer": "10.4.7",
|
"autoprefixer": "10.4.7",
|
||||||
"babel-loader": "8.2.5",
|
"babel-loader": "8.2.5",
|
||||||
"babel-plugin-transform-class-properties": "6.24.1",
|
"babel-plugin-transform-class-properties": "6.24.1",
|
||||||
"cross-env": "7.0.3",
|
"cross-env": "7.0.3",
|
||||||
"css-loader": "6.7.1",
|
"css-loader": "6.7.1",
|
||||||
"dotenv": "16.0.1",
|
"dotenv": "16.0.1",
|
||||||
|
"import-meta-loader": "1.1.0",
|
||||||
"mini-css-extract-plugin": "2.6.1",
|
"mini-css-extract-plugin": "2.6.1",
|
||||||
"postcss-loader": "7.0.1",
|
"postcss-loader": "7.0.1",
|
||||||
"sass-loader": "13.0.2",
|
"sass-loader": "13.0.2",
|
||||||
"size-limit": "8.2.4",
|
"size-limit": "8.2.4",
|
||||||
|
"style-loader": "3.3.3",
|
||||||
"terser-webpack-plugin": "5.3.3",
|
"terser-webpack-plugin": "5.3.3",
|
||||||
"ts-loader": "9.3.1",
|
"ts-loader": "9.3.1",
|
||||||
"typescript": "4.9.4",
|
"typescript": "4.9.4",
|
||||||
|
|
|
@ -4,5 +4,5 @@ if (process.env.NODE_ENV !== ENV.TEST) {
|
||||||
/* global __webpack_public_path__:writable */
|
/* global __webpack_public_path__:writable */
|
||||||
__webpack_public_path__ =
|
__webpack_public_path__ =
|
||||||
window.EXCALIDRAW_ASSET_PATH ||
|
window.EXCALIDRAW_ASSET_PATH ||
|
||||||
`https://unpkg.com/${process.env.PKG_NAME}@${process.env.PKG_VERSION}/dist/`;
|
`https://unpkg.com/${process.env.VITE_PKG_NAME}@${process.env.VITE_PKG_VERSION}/dist/`;
|
||||||
}
|
}
|
||||||
|
|
|
@ -47,6 +47,9 @@ module.exports = {
|
||||||
exclude:
|
exclude:
|
||||||
/node_modules\/(?!(browser-fs-access|canvas-roundrect-polyfill))/,
|
/node_modules\/(?!(browser-fs-access|canvas-roundrect-polyfill))/,
|
||||||
use: [
|
use: [
|
||||||
|
{
|
||||||
|
loader: "import-meta-loader",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
loader: "ts-loader",
|
loader: "ts-loader",
|
||||||
options: {
|
options: {
|
||||||
|
|
|
@ -50,6 +50,9 @@ module.exports = {
|
||||||
/node_modules\/(?!(browser-fs-access|canvas-roundrect-polyfill))/,
|
/node_modules\/(?!(browser-fs-access|canvas-roundrect-polyfill))/,
|
||||||
|
|
||||||
use: [
|
use: [
|
||||||
|
{
|
||||||
|
loader: "import-meta-loader",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
loader: "ts-loader",
|
loader: "ts-loader",
|
||||||
options: {
|
options: {
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -178,8 +178,10 @@ export const exportToSvg = async ({
|
||||||
appState = getDefaultAppState(),
|
appState = getDefaultAppState(),
|
||||||
files = {},
|
files = {},
|
||||||
exportPadding,
|
exportPadding,
|
||||||
|
renderEmbeddables,
|
||||||
}: Omit<ExportOpts, "getDimensions"> & {
|
}: Omit<ExportOpts, "getDimensions"> & {
|
||||||
exportPadding?: number;
|
exportPadding?: number;
|
||||||
|
renderEmbeddables?: boolean;
|
||||||
}): Promise<SVGSVGElement> => {
|
}): Promise<SVGSVGElement> => {
|
||||||
const { elements: restoredElements, appState: restoredAppState } = restore(
|
const { elements: restoredElements, appState: restoredAppState } = restore(
|
||||||
{ elements, appState },
|
{ elements, appState },
|
||||||
|
@ -197,6 +199,7 @@ export const exportToSvg = async ({
|
||||||
exportAppState,
|
exportAppState,
|
||||||
files,
|
files,
|
||||||
{
|
{
|
||||||
|
renderEmbeddables,
|
||||||
// NOTE as long as we're using the Scene hack, we need to ensure
|
// NOTE as long as we're using the Scene hack, we need to ensure
|
||||||
// we pass the original, uncloned elements when serializing
|
// we pass the original, uncloned elements when serializing
|
||||||
// so that we keep ids stable. Hence adding the serializeAsJSON helper
|
// so that we keep ids stable. Hence adding the serializeAsJSON helper
|
||||||
|
|
|
@ -27,7 +27,13 @@ import { RoughSVG } from "roughjs/bin/svg";
|
||||||
import { RoughGenerator } from "roughjs/bin/generator";
|
import { RoughGenerator } from "roughjs/bin/generator";
|
||||||
|
|
||||||
import { RenderConfig } from "../scene/types";
|
import { RenderConfig } from "../scene/types";
|
||||||
import { distance, getFontString, getFontFamilyString, isRTL } from "../utils";
|
import {
|
||||||
|
distance,
|
||||||
|
getFontString,
|
||||||
|
getFontFamilyString,
|
||||||
|
isRTL,
|
||||||
|
isTransparent,
|
||||||
|
} from "../utils";
|
||||||
import { getCornerRadius, isPathALoop, isRightAngle } from "../math";
|
import { getCornerRadius, isPathALoop, isRightAngle } from "../math";
|
||||||
import rough from "roughjs/bin/rough";
|
import rough from "roughjs/bin/rough";
|
||||||
import { AppState, BinaryFiles, Zoom } from "../types";
|
import { AppState, BinaryFiles, Zoom } from "../types";
|
||||||
|
@ -49,8 +55,12 @@ import {
|
||||||
getBoundTextMaxWidth,
|
getBoundTextMaxWidth,
|
||||||
} from "../element/textElement";
|
} from "../element/textElement";
|
||||||
import { LinearElementEditor } from "../element/linearElementEditor";
|
import { LinearElementEditor } from "../element/linearElementEditor";
|
||||||
|
import {
|
||||||
|
createPlaceholderEmbeddableLabel,
|
||||||
|
getEmbedLink,
|
||||||
|
} from "../element/embeddable";
|
||||||
import { getContainingFrame } from "../frame";
|
import { getContainingFrame } from "../frame";
|
||||||
import { normalizeLink } from "../data/url";
|
import { normalizeLink, toValidURL } from "../data/url";
|
||||||
|
|
||||||
// using a stronger invert (100% vs our regular 93%) and saturate
|
// using a stronger invert (100% vs our regular 93%) and saturate
|
||||||
// as a temp hack to make images in dark theme look closer to original
|
// as a temp hack to make images in dark theme look closer to original
|
||||||
|
@ -262,6 +272,7 @@ const drawElementOnCanvas = (
|
||||||
((getContainingFrame(element)?.opacity ?? 100) * element.opacity) / 10000;
|
((getContainingFrame(element)?.opacity ?? 100) * element.opacity) / 10000;
|
||||||
switch (element.type) {
|
switch (element.type) {
|
||||||
case "rectangle":
|
case "rectangle":
|
||||||
|
case "embeddable":
|
||||||
case "diamond":
|
case "diamond":
|
||||||
case "ellipse": {
|
case "ellipse": {
|
||||||
context.lineJoin = "round";
|
context.lineJoin = "round";
|
||||||
|
@ -427,13 +438,13 @@ export const generateRoughOptions = (
|
||||||
|
|
||||||
switch (element.type) {
|
switch (element.type) {
|
||||||
case "rectangle":
|
case "rectangle":
|
||||||
|
case "embeddable":
|
||||||
case "diamond":
|
case "diamond":
|
||||||
case "ellipse": {
|
case "ellipse": {
|
||||||
options.fillStyle = element.fillStyle;
|
options.fillStyle = element.fillStyle;
|
||||||
options.fill =
|
options.fill = isTransparent(element.backgroundColor)
|
||||||
element.backgroundColor === "transparent"
|
? undefined
|
||||||
? undefined
|
: element.backgroundColor;
|
||||||
: element.backgroundColor;
|
|
||||||
if (element.type === "ellipse") {
|
if (element.type === "ellipse") {
|
||||||
options.curveFitting = 1;
|
options.curveFitting = 1;
|
||||||
}
|
}
|
||||||
|
@ -458,6 +469,26 @@ export const generateRoughOptions = (
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const modifyEmbeddableForRoughOptions = (
|
||||||
|
element: NonDeletedExcalidrawElement,
|
||||||
|
isExporting: boolean,
|
||||||
|
) => {
|
||||||
|
if (
|
||||||
|
element.type === "embeddable" &&
|
||||||
|
(isExporting || !element.validated) &&
|
||||||
|
isTransparent(element.backgroundColor) &&
|
||||||
|
isTransparent(element.strokeColor)
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
...element,
|
||||||
|
roughness: 0,
|
||||||
|
backgroundColor: "#d3d3d3",
|
||||||
|
fillStyle: "solid",
|
||||||
|
} as const;
|
||||||
|
}
|
||||||
|
return element;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates the element's shape and puts it into the cache.
|
* Generates the element's shape and puts it into the cache.
|
||||||
* @param element
|
* @param element
|
||||||
|
@ -466,8 +497,9 @@ export const generateRoughOptions = (
|
||||||
const generateElementShape = (
|
const generateElementShape = (
|
||||||
element: NonDeletedExcalidrawElement,
|
element: NonDeletedExcalidrawElement,
|
||||||
generator: RoughGenerator,
|
generator: RoughGenerator,
|
||||||
|
isExporting: boolean = false,
|
||||||
) => {
|
) => {
|
||||||
let shape = shapeCache.get(element);
|
let shape = isExporting ? undefined : shapeCache.get(element);
|
||||||
|
|
||||||
// `null` indicates no rc shape applicable for this element type
|
// `null` indicates no rc shape applicable for this element type
|
||||||
// (= do not generate anything)
|
// (= do not generate anything)
|
||||||
|
@ -475,7 +507,11 @@ const generateElementShape = (
|
||||||
elementWithCanvasCache.delete(element);
|
elementWithCanvasCache.delete(element);
|
||||||
|
|
||||||
switch (element.type) {
|
switch (element.type) {
|
||||||
case "rectangle": {
|
case "rectangle":
|
||||||
|
case "embeddable": {
|
||||||
|
// this is for rendering the stroke/bg of the embeddable, especially
|
||||||
|
// when the src url is not set
|
||||||
|
|
||||||
if (element.roundness) {
|
if (element.roundness) {
|
||||||
const w = element.width;
|
const w = element.width;
|
||||||
const h = element.height;
|
const h = element.height;
|
||||||
|
@ -486,7 +522,10 @@ const generateElementShape = (
|
||||||
} Q ${w} ${h}, ${w - r} ${h} L ${r} ${h} Q 0 ${h}, 0 ${
|
} Q ${w} ${h}, ${w - r} ${h} L ${r} ${h} Q 0 ${h}, 0 ${
|
||||||
h - r
|
h - r
|
||||||
} L 0 ${r} Q 0 0, ${r} 0`,
|
} L 0 ${r} Q 0 0, ${r} 0`,
|
||||||
generateRoughOptions(element, true),
|
generateRoughOptions(
|
||||||
|
modifyEmbeddableForRoughOptions(element, isExporting),
|
||||||
|
true,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
shape = generator.rectangle(
|
shape = generator.rectangle(
|
||||||
|
@ -494,7 +533,10 @@ const generateElementShape = (
|
||||||
0,
|
0,
|
||||||
element.width,
|
element.width,
|
||||||
element.height,
|
element.height,
|
||||||
generateRoughOptions(element),
|
generateRoughOptions(
|
||||||
|
modifyEmbeddableForRoughOptions(element, isExporting),
|
||||||
|
false,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
setShapeForElement(element, shape);
|
setShapeForElement(element, shape);
|
||||||
|
@ -873,7 +915,7 @@ const drawElementFromCanvas = (
|
||||||
);
|
);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
process.env.REACT_APP_DEBUG_ENABLE_TEXT_CONTAINER_BOUNDING_BOX ===
|
import.meta.env.VITE_APP_DEBUG_ENABLE_TEXT_CONTAINER_BOUNDING_BOX ===
|
||||||
"true" &&
|
"true" &&
|
||||||
hasBoundTextElement(element)
|
hasBoundTextElement(element)
|
||||||
) {
|
) {
|
||||||
|
@ -996,8 +1038,9 @@ export const renderElement = (
|
||||||
case "line":
|
case "line":
|
||||||
case "arrow":
|
case "arrow":
|
||||||
case "image":
|
case "image":
|
||||||
case "text": {
|
case "text":
|
||||||
generateElementShape(element, generator);
|
case "embeddable": {
|
||||||
|
generateElementShape(element, generator, renderConfig.isExporting);
|
||||||
if (renderConfig.isExporting) {
|
if (renderConfig.isExporting) {
|
||||||
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
|
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
|
||||||
const cx = (x1 + x2) / 2 + renderConfig.scrollX;
|
const cx = (x1 + x2) / 2 + renderConfig.scrollX;
|
||||||
|
@ -1180,7 +1223,9 @@ export const renderElementToSvg = (
|
||||||
offsetY: number,
|
offsetY: number,
|
||||||
exportWithDarkMode?: boolean,
|
exportWithDarkMode?: boolean,
|
||||||
exportingFrameId?: string | null,
|
exportingFrameId?: string | null,
|
||||||
|
renderEmbeddables?: boolean,
|
||||||
) => {
|
) => {
|
||||||
|
const offset = { x: offsetX, y: offsetY };
|
||||||
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
|
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
|
||||||
let cx = (x2 - x1) / 2 - (element.x - x1);
|
let cx = (x2 - x1) / 2 - (element.x - x1);
|
||||||
let cy = (y2 - y1) / 2 - (element.y - y1);
|
let cy = (y2 - y1) / 2 - (element.y - y1);
|
||||||
|
@ -1253,6 +1298,106 @@ export const renderElementToSvg = (
|
||||||
g ? root.appendChild(g) : root.appendChild(node);
|
g ? root.appendChild(g) : root.appendChild(node);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case "embeddable": {
|
||||||
|
// render placeholder rectangle
|
||||||
|
generateElementShape(element, generator, true);
|
||||||
|
const node = roughSVGDrawWithPrecision(
|
||||||
|
rsvg,
|
||||||
|
getShapeForElement(element)!,
|
||||||
|
MAX_DECIMALS_FOR_SVG_EXPORT,
|
||||||
|
);
|
||||||
|
const opacity = element.opacity / 100;
|
||||||
|
if (opacity !== 1) {
|
||||||
|
node.setAttribute("stroke-opacity", `${opacity}`);
|
||||||
|
node.setAttribute("fill-opacity", `${opacity}`);
|
||||||
|
}
|
||||||
|
node.setAttribute("stroke-linecap", "round");
|
||||||
|
node.setAttribute(
|
||||||
|
"transform",
|
||||||
|
`translate(${offsetX || 0} ${
|
||||||
|
offsetY || 0
|
||||||
|
}) rotate(${degree} ${cx} ${cy})`,
|
||||||
|
);
|
||||||
|
root.appendChild(node);
|
||||||
|
|
||||||
|
const label: ExcalidrawElement =
|
||||||
|
createPlaceholderEmbeddableLabel(element);
|
||||||
|
renderElementToSvg(
|
||||||
|
label,
|
||||||
|
rsvg,
|
||||||
|
root,
|
||||||
|
files,
|
||||||
|
label.x + offset.x - element.x,
|
||||||
|
label.y + offset.y - element.y,
|
||||||
|
exportWithDarkMode,
|
||||||
|
exportingFrameId,
|
||||||
|
renderEmbeddables,
|
||||||
|
);
|
||||||
|
|
||||||
|
// render embeddable element + iframe
|
||||||
|
const embeddableNode = roughSVGDrawWithPrecision(
|
||||||
|
rsvg,
|
||||||
|
getShapeForElement(element)!,
|
||||||
|
MAX_DECIMALS_FOR_SVG_EXPORT,
|
||||||
|
);
|
||||||
|
embeddableNode.setAttribute("stroke-linecap", "round");
|
||||||
|
embeddableNode.setAttribute(
|
||||||
|
"transform",
|
||||||
|
`translate(${offsetX || 0} ${
|
||||||
|
offsetY || 0
|
||||||
|
}) rotate(${degree} ${cx} ${cy})`,
|
||||||
|
);
|
||||||
|
while (embeddableNode.firstChild) {
|
||||||
|
embeddableNode.removeChild(embeddableNode.firstChild);
|
||||||
|
}
|
||||||
|
const radius = getCornerRadius(
|
||||||
|
Math.min(element.width, element.height),
|
||||||
|
element,
|
||||||
|
);
|
||||||
|
|
||||||
|
const embedLink = getEmbedLink(toValidURL(element.link || ""));
|
||||||
|
|
||||||
|
// if rendering embeddables explicitly disabled or
|
||||||
|
// embedding documents via srcdoc (which doesn't seem to work for SVGs)
|
||||||
|
// replace with a link instead
|
||||||
|
if (renderEmbeddables === false || embedLink?.type === "document") {
|
||||||
|
const anchorTag = svgRoot.ownerDocument!.createElementNS(SVG_NS, "a");
|
||||||
|
anchorTag.setAttribute("href", normalizeLink(element.link || ""));
|
||||||
|
anchorTag.setAttribute("target", "_blank");
|
||||||
|
anchorTag.setAttribute("rel", "noopener noreferrer");
|
||||||
|
anchorTag.style.borderRadius = `${radius}px`;
|
||||||
|
|
||||||
|
embeddableNode.appendChild(anchorTag);
|
||||||
|
} else {
|
||||||
|
const foreignObject = svgRoot.ownerDocument!.createElementNS(
|
||||||
|
SVG_NS,
|
||||||
|
"foreignObject",
|
||||||
|
);
|
||||||
|
foreignObject.style.width = `${element.width}px`;
|
||||||
|
foreignObject.style.height = `${element.height}px`;
|
||||||
|
foreignObject.style.border = "none";
|
||||||
|
const div = foreignObject.ownerDocument!.createElementNS(SVG_NS, "div");
|
||||||
|
div.setAttribute("xmlns", "http://www.w3.org/1999/xhtml");
|
||||||
|
div.style.width = "100%";
|
||||||
|
div.style.height = "100%";
|
||||||
|
const iframe = div.ownerDocument!.createElement("iframe");
|
||||||
|
iframe.src = embedLink?.link ?? "";
|
||||||
|
iframe.style.width = "100%";
|
||||||
|
iframe.style.height = "100%";
|
||||||
|
iframe.style.border = "none";
|
||||||
|
iframe.style.borderRadius = `${radius}px`;
|
||||||
|
iframe.style.top = "0";
|
||||||
|
iframe.style.left = "0";
|
||||||
|
iframe.allowFullscreen = true;
|
||||||
|
div.appendChild(iframe);
|
||||||
|
foreignObject.appendChild(div);
|
||||||
|
|
||||||
|
embeddableNode.appendChild(foreignObject);
|
||||||
|
}
|
||||||
|
|
||||||
|
root.appendChild(embeddableNode);
|
||||||
|
break;
|
||||||
|
}
|
||||||
case "line":
|
case "line":
|
||||||
case "arrow": {
|
case "arrow": {
|
||||||
const boundText = getBoundTextElement(element);
|
const boundText = getBoundTextElement(element);
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue