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/
|
||||
REACT_APP_BACKEND_V2_POST_URL=https://json-dev.excalidraw.com/api/v2/post/
|
||||
VITE_APP_BACKEND_V2_GET_URL=https://json-dev.excalidraw.com/api/v2/
|
||||
VITE_APP_BACKEND_V2_POST_URL=https://json-dev.excalidraw.com/api/v2/post/
|
||||
|
||||
REACT_APP_LIBRARY_URL=https://libraries.excalidraw.com
|
||||
REACT_APP_LIBRARY_BACKEND=https://us-central1-excalidraw-room-persistence.cloudfunctions.net/libraries
|
||||
VITE_APP_LIBRARY_URL=https://libraries.excalidraw.com
|
||||
VITE_APP_LIBRARY_BACKEND=https://us-central1-excalidraw-room-persistence.cloudfunctions.net/libraries
|
||||
|
||||
# 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
|
||||
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!
|
||||
# must be lowercase `true` when turned on
|
||||
#
|
||||
# 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
|
||||
# debugging Service Workers.
|
||||
REACT_APP_DEV_DISABLE_LIVE_RELOAD=
|
||||
REACT_APP_DISABLE_TRACKING=true
|
||||
VITE_APP_DEV_DISABLE_LIVE_RELOAD=
|
||||
VITE_APP_DISABLE_TRACKING=true
|
||||
|
||||
FAST_REFRESH=false
|
||||
|
||||
# The port the run the dev server
|
||||
VITE_APP_PORT=3000
|
||||
|
||||
#Debug flags
|
||||
|
||||
# To enable bounding box for text containers
|
||||
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_POST_URL=https://json.excalidraw.com/api/v2/post/
|
||||
|
||||
REACT_APP_LIBRARY_URL=https://libraries.excalidraw.com
|
||||
REACT_APP_LIBRARY_BACKEND=https://us-central1-excalidraw-room-persistence.cloudfunctions.net/libraries
|
||||
VITE_APP_LIBRARY_URL=https://libraries.excalidraw.com
|
||||
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.
|
||||
# Meant for forks only: excalidraw.com uses custom REACT_APP_PORTAL_URL flow
|
||||
REACT_APP_WS_SERVER_URL=
|
||||
# Meant for forks only: excalidraw.com uses custom VITE_APP_PORTAL_URL flow
|
||||
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
|
||||
REACT_APP_DISABLE_TRACKING=
|
||||
VITE_APP_PLUS_APP=https://app.excalidraw.com
|
||||
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
|
||||
with:
|
||||
fetch-depth: 2
|
||||
- name: Setup Node.js 14.x
|
||||
- name: Setup Node.js 18.x
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 14.x
|
||||
node-version: 18.x
|
||||
- name: Set up publish access
|
||||
run: |
|
||||
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:
|
||||
ref: ${{ steps.sha.outputs.result }}
|
||||
fetch-depth: 2
|
||||
- name: Setup Node.js 14.x
|
||||
- name: Setup Node.js 18.x
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 14.x
|
||||
node-version: 18.x
|
||||
- name: Set up publish access
|
||||
run: |
|
||||
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:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Setup Node.js 14.x
|
||||
- name: Setup Node.js 18.x
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 14.x
|
||||
node-version: 18.x
|
||||
|
||||
- name: Install and lint
|
||||
run: |
|
||||
|
|
4
.github/workflows/locales-coverage.yml
vendored
4
.github/workflows/locales-coverage.yml
vendored
|
@ -14,10 +14,10 @@ jobs:
|
|||
with:
|
||||
token: ${{ secrets.PUSH_TRANSLATIONS_COVERAGE_PAT }}
|
||||
|
||||
- name: Setup Node.js 14.x
|
||||
- name: Setup Node.js 18.x
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 14.x
|
||||
node-version: 18.x
|
||||
|
||||
- name: Create report file
|
||||
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
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
pull_request:
|
||||
types:
|
||||
- opened
|
||||
- edited
|
||||
|
|
4
.github/workflows/sentry-production.yml
vendored
4
.github/workflows/sentry-production.yml
vendored
|
@ -10,10 +10,10 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Setup Node.js 14.x
|
||||
- name: Setup Node.js 18.x
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 14.x
|
||||
node-version: 18.x
|
||||
- name: Install and build
|
||||
run: |
|
||||
yarn --frozen-lockfile
|
||||
|
|
4
.github/workflows/test.yml
vendored
4
.github/workflows/test.yml
vendored
|
@ -7,10 +7,10 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Setup Node.js 14.x
|
||||
- name: Setup Node.js 18.x
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 14.x
|
||||
node-version: 18.x
|
||||
- name: Install and test
|
||||
run: |
|
||||
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.development.js
|
||||
coverage
|
||||
dev-dist
|
||||
html
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
FROM node:14-alpine AS build
|
||||
FROM node:18 AS build
|
||||
|
||||
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. |
|
||||
| [`autoFocus`](#autofocus) | `boolean` | `false` | indicates whether to focus the Excalidraw component on page load |
|
||||
| [`generateIdForFile`](#generateidforfile) | `function` | _ | Allows you to override `id` generation for files added on canvas |
|
||||
| [`validateEmbeddable`](#validateEmbeddable) | string[] | `boolean | RegExp | RegExp[] | ((link: string) => boolean | undefined)` | \_ | use for custom src url validation |
|
||||
| [`renderEmbeddable`](/docs/@excalidraw/excalidraw/api/props/render-props#renderEmbeddable) | `function` | \_ | Render function that can override the built-in `<iframe>` |
|
||||
|
||||
### 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).
|
||||
|
||||
|
||||
### autoFocus
|
||||
|
||||
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>
|
||||
```
|
||||
|
||||
### 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>
|
||||
<!------------------------------------------------------------------------->
|
||||
|
||||
<% if (process.env.NODE_ENV === "production") { %>
|
||||
<% if ("%PROD%" === "true") { %>
|
||||
<script>
|
||||
// Redirect Excalidraw+ users which have auto-redirect enabled.
|
||||
//
|
||||
|
@ -100,41 +99,35 @@
|
|||
</script>
|
||||
<% } %>
|
||||
|
||||
<link rel="shortcut icon" href="favicon.ico" type="image/x-icon" />
|
||||
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon" />
|
||||
|
||||
<!-- Excalidraw version -->
|
||||
<meta name="version" content="{version}" />
|
||||
|
||||
<link
|
||||
rel="preload"
|
||||
href="Virgil.woff2"
|
||||
href="/Virgil.woff2"
|
||||
as="font"
|
||||
type="font/woff2"
|
||||
crossorigin="anonymous"
|
||||
/>
|
||||
<link
|
||||
rel="preload"
|
||||
href="Cascadia.woff2"
|
||||
href="/Cascadia.woff2"
|
||||
as="font"
|
||||
type="font/woff2"
|
||||
crossorigin="anonymous"
|
||||
/>
|
||||
|
||||
<link
|
||||
rel="manifest"
|
||||
href="manifest.json"
|
||||
style="--pwacompat-splash-font: 24px Virgil"
|
||||
/>
|
||||
|
||||
<link rel="stylesheet" href="fonts.css" type="text/css" />
|
||||
<% if (process.env.REACT_APP_DEV_DISABLE_LIVE_RELOAD==="true" ) { %>
|
||||
<link rel="stylesheet" href="/fonts.css" type="text/css" />
|
||||
<% if ("%VITE_APP_DEV_DISABLE_LIVE_RELOAD%"==="true" ) { %>
|
||||
<script>
|
||||
{
|
||||
const _WebSocket = window.WebSocket;
|
||||
window.WebSocket = function (url) {
|
||||
if (/ws:\/\/localhost:.+?\/sockjs-node/.test(url)) {
|
||||
console.info(
|
||||
"[!!!] Live reload is disabled via process.env.REACT_APP_DEV_DISABLE_LIVE_RELOAD [!!!]",
|
||||
"[!!!] Live reload is disabled via VITE_APP_DEV_DISABLE_LIVE_RELOAD [!!!]",
|
||||
);
|
||||
} else {
|
||||
return new _WebSocket(url);
|
||||
|
@ -200,7 +193,8 @@
|
|||
<h1 class="visually-hidden">Excalidraw</h1>
|
||||
</header>
|
||||
<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 -->
|
||||
<script>
|
||||
// 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",
|
||||
"clsx": "1.1.1",
|
||||
"cross-env": "7.0.3",
|
||||
"eslint-plugin-react": "7.32.2",
|
||||
"fake-indexeddb": "3.1.7",
|
||||
"firebase": "8.3.3",
|
||||
"i18next-browser-languagedetector": "6.1.4",
|
||||
|
@ -51,28 +52,14 @@
|
|||
"pwacompat": "2.0.17",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-scripts": "5.0.1",
|
||||
"roughjs": "4.5.2",
|
||||
"sass": "1.51.0",
|
||||
"socket.io-client": "2.3.1",
|
||||
"tunnel-rat": "0.1.2",
|
||||
"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"
|
||||
"tunnel-rat": "0.1.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@excalidraw/eslint-config": "1.0.0",
|
||||
"@excalidraw/eslint-config": "1.0.3",
|
||||
"@excalidraw/prettier-config": "1.0.2",
|
||||
"@size-limit/preset-big-lib": "8.2.4",
|
||||
"@types/chai": "4.3.0",
|
||||
"@types/jest": "27.4.0",
|
||||
"@types/lodash.throttle": "4.1.7",
|
||||
|
@ -82,49 +69,42 @@
|
|||
"@types/react-dom": "18.0.6",
|
||||
"@types/resize-observer-browser": "0.1.7",
|
||||
"@types/socket.io-client": "1.4.36",
|
||||
"@vitejs/plugin-react": "3.1.0",
|
||||
"@vitest/ui": "0.32.2",
|
||||
"chai": "4.3.6",
|
||||
"dotenv": "16.0.1",
|
||||
"eslint-config-prettier": "8.5.0",
|
||||
"eslint-config-react-app": "7.0.1",
|
||||
"eslint-plugin-prettier": "3.3.1",
|
||||
"http-server": "14.1.1",
|
||||
"husky": "7.0.4",
|
||||
"jest-canvas-mock": "2.4.0",
|
||||
"jsdom": "22.1.0",
|
||||
"lint-staged": "12.3.7",
|
||||
"pepjs": "0.5.3",
|
||||
"prettier": "2.6.2",
|
||||
"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": {
|
||||
"node": ">=14.0.0"
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"homepage": ".",
|
||||
"jest": {
|
||||
"collectCoverageFrom": [
|
||||
"src/**/*.{js,jsx,ts,tsx}"
|
||||
],
|
||||
"coveragePathIgnorePatterns": [
|
||||
"<rootDir>/locales",
|
||||
"<rootDir>/src/packages/excalidraw/dist/",
|
||||
"<rootDir>/src/packages/excalidraw/types",
|
||||
"<rootDir>/src/packages/excalidraw/example"
|
||||
],
|
||||
"transformIgnorePatterns": [
|
||||
"node_modules/(?!(roughjs|points-on-curve|path-data-parser|points-on-path|browser-fs-access|canvas-roundrect-polyfill)/)"
|
||||
],
|
||||
"resetMocks": false
|
||||
},
|
||||
"name": "excalidraw",
|
||||
"prettier": "@excalidraw/prettier-config",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build-node": "node ./scripts/build-node.js",
|
||||
"build:app:docker": "cross-env REACT_APP_DISABLE_SENTRY=true REACT_APP_DISABLE_TRACKING=true react-scripts build",
|
||||
"build:app": "cross-env REACT_APP_GIT_SHA=$VERCEL_GIT_COMMIT_SHA react-scripts build",
|
||||
"build: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 vite build",
|
||||
"build:version": "node ./scripts/build-version.js",
|
||||
"build": "yarn build:app && yarn build:version",
|
||||
"eject": "react-scripts eject",
|
||||
"fix:code": "yarn test:code --fix",
|
||||
"fix:other": "yarn prettier --write",
|
||||
"fix": "yarn fix:other && yarn fix:code",
|
||||
|
@ -132,19 +112,20 @@
|
|||
"locales-coverage:description": "node scripts/locales-coverage-description.js",
|
||||
"prepare": "husky install",
|
||||
"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",
|
||||
"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:debug": "react-scripts --inspect-brk test --runInBand --no-cache",
|
||||
"test:other": "yarn prettier --list-different",
|
||||
"test:typecheck": "tsc",
|
||||
"test:update": "yarn test:app --updateSnapshot --watchAll=false",
|
||||
"test:update": "yarn test:app --update --watch=false",
|
||||
"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",
|
||||
"prerelease": "node scripts/prerelease.js",
|
||||
"build:preview": "yarn build && vite preview --port 5000",
|
||||
"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 { randomId } from "../random";
|
||||
import { t } from "../i18n";
|
||||
import { LIBRARY_DISABLED_TYPES } from "../constants";
|
||||
|
||||
export const actionAddToLibrary = register({
|
||||
name: "addToLibrary",
|
||||
|
@ -12,14 +13,17 @@ export const actionAddToLibrary = register({
|
|||
includeBoundTextElement: true,
|
||||
includeElementsInFrames: true,
|
||||
});
|
||||
if (selectedElements.some((element) => element.type === "image")) {
|
||||
return {
|
||||
commitToHistory: false,
|
||||
appState: {
|
||||
...appState,
|
||||
errorMessage: "Support for adding images to the library coming soon!",
|
||||
},
|
||||
};
|
||||
|
||||
for (const type of LIBRARY_DISABLED_TYPES) {
|
||||
if (selectedElements.some((element) => element.type === type)) {
|
||||
return {
|
||||
commitToHistory: false,
|
||||
appState: {
|
||||
...appState,
|
||||
errorMessage: t(`errors.libraryElementTypeError.${type}`),
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return app.library
|
||||
|
|
|
@ -396,6 +396,7 @@ export const actionToggleEraserTool = register({
|
|||
...appState,
|
||||
selectedElementIds: {},
|
||||
selectedGroupIds: {},
|
||||
activeEmbeddable: null,
|
||||
activeTool,
|
||||
},
|
||||
commitToHistory: true,
|
||||
|
@ -430,6 +431,7 @@ export const actionToggleHandTool = register({
|
|||
...appState,
|
||||
selectedElementIds: {},
|
||||
selectedGroupIds: {},
|
||||
activeEmbeddable: null,
|
||||
activeTool,
|
||||
},
|
||||
commitToHistory: true,
|
||||
|
|
|
@ -158,6 +158,7 @@ export const actionDeleteSelected = register({
|
|||
...nextAppState,
|
||||
activeTool: updateActiveTool(appState, { type: "selection" }),
|
||||
multiElement: null,
|
||||
activeEmbeddable: null,
|
||||
},
|
||||
commitToHistory: isSomeElementSelected(
|
||||
getNonDeletedElements(elements),
|
||||
|
|
|
@ -65,7 +65,7 @@ export const actionChangeExportScale = register({
|
|||
);
|
||||
|
||||
const scaleButtonTitle = `${t(
|
||||
"buttons.scale",
|
||||
"imageExportDialog.label.scale",
|
||||
)} ${s}x (${width}x${height})`;
|
||||
|
||||
return (
|
||||
|
@ -102,7 +102,7 @@ export const actionChangeExportBackground = register({
|
|||
checked={appState.exportBackground}
|
||||
onChange={(checked) => updateData(checked)}
|
||||
>
|
||||
{t("labels.withBackground")}
|
||||
{t("imageExportDialog.label.withBackground")}
|
||||
</CheckboxItem>
|
||||
),
|
||||
});
|
||||
|
@ -121,8 +121,8 @@ export const actionChangeExportEmbedScene = register({
|
|||
checked={appState.exportEmbedScene}
|
||||
onChange={(checked) => updateData(checked)}
|
||||
>
|
||||
{t("labels.exportEmbedScene")}
|
||||
<Tooltip label={t("labels.exportEmbedScene_details")} long={true}>
|
||||
{t("imageExportDialog.label.embedScene")}
|
||||
<Tooltip label={t("imageExportDialog.tooltip.embedScene")} long={true}>
|
||||
<div className="excalidraw-tooltip-icon">{questionCircle}</div>
|
||||
</Tooltip>
|
||||
</CheckboxItem>
|
||||
|
@ -277,7 +277,7 @@ export const actionExportWithDarkMode = register({
|
|||
onChange={(theme: Theme) => {
|
||||
updateData(theme === THEME.DARK);
|
||||
}}
|
||||
title={t("labels.toggleExportColorScheme")}
|
||||
title={t("imageExportDialog.label.darkMode")}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
|
|
|
@ -160,6 +160,7 @@ export const actionFinalize = register({
|
|||
multiPointElement
|
||||
? appState.activeTool
|
||||
: activeTool,
|
||||
activeEmbeddable: null,
|
||||
draggingElement: null,
|
||||
multiElement: null,
|
||||
editingElement: null,
|
||||
|
|
|
@ -121,6 +121,7 @@ export type ActionName =
|
|||
| "removeAllElementsFromFrame"
|
||||
| "updateFrameRendering"
|
||||
| "setFrameAsActiveTool"
|
||||
| "setEmbeddableAsActiveTool"
|
||||
| "createContainerFromText"
|
||||
| "wrapTextInContainer";
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@ export const trackEvent = (
|
|||
// Uncomment the next line to track locally
|
||||
// console.log("Track Event", { category, action, label, value });
|
||||
|
||||
if (typeof window === "undefined" || process.env.JEST_WORKER_ID) {
|
||||
if (typeof window === "undefined" || import.meta.env.VITE_WORKER_ID) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -38,6 +38,7 @@ export const getDefaultAppState = (): Omit<
|
|||
currentItemStrokeWidth: DEFAULT_ELEMENT_PROPS.strokeWidth,
|
||||
currentItemTextAlign: DEFAULT_TEXT_ALIGN,
|
||||
cursorButton: "up",
|
||||
activeEmbeddable: null,
|
||||
draggingElement: null,
|
||||
editingElement: null,
|
||||
editingGroupId: null,
|
||||
|
@ -139,6 +140,7 @@ const APP_STATE_STORAGE_CONF = (<
|
|||
currentItemStrokeWidth: { browser: true, export: false, server: false },
|
||||
currentItemTextAlign: { browser: true, export: false, server: false },
|
||||
cursorButton: { browser: true, export: false, server: false },
|
||||
activeEmbeddable: { browser: false, export: false, server: false },
|
||||
draggingElement: { browser: false, export: false, server: false },
|
||||
editingElement: { browser: false, export: false, server: false },
|
||||
editingGroupId: { browser: true, export: false, server: false },
|
||||
|
|
|
@ -6,7 +6,6 @@ import {
|
|||
import {
|
||||
DEFAULT_FONT_FAMILY,
|
||||
DEFAULT_FONT_SIZE,
|
||||
ENV,
|
||||
VERTICAL_ALIGN,
|
||||
} from "./constants";
|
||||
import { newElement, newLinearElement, newTextElement } from "./element";
|
||||
|
@ -384,7 +383,7 @@ const chartTypeBar = (
|
|||
y,
|
||||
groupId,
|
||||
backgroundColor,
|
||||
process.env.NODE_ENV === ENV.DEVELOPMENT,
|
||||
import.meta.env.DEV,
|
||||
),
|
||||
];
|
||||
};
|
||||
|
@ -473,7 +472,7 @@ const chartTypeLine = (
|
|||
y,
|
||||
groupId,
|
||||
backgroundColor,
|
||||
process.env.NODE_ENV === ENV.DEVELOPMENT,
|
||||
import.meta.env.DEV,
|
||||
),
|
||||
line,
|
||||
...lines,
|
||||
|
|
|
@ -21,7 +21,7 @@ export type ColorPickerColor =
|
|||
export type ColorTuple = readonly [string, string, string, string, string];
|
||||
export type ColorPalette = Merge<
|
||||
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
|
||||
|
|
|
@ -36,7 +36,7 @@ import {
|
|||
|
||||
import "./Actions.scss";
|
||||
import DropdownMenu from "./dropdownMenu/DropdownMenu";
|
||||
import { extraToolsIcon, frameToolIcon } from "./icons";
|
||||
import { EmbedIcon, extraToolsIcon, frameToolIcon } from "./icons";
|
||||
import { KEYS } from "../keys";
|
||||
|
||||
export const SelectedShapeActions = ({
|
||||
|
@ -266,6 +266,7 @@ export const ShapesSwitcher = ({
|
|||
});
|
||||
setAppState({
|
||||
activeTool: nextActiveTool,
|
||||
activeEmbeddable: null,
|
||||
multiElement: null,
|
||||
selectedElementIds: {},
|
||||
});
|
||||
|
@ -283,39 +284,72 @@ export const ShapesSwitcher = ({
|
|||
<div className="App-toolbar__divider" />
|
||||
{/* TEMP HACK because dropdown doesn't work well inside mobile toolbar */}
|
||||
{device.isMobile ? (
|
||||
<ToolButton
|
||||
className={clsx("Shape", { fillable: false })}
|
||||
type="radio"
|
||||
icon={frameToolIcon}
|
||||
checked={activeTool.type === "frame"}
|
||||
name="editor-current-shape"
|
||||
title={`${capitalizeString(
|
||||
t("toolBar.frame"),
|
||||
)} — ${KEYS.F.toLocaleUpperCase()}`}
|
||||
keyBindingLabel={KEYS.F.toLocaleUpperCase()}
|
||||
aria-label={capitalizeString(t("toolBar.frame"))}
|
||||
aria-keyshortcuts={KEYS.F.toLocaleUpperCase()}
|
||||
data-testid={`toolbar-frame`}
|
||||
onPointerDown={({ pointerType }) => {
|
||||
if (!appState.penDetected && pointerType === "pen") {
|
||||
setAppState({
|
||||
penDetected: true,
|
||||
penMode: true,
|
||||
<>
|
||||
<ToolButton
|
||||
className={clsx("Shape", { fillable: false })}
|
||||
type="radio"
|
||||
icon={frameToolIcon}
|
||||
checked={activeTool.type === "frame"}
|
||||
name="editor-current-shape"
|
||||
title={`${capitalizeString(
|
||||
t("toolBar.frame"),
|
||||
)} — ${KEYS.F.toLocaleUpperCase()}`}
|
||||
keyBindingLabel={KEYS.F.toLocaleUpperCase()}
|
||||
aria-label={capitalizeString(t("toolBar.frame"))}
|
||||
aria-keyshortcuts={KEYS.F.toLocaleUpperCase()}
|
||||
data-testid={`toolbar-frame`}
|
||||
onPointerDown={({ pointerType }) => {
|
||||
if (!appState.penDetected && pointerType === "pen") {
|
||||
setAppState({
|
||||
penDetected: true,
|
||||
penMode: true,
|
||||
});
|
||||
}
|
||||
}}
|
||||
onChange={({ pointerType }) => {
|
||||
trackEvent("toolbar", "frame", "ui");
|
||||
const nextActiveTool = updateActiveTool(appState, {
|
||||
type: "frame",
|
||||
});
|
||||
}
|
||||
}}
|
||||
onChange={({ pointerType }) => {
|
||||
trackEvent("toolbar", "frame", "ui");
|
||||
const nextActiveTool = updateActiveTool(appState, {
|
||||
type: "frame",
|
||||
});
|
||||
setAppState({
|
||||
activeTool: nextActiveTool,
|
||||
multiElement: null,
|
||||
selectedElementIds: {},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
setAppState({
|
||||
activeTool: nextActiveTool,
|
||||
multiElement: null,
|
||||
selectedElementIds: {},
|
||||
activeEmbeddable: null,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<ToolButton
|
||||
className={clsx("Shape", { fillable: false })}
|
||||
type="radio"
|
||||
icon={EmbedIcon}
|
||||
checked={activeTool.type === "embeddable"}
|
||||
name="editor-current-shape"
|
||||
title={capitalizeString(t("toolBar.embeddable"))}
|
||||
aria-label={capitalizeString(t("toolBar.embeddable"))}
|
||||
data-testid={`toolbar-embeddable`}
|
||||
onPointerDown={({ pointerType }) => {
|
||||
if (!appState.penDetected && pointerType === "pen") {
|
||||
setAppState({
|
||||
penDetected: true,
|
||||
penMode: true,
|
||||
});
|
||||
}
|
||||
}}
|
||||
onChange={({ pointerType }) => {
|
||||
trackEvent("toolbar", "embeddable", "ui");
|
||||
const nextActiveTool = updateActiveTool(appState, {
|
||||
type: "embeddable",
|
||||
});
|
||||
setAppState({
|
||||
activeTool: nextActiveTool,
|
||||
multiElement: null,
|
||||
selectedElementIds: {},
|
||||
activeEmbeddable: null,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<DropdownMenu open={isExtraToolsMenuOpen}>
|
||||
<DropdownMenu.Trigger
|
||||
|
@ -347,6 +381,22 @@ export const ShapesSwitcher = ({
|
|||
>
|
||||
{t("toolBar.frame")}
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item
|
||||
onSelect={() => {
|
||||
const nextActiveTool = updateActiveTool(appState, {
|
||||
type: "embeddable",
|
||||
});
|
||||
setAppState({
|
||||
activeTool: nextActiveTool,
|
||||
multiElement: null,
|
||||
selectedElementIds: {},
|
||||
});
|
||||
}}
|
||||
icon={EmbedIcon}
|
||||
data-testid="toolbar-embeddable"
|
||||
>
|
||||
{t("toolBar.embeddable")}
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
|
|
|
@ -4,8 +4,9 @@ import { reseed } from "../random";
|
|||
import { render, queryByTestId } from "../tests/test-utils";
|
||||
|
||||
import ExcalidrawApp from "../excalidraw-app";
|
||||
import { vi } from "vitest";
|
||||
|
||||
const renderScene = jest.spyOn(Renderer, "renderScene");
|
||||
const renderScene = vi.spyOn(Renderer, "renderScene");
|
||||
|
||||
describe("Test <App/>", () => {
|
||||
beforeEach(async () => {
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -8,7 +8,7 @@ import {
|
|||
} from "./colorPickerUtils";
|
||||
import HotkeyLabel from "./HotkeyLabel";
|
||||
import { ColorPaletteCustom } from "../../colors";
|
||||
import { t } from "../../i18n";
|
||||
import { TranslationKeys, t } from "../../i18n";
|
||||
|
||||
interface PickerColorListProps {
|
||||
palette: ColorPaletteCustom;
|
||||
|
@ -48,7 +48,11 @@ const PickerColorList = ({
|
|||
(Array.isArray(value) ? value[activeShade] : value) || "transparent";
|
||||
|
||||
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 (
|
||||
<button
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import clsx from "clsx";
|
||||
import { Popover } from "./Popover";
|
||||
import { t } from "../i18n";
|
||||
import { t, TranslationKeys } from "../i18n";
|
||||
|
||||
import "./ContextMenu.scss";
|
||||
import {
|
||||
|
@ -83,10 +83,14 @@ export const ContextMenu = React.memo(
|
|||
if (item.contextItemLabel) {
|
||||
if (typeof item.contextItemLabel === "function") {
|
||||
label = t(
|
||||
item.contextItemLabel(elements, appState, actionManager.app),
|
||||
item.contextItemLabel(
|
||||
elements,
|
||||
appState,
|
||||
actionManager.app,
|
||||
) as unknown as TranslationKeys,
|
||||
);
|
||||
} else {
|
||||
label = t(item.contextItemLabel);
|
||||
label = t(item.contextItemLabel as unknown as TranslationKeys);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -58,7 +58,7 @@ export const EyeDropper: React.FC<{
|
|||
return;
|
||||
}
|
||||
|
||||
let currentColor = COLOR_PALETTE.black;
|
||||
let currentColor: string = COLOR_PALETTE.black;
|
||||
let isHoldingPointerDown = false;
|
||||
|
||||
const ctx = app.canvas.getContext("2d")!;
|
||||
|
|
|
@ -44,6 +44,10 @@ const getHints = ({ appState, isMobile, device, app }: HintViewerProps) => {
|
|||
return t("hints.text");
|
||||
}
|
||||
|
||||
if (activeTool.type === "embeddable") {
|
||||
return t("hints.embeddable");
|
||||
}
|
||||
|
||||
if (appState.activeTool.type === "image" && appState.pendingImageElementId) {
|
||||
return t("hints.placeImage");
|
||||
}
|
||||
|
|
|
@ -29,6 +29,7 @@ import "./LibraryMenu.scss";
|
|||
import { LibraryMenuControlButtons } from "./LibraryMenuControlButtons";
|
||||
import { isShallowEqual } from "../utils";
|
||||
import { NonDeletedExcalidrawElement } from "../element/types";
|
||||
import { LIBRARY_DISABLED_TYPES } from "../constants";
|
||||
|
||||
export const isLibraryMenuOpenAtom = atom(false);
|
||||
|
||||
|
@ -68,11 +69,12 @@ export const LibraryMenuContent = ({
|
|||
libraryItems: LibraryItems,
|
||||
) => {
|
||||
trackEvent("element", "addToLibrary", "ui");
|
||||
if (processedElements.some((element) => element.type === "image")) {
|
||||
return setAppState({
|
||||
errorMessage:
|
||||
"Support for adding images to the library coming soon!",
|
||||
});
|
||||
for (const type of LIBRARY_DISABLED_TYPES) {
|
||||
if (processedElements.some((element) => element.type === type)) {
|
||||
return setAppState({
|
||||
errorMessage: t(`errors.libraryElementTypeError.${type}`),
|
||||
});
|
||||
}
|
||||
}
|
||||
const nextItems: LibraryItems = [
|
||||
{
|
||||
|
@ -197,6 +199,7 @@ export const LibraryMenu = () => {
|
|||
setAppState({
|
||||
selectedElementIds: {},
|
||||
selectedGroupIds: {},
|
||||
activeEmbeddable: null,
|
||||
});
|
||||
}, [setAppState]);
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@ const LibraryMenuBrowseButton = ({
|
|||
return (
|
||||
<a
|
||||
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"
|
||||
}&referrer=${referrer}&useHash=true&token=${id}&theme=${theme}&version=${
|
||||
VERSIONS.excalidrawLibrary
|
||||
|
|
|
@ -12,6 +12,11 @@
|
|||
box-sizing: border-box;
|
||||
border-radius: var(--border-radius-lg);
|
||||
|
||||
svg {
|
||||
// to prevent clicks on links and such
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&--hover {
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
|
|
|
@ -319,7 +319,7 @@ const PublishLibrary = ({
|
|||
formData.append("twitterHandle", libraryData.twitterHandle);
|
||||
formData.append("website", libraryData.website);
|
||||
|
||||
fetch(`${process.env.REACT_APP_LIBRARY_BACKEND}/submit`, {
|
||||
fetch(`${import.meta.env.VITE_APP_LIBRARY_BACKEND}/submit`, {
|
||||
method: "post",
|
||||
body: formData,
|
||||
})
|
||||
|
|
|
@ -3,7 +3,7 @@ import { t } from "../i18n";
|
|||
import { useExcalidrawContainer } from "./App";
|
||||
|
||||
export const Section: React.FC<{
|
||||
heading: string;
|
||||
heading: "canvasActions" | "selectedShapeActions" | "shapes";
|
||||
children?: React.ReactNode | ((heading: React.ReactNode) => React.ReactNode);
|
||||
className?: string;
|
||||
}> = ({ heading, children, ...props }) => {
|
||||
|
|
|
@ -10,6 +10,7 @@ import {
|
|||
waitFor,
|
||||
withExcalidrawDimensions,
|
||||
} from "../../tests/test-utils";
|
||||
import { vi } from "vitest";
|
||||
|
||||
export const assertSidebarDockButton = async <T extends boolean>(
|
||||
hasDockButton: T,
|
||||
|
@ -205,7 +206,7 @@ describe("Sidebar", () => {
|
|||
});
|
||||
|
||||
it("<Sidebar.Header> should render close button", async () => {
|
||||
const onStateChange = jest.fn();
|
||||
const onStateChange = vi.fn();
|
||||
const CustomExcalidraw = () => {
|
||||
return (
|
||||
<Excalidraw
|
||||
|
|
|
@ -53,7 +53,7 @@ export const SidebarInner = forwardRef(
|
|||
}: SidebarProps & Omit<React.RefAttributes<HTMLDivElement>, "onSelect">,
|
||||
ref: React.ForwardedRef<HTMLDivElement>,
|
||||
) => {
|
||||
if (process.env.NODE_ENV === "development" && onDock && docked == null) {
|
||||
if (import.meta.env.DEV && onDock && docked == null) {
|
||||
console.warn(
|
||||
"Sidebar: `docked` must be set when `onDock` is supplied for the sidebar to be user-dockable. To hide this message, either pass `docked` or remove `onDock`",
|
||||
);
|
||||
|
|
|
@ -3,6 +3,7 @@ import { render } from "@testing-library/react";
|
|||
import fallbackLangData from "../locales/en.json";
|
||||
|
||||
import Trans from "./Trans";
|
||||
import { TranslationKeys } from "../i18n";
|
||||
|
||||
describe("Test <Trans/>", () => {
|
||||
it("should translate the the strings correctly", () => {
|
||||
|
@ -18,24 +19,27 @@ describe("Test <Trans/>", () => {
|
|||
const { getByTestId } = render(
|
||||
<>
|
||||
<div data-testid="test1">
|
||||
<Trans i18nKey="transTest.key1" audience="world" />
|
||||
<Trans
|
||||
i18nKey={"transTest.key1" as unknown as TranslationKeys}
|
||||
audience="world"
|
||||
/>
|
||||
</div>
|
||||
<div data-testid="test2">
|
||||
<Trans
|
||||
i18nKey="transTest.key2"
|
||||
i18nKey={"transTest.key2" as unknown as TranslationKeys}
|
||||
link={(el) => <a href="https://example.com">{el}</a>}
|
||||
/>
|
||||
</div>
|
||||
<div data-testid="test3">
|
||||
<Trans
|
||||
i18nKey="transTest.key3"
|
||||
i18nKey={"transTest.key3" as unknown as TranslationKeys}
|
||||
link={(el) => <a href="https://example.com">{el}</a>}
|
||||
location="the button"
|
||||
/>
|
||||
</div>
|
||||
<div data-testid="test4">
|
||||
<Trans
|
||||
i18nKey="transTest.key4"
|
||||
i18nKey={"transTest.key4" as unknown as TranslationKeys}
|
||||
link={(el) => <a href="https://example.com">{el}</a>}
|
||||
location="the button"
|
||||
bold={(el) => <strong>{el}</strong>}
|
||||
|
@ -43,7 +47,7 @@ describe("Test <Trans/>", () => {
|
|||
</div>
|
||||
<div data-testid="test5">
|
||||
<Trans
|
||||
i18nKey="transTest.key5"
|
||||
i18nKey={"transTest.key5" as unknown as TranslationKeys}
|
||||
connect-link={(el) => <a href="https://example.com">{el}</a>}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import React from "react";
|
||||
|
||||
import { useI18n } from "../i18n";
|
||||
import { TranslationKeys, useI18n } from "../i18n";
|
||||
|
||||
// Used for splitting i18nKey into tokens in Trans component
|
||||
// Example:
|
||||
|
@ -153,7 +153,7 @@ const Trans = ({
|
|||
children,
|
||||
...props
|
||||
}: {
|
||||
i18nKey: string;
|
||||
i18nKey: TranslationKeys;
|
||||
[key: string]: React.ReactNode | ((el: React.ReactNode) => React.ReactNode);
|
||||
}) => {
|
||||
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
|
||||
data-testid="brave-measure-text-error"
|
||||
>
|
||||
|
|
|
@ -396,6 +396,14 @@ export const TrashIcon = createIcon(
|
|||
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(
|
||||
<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" />
|
||||
|
|
|
@ -4,6 +4,7 @@ import {
|
|||
useExcalidrawSetAppState,
|
||||
useExcalidrawActionManager,
|
||||
useExcalidrawElements,
|
||||
useAppProps,
|
||||
} from "../App";
|
||||
import {
|
||||
ExportIcon,
|
||||
|
@ -198,13 +199,20 @@ export const ChangeCanvasBackground = () => {
|
|||
const { t } = useI18n();
|
||||
const appState = useUIAppState();
|
||||
const actionManager = useExcalidrawActionManager();
|
||||
const appProps = useAppProps();
|
||||
|
||||
if (appState.viewModeEnabled) {
|
||||
if (
|
||||
appState.viewModeEnabled ||
|
||||
!appProps.UIOptions.canvasActions.changeViewBackgroundColor
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<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")}
|
||||
</div>
|
||||
<div style={{ padding: "0 0.625rem" }}>
|
||||
|
|
|
@ -71,8 +71,18 @@ export enum EVENT {
|
|||
// custom events
|
||||
EXCALIDRAW_LINK = "excalidraw-link",
|
||||
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 = {
|
||||
TEST: "test",
|
||||
DEVELOPMENT: "development",
|
||||
|
@ -92,7 +102,7 @@ export const FONT_FAMILY = {
|
|||
export const THEME = {
|
||||
LIGHT: "light",
|
||||
DARK: "dark",
|
||||
};
|
||||
} as const;
|
||||
|
||||
export const FRAME_STYLE = {
|
||||
strokeColor: "#bbb" as ExcalidrawElement["strokeColor"],
|
||||
|
@ -301,3 +311,5 @@ export const DEFAULT_SIDEBAR = {
|
|||
name: "default",
|
||||
defaultTab: LIBRARY_SIDEBAR_TAB,
|
||||
} as const;
|
||||
|
||||
export const LIBRARY_DISABLED_TYPES = new Set(["embeddable", "image"] as const);
|
||||
|
|
|
@ -77,6 +77,19 @@
|
|||
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 {
|
||||
// The percentage is inspired by
|
||||
// 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,
|
||||
ExcalidrawTextElement,
|
||||
FontFamilyValues,
|
||||
PointBinding,
|
||||
StrokeRoundness,
|
||||
} from "../element/types";
|
||||
import {
|
||||
|
@ -65,6 +66,7 @@ export const AllowedExcalidrawActiveTools: Record<
|
|||
eraser: false,
|
||||
custom: true,
|
||||
frame: true,
|
||||
embeddable: true,
|
||||
hand: true,
|
||||
};
|
||||
|
||||
|
@ -83,6 +85,13 @@ const getFontFamilyByName = (fontFamilyName: string): FontFamilyValues => {
|
|||
return DEFAULT_FONT_FAMILY;
|
||||
};
|
||||
|
||||
const repairBinding = (binding: PointBinding | null) => {
|
||||
if (!binding) {
|
||||
return null;
|
||||
}
|
||||
return { ...binding, focus: binding.focus || 0 };
|
||||
};
|
||||
|
||||
const restoreElementWithProperties = <
|
||||
T extends Required<Omit<ExcalidrawElement, "customData">> & {
|
||||
customData?: ExcalidrawElement["customData"];
|
||||
|
@ -258,8 +267,8 @@ const restoreElement = (
|
|||
(element.type as ExcalidrawElement["type"] | "draw") === "draw"
|
||||
? "line"
|
||||
: element.type,
|
||||
startBinding: element.startBinding,
|
||||
endBinding: element.endBinding,
|
||||
startBinding: repairBinding(element.startBinding),
|
||||
endBinding: repairBinding(element.endBinding),
|
||||
lastCommittedPoint: null,
|
||||
startArrowhead,
|
||||
endArrowhead,
|
||||
|
@ -276,6 +285,10 @@ const restoreElement = (
|
|||
return restoreElementWithProperties(element, {});
|
||||
case "diamond":
|
||||
return restoreElementWithProperties(element, {});
|
||||
case "embeddable":
|
||||
return restoreElementWithProperties(element, {
|
||||
validated: undefined,
|
||||
});
|
||||
case "frame":
|
||||
return restoreElementWithProperties(element, {
|
||||
name: element.name ?? null,
|
||||
|
|
|
@ -1,9 +1,35 @@
|
|||
import { sanitizeUrl } from "@braintree/sanitize-url";
|
||||
|
||||
export const normalizeLink = (link: string) => {
|
||||
link = link.trim();
|
||||
if (!link) {
|
||||
return link;
|
||||
}
|
||||
return sanitizeUrl(link);
|
||||
};
|
||||
|
||||
export const isLocalLink = (link: string | null) => {
|
||||
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 {
|
||||
color: $oc-red-6;
|
||||
}
|
||||
|
|
|
@ -5,8 +5,12 @@ import {
|
|||
viewportCoordsToSceneCoords,
|
||||
wrapEvent,
|
||||
} from "../utils";
|
||||
import { getEmbedLink, embeddableURLValidator } from "./embeddable";
|
||||
import { mutateElement } from "./mutateElement";
|
||||
import { NonDeletedExcalidrawElement } from "./types";
|
||||
import {
|
||||
ExcalidrawEmbeddableElement,
|
||||
NonDeletedExcalidrawElement,
|
||||
} from "./types";
|
||||
|
||||
import { register } from "../actions/register";
|
||||
import { ToolButton } from "../components/ToolButton";
|
||||
|
@ -21,7 +25,10 @@ import {
|
|||
} from "react";
|
||||
import clsx from "clsx";
|
||||
import { KEYS } from "../keys";
|
||||
import { DEFAULT_LINK_SIZE } from "../renderer/renderElement";
|
||||
import {
|
||||
DEFAULT_LINK_SIZE,
|
||||
invalidateShapeForElement,
|
||||
} from "../renderer/renderElement";
|
||||
import { rotate } from "../math";
|
||||
import { EVENT, HYPERLINK_TOOLTIP_DELAY, MIME_TYPES } from "../constants";
|
||||
import { Bounds } from "./bounds";
|
||||
|
@ -33,7 +40,8 @@ import { isLocalLink, normalizeLink } from "../data/url";
|
|||
|
||||
import "./Hyperlink.scss";
|
||||
import { trackEvent } from "../analytics";
|
||||
import { useExcalidrawAppState } from "../components/App";
|
||||
import { useAppProps, useExcalidrawAppState } from "../components/App";
|
||||
import { isEmbeddableElement } from "./typeChecks";
|
||||
|
||||
const CONTAINER_WIDTH = 320;
|
||||
const SPACE_BOTTOM = 85;
|
||||
|
@ -48,37 +56,112 @@ EXTERNAL_LINK_IMG.src = `data:${MIME_TYPES.svg}, ${encodeURIComponent(
|
|||
|
||||
let IS_HYPERLINK_TOOLTIP_VISIBLE = false;
|
||||
|
||||
const embeddableLinkCache = new Map<
|
||||
ExcalidrawEmbeddableElement["id"],
|
||||
string
|
||||
>();
|
||||
|
||||
export const Hyperlink = ({
|
||||
element,
|
||||
setAppState,
|
||||
onLinkOpen,
|
||||
setToast,
|
||||
}: {
|
||||
element: NonDeletedExcalidrawElement;
|
||||
setAppState: React.Component<any, AppState>["setState"];
|
||||
onLinkOpen: ExcalidrawProps["onLinkOpen"];
|
||||
setToast: (
|
||||
toast: { message: string; closable?: boolean; duration?: number } | null,
|
||||
) => void;
|
||||
}) => {
|
||||
const appState = useExcalidrawAppState();
|
||||
const appProps = useAppProps();
|
||||
|
||||
const linkVal = element.link || "";
|
||||
|
||||
const [inputVal, setInputVal] = useState(linkVal);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const isEditing = appState.showHyperlinkPopup === "editor" || !linkVal;
|
||||
const isEditing = appState.showHyperlinkPopup === "editor";
|
||||
|
||||
const handleSubmit = useCallback(() => {
|
||||
if (!inputRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const link = normalizeLink(inputRef.current.value);
|
||||
const link = normalizeLink(inputRef.current.value) || null;
|
||||
|
||||
if (!element.link && link) {
|
||||
trackEvent("hyperlink", "create");
|
||||
}
|
||||
|
||||
mutateElement(element, { link });
|
||||
setAppState({ showHyperlinkPopup: "info" });
|
||||
}, [element, setAppState]);
|
||||
if (isEmbeddableElement(element)) {
|
||||
if (appState.activeEmbeddable?.element === element) {
|
||||
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(() => {
|
||||
return () => {
|
||||
|
@ -132,10 +215,12 @@ export const Hyperlink = ({
|
|||
appState.draggingElement ||
|
||||
appState.resizingElement ||
|
||||
appState.isRotating ||
|
||||
appState.openMenu
|
||||
appState.openMenu ||
|
||||
appState.viewModeEnabled
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="excalidraw-hyperlinkContainer"
|
||||
|
@ -145,6 +230,11 @@ export const Hyperlink = ({
|
|||
width: CONTAINER_WIDTH,
|
||||
padding: CONTAINER_PADDING,
|
||||
}}
|
||||
onClick={() => {
|
||||
if (!element.link && !isEditing) {
|
||||
setAppState({ showHyperlinkPopup: "editor" });
|
||||
}
|
||||
}}
|
||||
>
|
||||
{isEditing ? (
|
||||
<input
|
||||
|
@ -162,15 +252,14 @@ export const Hyperlink = ({
|
|||
}
|
||||
if (event.key === KEYS.ENTER || event.key === KEYS.ESCAPE) {
|
||||
handleSubmit();
|
||||
setAppState({ showHyperlinkPopup: "info" });
|
||||
}
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
) : element.link ? (
|
||||
<a
|
||||
href={normalizeLink(element.link || "")}
|
||||
className={clsx("excalidraw-hyperlinkContainer-link", {
|
||||
"d-none": isEditing,
|
||||
})}
|
||||
className="excalidraw-hyperlinkContainer-link"
|
||||
target={isLocalLink(element.link) ? "_self" : "_blank"}
|
||||
onClick={(event) => {
|
||||
if (element.link && onLinkOpen) {
|
||||
|
@ -194,6 +283,10 @@ export const Hyperlink = ({
|
|||
>
|
||||
{element.link}
|
||||
</a>
|
||||
) : (
|
||||
<div className="excalidraw-hyperlinkContainer-link">
|
||||
{t("labels.link.empty")}
|
||||
</div>
|
||||
)}
|
||||
<div className="excalidraw-hyperlinkContainer__buttons">
|
||||
{!isEditing && (
|
||||
|
@ -207,8 +300,7 @@ export const Hyperlink = ({
|
|||
icon={FreedrawIcon}
|
||||
/>
|
||||
)}
|
||||
|
||||
{linkVal && (
|
||||
{linkVal && !isEmbeddableElement(element) && (
|
||||
<ToolButton
|
||||
type="button"
|
||||
title={t("buttons.remove")}
|
||||
|
@ -271,7 +363,11 @@ export const actionLink = register({
|
|||
type="button"
|
||||
icon={LinkIcon}
|
||||
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)}
|
||||
selected={selectedElements.length === 1 && !!selectedElements[0].link}
|
||||
/>
|
||||
|
@ -285,7 +381,11 @@ export const getContextMenuLabel = (
|
|||
) => {
|
||||
const selectedElements = getSelectedElements(elements, appState);
|
||||
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";
|
||||
return label;
|
||||
};
|
||||
|
@ -327,6 +427,26 @@ export const isPointHittingLinkIcon = (
|
|||
element: NonDeletedExcalidrawElement,
|
||||
appState: AppState,
|
||||
[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,
|
||||
) => {
|
||||
if (!element.link || appState.selectedElementIds[element.id]) {
|
||||
|
@ -340,19 +460,7 @@ export const isPointHittingLinkIcon = (
|
|||
) {
|
||||
return true;
|
||||
}
|
||||
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;
|
||||
return isPointHittingLinkIcon(element, appState, [x, y]);
|
||||
};
|
||||
|
||||
let HYPERLINK_TOOLTIP_TIMEOUT_ID: number | null = null;
|
||||
|
|
|
@ -18,6 +18,7 @@ import {
|
|||
ExcalidrawBindableElement,
|
||||
ExcalidrawElement,
|
||||
ExcalidrawRectangleElement,
|
||||
ExcalidrawEmbeddableElement,
|
||||
ExcalidrawDiamondElement,
|
||||
ExcalidrawTextElement,
|
||||
ExcalidrawEllipseElement,
|
||||
|
@ -39,7 +40,11 @@ import { FrameNameBoundsCache, Point } from "../types";
|
|||
import { Drawable } from "roughjs/bin/core";
|
||||
import { AppState } from "../types";
|
||||
import { getShapeForElement } from "../renderer/renderElement";
|
||||
import { hasBoundTextElement, isImageElement } from "./typeChecks";
|
||||
import {
|
||||
hasBoundTextElement,
|
||||
isEmbeddableElement,
|
||||
isImageElement,
|
||||
} from "./typeChecks";
|
||||
import { isTextElement } from ".";
|
||||
import { isTransparent } from "../utils";
|
||||
import { shouldShowBoundingBox } from "./transformHandles";
|
||||
|
@ -57,7 +62,9 @@ const isElementDraggableFromInside = (
|
|||
return true;
|
||||
}
|
||||
const isDraggableFromInside =
|
||||
!isTransparent(element.backgroundColor) || hasBoundTextElement(element);
|
||||
!isTransparent(element.backgroundColor) ||
|
||||
hasBoundTextElement(element) ||
|
||||
isEmbeddableElement(element);
|
||||
if (element.type === "line") {
|
||||
return isDraggableFromInside && isPathALoop(element.points);
|
||||
}
|
||||
|
@ -248,6 +255,7 @@ type HitTestArgs = {
|
|||
const hitTestPointAgainstElement = (args: HitTestArgs): boolean => {
|
||||
switch (args.element.type) {
|
||||
case "rectangle":
|
||||
case "embeddable":
|
||||
case "image":
|
||||
case "text":
|
||||
case "diamond":
|
||||
|
@ -306,6 +314,7 @@ export const distanceToBindableElement = (
|
|||
case "rectangle":
|
||||
case "image":
|
||||
case "text":
|
||||
case "embeddable":
|
||||
case "frame":
|
||||
return distanceToRectangle(element, point);
|
||||
case "diamond":
|
||||
|
@ -337,6 +346,7 @@ const distanceToRectangle = (
|
|||
| ExcalidrawTextElement
|
||||
| ExcalidrawFreeDrawElement
|
||||
| ExcalidrawImageElement
|
||||
| ExcalidrawEmbeddableElement
|
||||
| ExcalidrawFrameElement,
|
||||
point: Point,
|
||||
): number => {
|
||||
|
@ -645,17 +655,23 @@ export const determineFocusDistance = (
|
|||
const c = line[1];
|
||||
const mabs = Math.abs(m);
|
||||
const nabs = Math.abs(n);
|
||||
let ret;
|
||||
switch (element.type) {
|
||||
case "rectangle":
|
||||
case "image":
|
||||
case "text":
|
||||
case "embeddable":
|
||||
case "frame":
|
||||
return c / (hwidth * (nabs + q * mabs));
|
||||
ret = c / (hwidth * (nabs + q * mabs));
|
||||
break;
|
||||
case "diamond":
|
||||
return mabs < nabs ? c / (nabs * hwidth) : c / (mabs * hheight);
|
||||
ret = mabs < nabs ? c / (nabs * hwidth) : c / (mabs * hheight);
|
||||
break;
|
||||
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 = (
|
||||
|
@ -682,6 +698,7 @@ export const determineFocusPoint = (
|
|||
case "image":
|
||||
case "text":
|
||||
case "diamond":
|
||||
case "embeddable":
|
||||
case "frame":
|
||||
point = findFocusPointForRectangulars(element, focus, adjecentPointRel);
|
||||
break;
|
||||
|
@ -733,6 +750,7 @@ const getSortedElementLineIntersections = (
|
|||
case "image":
|
||||
case "text":
|
||||
case "diamond":
|
||||
case "embeddable":
|
||||
case "frame":
|
||||
const corners = getCorners(element);
|
||||
intersections = corners
|
||||
|
@ -768,6 +786,7 @@ const getCorners = (
|
|||
| ExcalidrawImageElement
|
||||
| ExcalidrawDiamondElement
|
||||
| ExcalidrawTextElement
|
||||
| ExcalidrawEmbeddableElement
|
||||
| ExcalidrawFrameElement,
|
||||
scale: number = 1,
|
||||
): GA.Point[] => {
|
||||
|
@ -777,6 +796,7 @@ const getCorners = (
|
|||
case "rectangle":
|
||||
case "image":
|
||||
case "text":
|
||||
case "embeddable":
|
||||
case "frame":
|
||||
return [
|
||||
GA.point(hx, hy),
|
||||
|
@ -926,6 +946,7 @@ export const findFocusPointForRectangulars = (
|
|||
| ExcalidrawImageElement
|
||||
| ExcalidrawDiamondElement
|
||||
| ExcalidrawTextElement
|
||||
| ExcalidrawEmbeddableElement
|
||||
| ExcalidrawFrameElement,
|
||||
// Between -1 and 1 for how far away should the focus point be relative
|
||||
// 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,
|
||||
ExcalidrawTextContainer,
|
||||
ExcalidrawFrameElement,
|
||||
ExcalidrawEmbeddableElement,
|
||||
} from "../element/types";
|
||||
import {
|
||||
arrayToMap,
|
||||
|
@ -130,6 +131,18 @@ export const newElement = (
|
|||
): NonDeleted<ExcalidrawGenericElement> =>
|
||||
_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 = (
|
||||
opts: ElementConstructorOpts,
|
||||
): NonDeleted<ExcalidrawFrameElement> => {
|
||||
|
@ -177,7 +190,6 @@ export const newTextElement = (
|
|||
containerId?: ExcalidrawTextContainer["id"] | null;
|
||||
lineHeight?: ExcalidrawTextElement["lineHeight"];
|
||||
strokeWidth?: ExcalidrawTextElement["strokeWidth"];
|
||||
isFrameName?: boolean;
|
||||
} & ElementConstructorOpts,
|
||||
): NonDeleted<ExcalidrawTextElement> => {
|
||||
const fontFamily = opts.fontFamily || DEFAULT_FONT_FAMILY;
|
||||
|
@ -212,7 +224,6 @@ export const newTextElement = (
|
|||
containerId: opts.containerId || null,
|
||||
originalText: text,
|
||||
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
|
||||
// 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.
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
if (import.meta.env.DEV) {
|
||||
if (
|
||||
objectType !== "[object Object]" &&
|
||||
objectType !== "[object Array]" &&
|
||||
|
|
|
@ -7,11 +7,11 @@ export const showSelectedShapeActions = (
|
|||
elements: readonly NonDeletedExcalidrawElement[],
|
||||
) =>
|
||||
Boolean(
|
||||
(!appState.viewModeEnabled &&
|
||||
appState.activeTool.type !== "custom" &&
|
||||
(appState.editingElement ||
|
||||
(appState.activeTool.type !== "selection" &&
|
||||
appState.activeTool.type !== "eraser" &&
|
||||
appState.activeTool.type !== "hand"))) ||
|
||||
getSelectedElements(elements, appState).length,
|
||||
!appState.viewModeEnabled &&
|
||||
((appState.activeTool.type !== "custom" &&
|
||||
(appState.editingElement ||
|
||||
(appState.activeTool.type !== "selection" &&
|
||||
appState.activeTool.type !== "eraser" &&
|
||||
appState.activeTool.type !== "hand"))) ||
|
||||
getSelectedElements(elements, appState).length),
|
||||
);
|
||||
|
|
|
@ -1,19 +1,32 @@
|
|||
import { vi } from "vitest";
|
||||
import { getPerfectElementSize } from "./sizeHelpers";
|
||||
import * as constants from "../constants";
|
||||
|
||||
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", () => {
|
||||
it("should return height:0 if `elementType` is line and locked angle is 0", () => {
|
||||
const { height, width } = getPerfectElementSize("line", 149, 10);
|
||||
expect(width).toBeCloseTo(149, 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)", () => {
|
||||
const { height, width } = getPerfectElementSize("line", 10, 140);
|
||||
expect(width).toBeCloseTo(0, EPSILON_DIGITS);
|
||||
expect(height).toBeCloseTo(140, EPSILON_DIGITS);
|
||||
});
|
||||
|
||||
it("should return height:0 if `elementType` is arrow and locked angle is 0", () => {
|
||||
const { height, width } = getPerfectElementSize("arrow", 200, 20);
|
||||
expect(width).toBeCloseTo(200, EPSILON_DIGITS);
|
||||
|
@ -24,16 +37,19 @@ describe("getPerfectElementSize", () => {
|
|||
expect(width).toBeCloseTo(0, EPSILON_DIGITS);
|
||||
expect(height).toBeCloseTo(100, EPSILON_DIGITS);
|
||||
});
|
||||
|
||||
it("should return adjust height to be width * tan(locked angle)", () => {
|
||||
const { height, width } = getPerfectElementSize("arrow", 120, 185);
|
||||
expect(width).toBeCloseTo(120, EPSILON_DIGITS);
|
||||
expect(height).toBeCloseTo(207.846, EPSILON_DIGITS);
|
||||
});
|
||||
|
||||
it("should return height equals to width if locked angle is 45 deg", () => {
|
||||
const { height, width } = getPerfectElementSize("arrow", 135, 145);
|
||||
expect(width).toBeCloseTo(135, EPSILON_DIGITS);
|
||||
expect(height).toBeCloseTo(135, EPSILON_DIGITS);
|
||||
});
|
||||
|
||||
it("should return height:0 and width:0 when width and height are 0", () => {
|
||||
const { height, width } = getPerfectElementSize("arrow", 0, 0);
|
||||
expect(width).toBeCloseTo(0, EPSILON_DIGITS);
|
||||
|
|
|
@ -955,7 +955,7 @@ describe("textWysiwyg", () => {
|
|||
// should center align horizontally and vertically by default
|
||||
resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 100]);
|
||||
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
[
|
||||
85,
|
||||
4.5,
|
||||
]
|
||||
|
@ -979,7 +979,7 @@ describe("textWysiwyg", () => {
|
|||
// should left align horizontally and bottom vertically after resize
|
||||
resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 100]);
|
||||
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
[
|
||||
15,
|
||||
65,
|
||||
]
|
||||
|
@ -1001,7 +1001,7 @@ describe("textWysiwyg", () => {
|
|||
// should right align horizontally and top vertically after resize
|
||||
resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 100]);
|
||||
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
[
|
||||
375,
|
||||
-539,
|
||||
]
|
||||
|
@ -1279,7 +1279,7 @@ describe("textWysiwyg", () => {
|
|||
fireEvent.click(screen.getByTitle("Left"));
|
||||
fireEvent.click(screen.getByTitle("Align top"));
|
||||
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
[
|
||||
15,
|
||||
25,
|
||||
]
|
||||
|
@ -1290,7 +1290,7 @@ describe("textWysiwyg", () => {
|
|||
fireEvent.click(screen.getByTitle("Center"));
|
||||
fireEvent.click(screen.getByTitle("Align top"));
|
||||
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
[
|
||||
30,
|
||||
25,
|
||||
]
|
||||
|
@ -1302,7 +1302,7 @@ describe("textWysiwyg", () => {
|
|||
fireEvent.click(screen.getByTitle("Align top"));
|
||||
|
||||
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
[
|
||||
45,
|
||||
25,
|
||||
]
|
||||
|
@ -1313,7 +1313,7 @@ describe("textWysiwyg", () => {
|
|||
fireEvent.click(screen.getByTitle("Center vertically"));
|
||||
fireEvent.click(screen.getByTitle("Left"));
|
||||
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
[
|
||||
15,
|
||||
45,
|
||||
]
|
||||
|
@ -1325,7 +1325,7 @@ describe("textWysiwyg", () => {
|
|||
fireEvent.click(screen.getByTitle("Center vertically"));
|
||||
|
||||
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
[
|
||||
30,
|
||||
45,
|
||||
]
|
||||
|
@ -1337,7 +1337,7 @@ describe("textWysiwyg", () => {
|
|||
fireEvent.click(screen.getByTitle("Center vertically"));
|
||||
|
||||
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
[
|
||||
45,
|
||||
45,
|
||||
]
|
||||
|
@ -1349,7 +1349,7 @@ describe("textWysiwyg", () => {
|
|||
fireEvent.click(screen.getByTitle("Align bottom"));
|
||||
|
||||
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
[
|
||||
15,
|
||||
65,
|
||||
]
|
||||
|
@ -1360,7 +1360,7 @@ describe("textWysiwyg", () => {
|
|||
fireEvent.click(screen.getByTitle("Center"));
|
||||
fireEvent.click(screen.getByTitle("Align bottom"));
|
||||
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
[
|
||||
30,
|
||||
65,
|
||||
]
|
||||
|
@ -1371,7 +1371,7 @@ describe("textWysiwyg", () => {
|
|||
fireEvent.click(screen.getByTitle("Right"));
|
||||
fireEvent.click(screen.getByTitle("Align bottom"));
|
||||
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
[
|
||||
45,
|
||||
65,
|
||||
]
|
||||
|
|
|
@ -4,6 +4,7 @@ import { MarkNonNullable } from "../utility-types";
|
|||
import {
|
||||
ExcalidrawElement,
|
||||
ExcalidrawTextElement,
|
||||
ExcalidrawEmbeddableElement,
|
||||
ExcalidrawLinearElement,
|
||||
ExcalidrawBindableElement,
|
||||
ExcalidrawGenericElement,
|
||||
|
@ -24,7 +25,8 @@ export const isGenericElement = (
|
|||
(element.type === "selection" ||
|
||||
element.type === "rectangle" ||
|
||||
element.type === "diamond" ||
|
||||
element.type === "ellipse")
|
||||
element.type === "ellipse" ||
|
||||
element.type === "embeddable")
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -40,6 +42,12 @@ export const isImageElement = (
|
|||
return !!element && element.type === "image";
|
||||
};
|
||||
|
||||
export const isEmbeddableElement = (
|
||||
element: ExcalidrawElement | null | undefined,
|
||||
): element is ExcalidrawEmbeddableElement => {
|
||||
return !!element && element.type === "embeddable";
|
||||
};
|
||||
|
||||
export const isTextElement = (
|
||||
element: ExcalidrawElement | null,
|
||||
): element is ExcalidrawTextElement => {
|
||||
|
@ -112,6 +120,7 @@ export const isBindableElement = (
|
|||
element.type === "diamond" ||
|
||||
element.type === "ellipse" ||
|
||||
element.type === "image" ||
|
||||
element.type === "embeddable" ||
|
||||
(element.type === "text" && !element.containerId))
|
||||
);
|
||||
};
|
||||
|
@ -135,6 +144,7 @@ export const isExcalidrawElement = (element: any): boolean => {
|
|||
element?.type === "text" ||
|
||||
element?.type === "diamond" ||
|
||||
element?.type === "rectangle" ||
|
||||
element?.type === "embeddable" ||
|
||||
element?.type === "ellipse" ||
|
||||
element?.type === "arrow" ||
|
||||
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) =>
|
||||
type === "line" || type === "arrow" || type === "diamond";
|
||||
|
@ -193,17 +204,13 @@ export const canApplyRoundnessTypeToElement = (
|
|||
export const getDefaultRoundnessTypeForElement = (
|
||||
element: ExcalidrawElement,
|
||||
) => {
|
||||
if (
|
||||
element.type === "arrow" ||
|
||||
element.type === "line" ||
|
||||
element.type === "diamond"
|
||||
) {
|
||||
if (isUsingProportionalRadius(element.type)) {
|
||||
return {
|
||||
type: ROUNDNESS.PROPORTIONAL_RADIUS,
|
||||
};
|
||||
}
|
||||
|
||||
if (element.type === "rectangle") {
|
||||
if (isUsingAdaptiveRadius(element.type)) {
|
||||
return {
|
||||
type: ROUNDNESS.ADAPTIVE_RADIUS,
|
||||
};
|
||||
|
|
|
@ -84,6 +84,19 @@ export type ExcalidrawEllipseElement = _ExcalidrawElementBase & {
|
|||
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 &
|
||||
Readonly<{
|
||||
type: "image";
|
||||
|
@ -110,6 +123,7 @@ export type ExcalidrawFrameElement = _ExcalidrawElementBase & {
|
|||
export type ExcalidrawGenericElement =
|
||||
| ExcalidrawSelectionElement
|
||||
| ExcalidrawRectangleElement
|
||||
| ExcalidrawEmbeddableElement
|
||||
| ExcalidrawDiamondElement
|
||||
| ExcalidrawEllipseElement;
|
||||
|
||||
|
@ -156,6 +170,7 @@ export type ExcalidrawBindableElement =
|
|||
| ExcalidrawEllipseElement
|
||||
| ExcalidrawTextElement
|
||||
| ExcalidrawImageElement
|
||||
| ExcalidrawEmbeddableElement
|
||||
| ExcalidrawFrameElement;
|
||||
|
||||
export type ExcalidrawTextContainer =
|
||||
|
|
|
@ -171,10 +171,7 @@ class Collab extends PureComponent<Props, CollabState> {
|
|||
|
||||
appJotaiStore.set(collabAPIAtom, collabAPI);
|
||||
|
||||
if (
|
||||
process.env.NODE_ENV === ENV.TEST ||
|
||||
process.env.NODE_ENV === ENV.DEVELOPMENT
|
||||
) {
|
||||
if (import.meta.env.MODE === ENV.TEST || import.meta.env.DEV) {
|
||||
window.collab = window.collab || ({} as Window["collab"]);
|
||||
Object.defineProperties(window, {
|
||||
collab: {
|
||||
|
@ -333,7 +330,7 @@ class Collab extends PureComponent<Props, CollabState> {
|
|||
* Indicates whether to fetch files that are errored or pending and older
|
||||
* 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.
|
||||
*/
|
||||
forceFetchFiles?: boolean;
|
||||
|
@ -860,10 +857,7 @@ declare global {
|
|||
}
|
||||
}
|
||||
|
||||
if (
|
||||
process.env.NODE_ENV === ENV.TEST ||
|
||||
process.env.NODE_ENV === ENV.DEVELOPMENT
|
||||
) {
|
||||
if (import.meta.env.MODE === ENV.TEST || import.meta.env.DEV) {
|
||||
window.collab = window.collab || ({} as Window["collab"]);
|
||||
}
|
||||
|
||||
|
|
|
@ -19,7 +19,9 @@ export const AppWelcomeScreen: React.FC<{
|
|||
return (
|
||||
<a
|
||||
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}
|
||||
>
|
||||
Excalidraw+
|
||||
|
|
|
@ -6,7 +6,9 @@ export const ExcalidrawPlusAppLink = () => {
|
|||
}
|
||||
return (
|
||||
<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"
|
||||
rel="noreferrer"
|
||||
className="plus-button"
|
||||
|
|
|
@ -21,10 +21,12 @@ import { ResolutionType } from "../../utility-types";
|
|||
|
||||
let FIREBASE_CONFIG: Record<string, any>;
|
||||
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) {
|
||||
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 = {};
|
||||
}
|
||||
|
|
|
@ -47,8 +47,8 @@ export const getSyncableElements = (elements: readonly ExcalidrawElement[]) =>
|
|||
isSyncableElement(element),
|
||||
) as SyncableExcalidrawElement[];
|
||||
|
||||
const BACKEND_V2_GET = process.env.REACT_APP_BACKEND_V2_GET_URL;
|
||||
const BACKEND_V2_POST = process.env.REACT_APP_BACKEND_V2_POST_URL;
|
||||
const BACKEND_V2_GET = import.meta.env.VITE_APP_BACKEND_V2_GET_URL;
|
||||
const BACKEND_V2_POST = import.meta.env.VITE_APP_BACKEND_V2_POST_URL;
|
||||
|
||||
const generateRoomId = async () => {
|
||||
const buffer = new Uint8Array(ROOM_ID_BYTES);
|
||||
|
@ -67,16 +67,16 @@ export const getCollabServer = async (): Promise<{
|
|||
url: string;
|
||||
polling: boolean;
|
||||
}> => {
|
||||
if (process.env.REACT_APP_WS_SERVER_URL) {
|
||||
if (import.meta.env.VITE_APP_WS_SERVER_URL) {
|
||||
return {
|
||||
url: process.env.REACT_APP_WS_SERVER_URL,
|
||||
url: import.meta.env.VITE_APP_WS_SERVER_URL,
|
||||
polling: true,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
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();
|
||||
} catch (error) {
|
||||
|
|
|
@ -100,6 +100,20 @@ polyfill();
|
|||
|
||||
window.EXCALIDRAW_THROTTLE_RENDER = true;
|
||||
|
||||
let isSelfEmbedding = false;
|
||||
|
||||
if (window.self !== window.top) {
|
||||
try {
|
||||
const parentUrl = new URL(document.referrer);
|
||||
const currentUrl = new URL(window.location.href);
|
||||
if (parentUrl.origin === currentUrl.origin) {
|
||||
isSelfEmbedding = true;
|
||||
}
|
||||
} catch (error) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
const languageDetector = new LanguageDetector();
|
||||
languageDetector.init({
|
||||
languageUtils: {},
|
||||
|
@ -518,7 +532,9 @@ const ExcalidrawWrapper = () => {
|
|||
|
||||
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
|
||||
importFromLocalStorage().appState?.theme ||
|
||||
THEME.LIGHT,
|
||||
|
@ -641,6 +657,25 @@ const ExcalidrawWrapper = () => {
|
|||
|
||||
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 (
|
||||
<div
|
||||
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 =
|
||||
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
|
||||
const onlineEnv =
|
||||
|
@ -21,7 +21,7 @@ Sentry.init({
|
|||
? "https://7bfc596a5bf945eda6b660d3015a5460@sentry.io/5179260"
|
||||
: undefined,
|
||||
environment: onlineEnv ? SentryEnvHostnameMap[onlineEnv] : undefined,
|
||||
release: process.env.REACT_APP_GIT_SHA,
|
||||
release: import.meta.env.VITE_APP_GIT_SHA,
|
||||
ignoreErrors: [
|
||||
"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;
|
||||
}
|
||||
|
||||
// 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 {
|
||||
write(data: any[]): Promise<void>;
|
||||
}
|
||||
|
@ -120,3 +110,9 @@ declare module "image-blob-reduce" {
|
|||
const reduce: ImageBlobReduce.ImageBlobReduceStatic;
|
||||
export = reduce;
|
||||
}
|
||||
|
||||
declare namespace jest {
|
||||
interface Expect {
|
||||
toBeNonNaNNumber(): void;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@ const exportLibraryItemToSvg = async (elements: LibraryItem["elements"]) => {
|
|||
viewBackgroundColor: COLOR_PALETTE.white,
|
||||
},
|
||||
files: null,
|
||||
renderEmbeddables: false,
|
||||
});
|
||||
};
|
||||
|
||||
|
|
10
src/i18n.ts
10
src/i18n.ts
|
@ -1,8 +1,8 @@
|
|||
import fallbackLangData from "./locales/en.json";
|
||||
import percentages from "./locales/percentages.json";
|
||||
import { ENV } from "./constants";
|
||||
import { jotaiScope, jotaiStore } from "./jotai";
|
||||
import { atom, useAtomValue } from "jotai";
|
||||
import { NestedKeyOf } from "./utility-types";
|
||||
|
||||
const COMPLETION_THRESHOLD = 85;
|
||||
|
||||
|
@ -12,6 +12,8 @@ export interface Language {
|
|||
rtl?: boolean;
|
||||
}
|
||||
|
||||
export type TranslationKeys = NestedKeyOf<typeof fallbackLangData>;
|
||||
|
||||
export const defaultLang = { code: "en", label: "English" };
|
||||
|
||||
export const languages: Language[] = [
|
||||
|
@ -71,7 +73,7 @@ export const languages: Language[] = [
|
|||
];
|
||||
|
||||
const TEST_LANG_CODE = "__test__";
|
||||
if (process.env.NODE_ENV === ENV.DEVELOPMENT) {
|
||||
if (import.meta.env.DEV) {
|
||||
languages.unshift(
|
||||
{ code: TEST_LANG_CODE, label: "test language" },
|
||||
{
|
||||
|
@ -123,7 +125,7 @@ const findPartsForData = (data: any, parts: string[]) => {
|
|||
};
|
||||
|
||||
export const t = (
|
||||
path: string,
|
||||
path: NestedKeyOf<typeof fallbackLangData>,
|
||||
replacement?: { [key: string]: string | number } | null,
|
||||
fallback?: string,
|
||||
) => {
|
||||
|
@ -142,7 +144,7 @@ export const t = (
|
|||
if (translation === undefined) {
|
||||
const errorMessage = `Can't find translation for ${path}`;
|
||||
// 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);
|
||||
return "";
|
||||
}
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
import { StrictMode } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import ExcalidrawApp from "./excalidraw-app";
|
||||
import { registerSW } from "virtual:pwa-register";
|
||||
|
||||
import "./excalidraw-app/pwa";
|
||||
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 root = createRoot(rootElement);
|
||||
registerSW();
|
||||
|
||||
root.render(
|
||||
<StrictMode>
|
||||
<ExcalidrawApp />
|
||||
|
|
|
@ -65,6 +65,7 @@ export const KEYS = {
|
|||
Y: "y",
|
||||
Z: "z",
|
||||
K: "k",
|
||||
W: "w",
|
||||
|
||||
0: "0",
|
||||
1: "1",
|
||||
|
|
|
@ -109,8 +109,12 @@
|
|||
"createContainerFromText": "Wrap text in a container",
|
||||
"link": {
|
||||
"edit": "Edit link",
|
||||
"editEmbed": "Edit link & embed",
|
||||
"create": "Create link",
|
||||
"label": "Link"
|
||||
"createEmbed": "Create link & embed",
|
||||
"label": "Link",
|
||||
"labelEmbed": "Link & embed",
|
||||
"empty": "No link is set"
|
||||
},
|
||||
"lineEditor": {
|
||||
"edit": "Edit line",
|
||||
|
@ -164,9 +168,11 @@
|
|||
"cancel": "Cancel",
|
||||
"clear": "Clear",
|
||||
"remove": "Remove",
|
||||
"embed": "Toggle embedding",
|
||||
"publishLibrary": "Publish",
|
||||
"submit": "Submit",
|
||||
"confirm": "Confirm"
|
||||
"confirm": "Confirm",
|
||||
"embeddableInteractionButton": "Click to interact"
|
||||
},
|
||||
"alerts": {
|
||||
"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.",
|
||||
"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>"
|
||||
},
|
||||
"libraryElementTypeError": {
|
||||
"embeddable": "Embeddable elements cannot be added to the library.",
|
||||
"image": "Support for adding images to the library coming soon!"
|
||||
}
|
||||
},
|
||||
"toolBar": {
|
||||
|
@ -224,6 +234,7 @@
|
|||
"link": "Add/ Update link for a selected shape",
|
||||
"eraser": "Eraser",
|
||||
"frame": "Frame tool",
|
||||
"embeddable": "Web Embed",
|
||||
"hand": "Hand (panning tool)",
|
||||
"extraTools": "More tools"
|
||||
},
|
||||
|
@ -237,6 +248,7 @@
|
|||
"linearElement": "Click to start multiple points, drag for single line",
|
||||
"freeDraw": "Click and drag, release when you're finished",
|
||||
"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_editing": "Press Escape or CtrlOrCmd+ENTER to finish editing",
|
||||
"linearElementMulti": "Click on last point or press Escape or Enter to finish",
|
||||
|
@ -411,7 +423,9 @@
|
|||
"fileSavedToFilename": "Saved to {filename}",
|
||||
"canvas": "canvas",
|
||||
"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": {
|
||||
"transparent": "Transparent",
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
[
|
||||
{
|
||||
"path": "dist/excalidraw.production.min.js",
|
||||
"limit": "285 kB"
|
||||
"limit": "290 kB"
|
||||
},
|
||||
{
|
||||
"path": "dist/excalidraw-assets/locales",
|
||||
|
|
|
@ -13,8 +13,27 @@ Please add the latest change on the top under the correct section.
|
|||
|
||||
## 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
|
||||
|
||||
- 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).
|
||||
- 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)
|
||||
|
|
|
@ -9,9 +9,9 @@ const parseEnvVariables = (filepath) => {
|
|||
},
|
||||
{},
|
||||
);
|
||||
envVars.PKG_NAME = JSON.stringify(pkg.name);
|
||||
envVars.PKG_VERSION = JSON.stringify(pkg.version);
|
||||
envVars.IS_EXCALIDRAW_NPM_PACKAGE = JSON.stringify(true);
|
||||
envVars.VITE_PKG_NAME = JSON.stringify(pkg.name);
|
||||
envVars.VITE_PKG_VERSION = JSON.stringify(pkg.version);
|
||||
envVars.VITE_IS_EXCALIDRAW_NPM_PACKAGE = JSON.stringify(true);
|
||||
return envVars;
|
||||
};
|
||||
|
||||
|
|
|
@ -26,7 +26,7 @@ import {
|
|||
LibraryItems,
|
||||
PointerDownState as ExcalidrawPointerDownState,
|
||||
} from "../../../types";
|
||||
import { NonDeletedExcalidrawElement } from "../../../element/types";
|
||||
import { NonDeletedExcalidrawElement, Theme } from "../../../element/types";
|
||||
import { ImportedLibraryData } from "../../../data/types";
|
||||
import CustomFooter from "./CustomFooter";
|
||||
import MobileFooter from "./MobileFooter";
|
||||
|
@ -97,7 +97,7 @@ export default function App({ appTitle, useCustom, customArgs }: AppProps) {
|
|||
const [canvasUrl, setCanvasUrl] = useState<string>("");
|
||||
const [exportWithDarkMode, setExportWithDarkMode] = useState(false);
|
||||
const [exportEmbedScene, setExportEmbedScene] = useState(false);
|
||||
const [theme, setTheme] = useState("light");
|
||||
const [theme, setTheme] = useState<Theme>("light");
|
||||
const [isCollaborating, setIsCollaborating] = useState(false);
|
||||
const [commentIcons, setCommentIcons] = useState<{ [id: string]: Comment }>(
|
||||
{},
|
||||
|
@ -601,11 +601,7 @@ export default function App({ appTitle, useCustom, customArgs }: AppProps) {
|
|||
type="checkbox"
|
||||
checked={theme === "dark"}
|
||||
onChange={() => {
|
||||
let newTheme = "light";
|
||||
if (theme === "light") {
|
||||
newTheme = "dark";
|
||||
}
|
||||
setTheme(newTheme);
|
||||
setTheme(theme === "light" ? "dark" : "light");
|
||||
}}
|
||||
/>
|
||||
Switch to Dark Theme
|
||||
|
@ -684,11 +680,17 @@ export default function App({ appTitle, useCustom, customArgs }: AppProps) {
|
|||
gridModeEnabled={gridModeEnabled}
|
||||
theme={theme}
|
||||
name="Custom name of drawing"
|
||||
UIOptions={{ canvasActions: { loadScene: false } }}
|
||||
UIOptions={{
|
||||
canvasActions: {
|
||||
loadScene: false,
|
||||
},
|
||||
}}
|
||||
renderTopRightUI={renderTopRightUI}
|
||||
onLinkOpen={onLinkOpen}
|
||||
onPointerDown={onPointerDown}
|
||||
onScrollChange={rerenderCommentIcons}
|
||||
// allow all urls
|
||||
validateEmbeddable={true}
|
||||
>
|
||||
{excalidrawAPI && (
|
||||
<Footer>
|
||||
|
|
|
@ -42,6 +42,8 @@ const ExcalidrawBase = (props: ExcalidrawProps) => {
|
|||
onPointerDown,
|
||||
onScrollChange,
|
||||
children,
|
||||
validateEmbeddable,
|
||||
renderEmbeddable,
|
||||
} = props;
|
||||
|
||||
const canvasActions = props.UIOptions?.canvasActions;
|
||||
|
@ -115,6 +117,8 @@ const ExcalidrawBase = (props: ExcalidrawProps) => {
|
|||
onLinkOpen={onLinkOpen}
|
||||
onPointerDown={onPointerDown}
|
||||
onScrollChange={onScrollChange}
|
||||
validateEmbeddable={validateEmbeddable}
|
||||
renderEmbeddable={renderEmbeddable}
|
||||
>
|
||||
{children}
|
||||
</App>
|
||||
|
|
|
@ -52,16 +52,19 @@
|
|||
"@babel/preset-env": "7.18.6",
|
||||
"@babel/preset-react": "7.18.6",
|
||||
"@babel/preset-typescript": "7.18.6",
|
||||
"@size-limit/preset-big-lib": "8.2.6",
|
||||
"autoprefixer": "10.4.7",
|
||||
"babel-loader": "8.2.5",
|
||||
"babel-plugin-transform-class-properties": "6.24.1",
|
||||
"cross-env": "7.0.3",
|
||||
"css-loader": "6.7.1",
|
||||
"dotenv": "16.0.1",
|
||||
"import-meta-loader": "1.1.0",
|
||||
"mini-css-extract-plugin": "2.6.1",
|
||||
"postcss-loader": "7.0.1",
|
||||
"sass-loader": "13.0.2",
|
||||
"size-limit": "8.2.4",
|
||||
"style-loader": "3.3.3",
|
||||
"terser-webpack-plugin": "5.3.3",
|
||||
"ts-loader": "9.3.1",
|
||||
"typescript": "4.9.4",
|
||||
|
|
|
@ -4,5 +4,5 @@ if (process.env.NODE_ENV !== ENV.TEST) {
|
|||
/* global __webpack_public_path__:writable */
|
||||
__webpack_public_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:
|
||||
/node_modules\/(?!(browser-fs-access|canvas-roundrect-polyfill))/,
|
||||
use: [
|
||||
{
|
||||
loader: "import-meta-loader",
|
||||
},
|
||||
{
|
||||
loader: "ts-loader",
|
||||
options: {
|
||||
|
|
|
@ -50,6 +50,9 @@ module.exports = {
|
|||
/node_modules\/(?!(browser-fs-access|canvas-roundrect-polyfill))/,
|
||||
|
||||
use: [
|
||||
{
|
||||
loader: "import-meta-loader",
|
||||
},
|
||||
{
|
||||
loader: "ts-loader",
|
||||
options: {
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -178,8 +178,10 @@ export const exportToSvg = async ({
|
|||
appState = getDefaultAppState(),
|
||||
files = {},
|
||||
exportPadding,
|
||||
renderEmbeddables,
|
||||
}: Omit<ExportOpts, "getDimensions"> & {
|
||||
exportPadding?: number;
|
||||
renderEmbeddables?: boolean;
|
||||
}): Promise<SVGSVGElement> => {
|
||||
const { elements: restoredElements, appState: restoredAppState } = restore(
|
||||
{ elements, appState },
|
||||
|
@ -197,6 +199,7 @@ export const exportToSvg = async ({
|
|||
exportAppState,
|
||||
files,
|
||||
{
|
||||
renderEmbeddables,
|
||||
// NOTE as long as we're using the Scene hack, we need to ensure
|
||||
// we pass the original, uncloned elements when serializing
|
||||
// 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 { 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 rough from "roughjs/bin/rough";
|
||||
import { AppState, BinaryFiles, Zoom } from "../types";
|
||||
|
@ -49,8 +55,12 @@ import {
|
|||
getBoundTextMaxWidth,
|
||||
} from "../element/textElement";
|
||||
import { LinearElementEditor } from "../element/linearElementEditor";
|
||||
import {
|
||||
createPlaceholderEmbeddableLabel,
|
||||
getEmbedLink,
|
||||
} from "../element/embeddable";
|
||||
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
|
||||
// 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;
|
||||
switch (element.type) {
|
||||
case "rectangle":
|
||||
case "embeddable":
|
||||
case "diamond":
|
||||
case "ellipse": {
|
||||
context.lineJoin = "round";
|
||||
|
@ -427,13 +438,13 @@ export const generateRoughOptions = (
|
|||
|
||||
switch (element.type) {
|
||||
case "rectangle":
|
||||
case "embeddable":
|
||||
case "diamond":
|
||||
case "ellipse": {
|
||||
options.fillStyle = element.fillStyle;
|
||||
options.fill =
|
||||
element.backgroundColor === "transparent"
|
||||
? undefined
|
||||
: element.backgroundColor;
|
||||
options.fill = isTransparent(element.backgroundColor)
|
||||
? undefined
|
||||
: element.backgroundColor;
|
||||
if (element.type === "ellipse") {
|
||||
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.
|
||||
* @param element
|
||||
|
@ -466,8 +497,9 @@ export const generateRoughOptions = (
|
|||
const generateElementShape = (
|
||||
element: NonDeletedExcalidrawElement,
|
||||
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
|
||||
// (= do not generate anything)
|
||||
|
@ -475,7 +507,11 @@ const generateElementShape = (
|
|||
elementWithCanvasCache.delete(element);
|
||||
|
||||
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) {
|
||||
const w = element.width;
|
||||
const h = element.height;
|
||||
|
@ -486,7 +522,10 @@ const generateElementShape = (
|
|||
} Q ${w} ${h}, ${w - r} ${h} L ${r} ${h} Q 0 ${h}, 0 ${
|
||||
h - r
|
||||
} L 0 ${r} Q 0 0, ${r} 0`,
|
||||
generateRoughOptions(element, true),
|
||||
generateRoughOptions(
|
||||
modifyEmbeddableForRoughOptions(element, isExporting),
|
||||
true,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
shape = generator.rectangle(
|
||||
|
@ -494,7 +533,10 @@ const generateElementShape = (
|
|||
0,
|
||||
element.width,
|
||||
element.height,
|
||||
generateRoughOptions(element),
|
||||
generateRoughOptions(
|
||||
modifyEmbeddableForRoughOptions(element, isExporting),
|
||||
false,
|
||||
),
|
||||
);
|
||||
}
|
||||
setShapeForElement(element, shape);
|
||||
|
@ -873,7 +915,7 @@ const drawElementFromCanvas = (
|
|||
);
|
||||
|
||||
if (
|
||||
process.env.REACT_APP_DEBUG_ENABLE_TEXT_CONTAINER_BOUNDING_BOX ===
|
||||
import.meta.env.VITE_APP_DEBUG_ENABLE_TEXT_CONTAINER_BOUNDING_BOX ===
|
||||
"true" &&
|
||||
hasBoundTextElement(element)
|
||||
) {
|
||||
|
@ -996,8 +1038,9 @@ export const renderElement = (
|
|||
case "line":
|
||||
case "arrow":
|
||||
case "image":
|
||||
case "text": {
|
||||
generateElementShape(element, generator);
|
||||
case "text":
|
||||
case "embeddable": {
|
||||
generateElementShape(element, generator, renderConfig.isExporting);
|
||||
if (renderConfig.isExporting) {
|
||||
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
|
||||
const cx = (x1 + x2) / 2 + renderConfig.scrollX;
|
||||
|
@ -1180,7 +1223,9 @@ export const renderElementToSvg = (
|
|||
offsetY: number,
|
||||
exportWithDarkMode?: boolean,
|
||||
exportingFrameId?: string | null,
|
||||
renderEmbeddables?: boolean,
|
||||
) => {
|
||||
const offset = { x: offsetX, y: offsetY };
|
||||
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
|
||||
let cx = (x2 - x1) / 2 - (element.x - x1);
|
||||
let cy = (y2 - y1) / 2 - (element.y - y1);
|
||||
|
@ -1253,6 +1298,106 @@ export const renderElementToSvg = (
|
|||
g ? root.appendChild(g) : root.appendChild(node);
|
||||
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 "arrow": {
|
||||
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