Compare commits

..

3 Commits

Author SHA1 Message Date
dwelle
9bf3466cca Merge branch 'master' into zsviczian-fix-exportToSvg
# Conflicts:
#	src/scene/export.ts
2023-07-28 11:26:43 +02:00
zsviczian
9a10503c5f
Update export.ts 2022-12-16 23:24:08 +01:00
zsviczian
d87e080e88
Update export.ts 2022-12-16 23:12:03 +01:00
42 changed files with 618 additions and 3798 deletions

View File

@ -1,5 +0,0 @@
FROM node:18-bullseye
# Vite wants to open the browser using `open`, so we
# need to install those utils.
RUN apt update -y && apt install -y xdg-utils

View File

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

View File

@ -1,5 +1,5 @@
VITE_APP_BACKEND_V2_GET_URL=https://json.excalidraw.com/api/v2/ REACT_APP_BACKEND_V2_GET_URL=https://json.excalidraw.com/api/v2/
VITE_APP_BACKEND_V2_POST_URL=https://json.excalidraw.com/api/v2/post/ REACT_APP_BACKEND_V2_POST_URL=https://json.excalidraw.com/api/v2/post/
VITE_APP_LIBRARY_URL=https://libraries.excalidraw.com VITE_APP_LIBRARY_URL=https://libraries.excalidraw.com
VITE_APP_LIBRARY_BACKEND=https://us-central1-excalidraw-room-persistence.cloudfunctions.net/libraries VITE_APP_LIBRARY_BACKEND=https://us-central1-excalidraw-room-persistence.cloudfunctions.net/libraries

View File

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

2
.nvmrc
View File

@ -1 +1 @@
18 14

View File

@ -69,10 +69,6 @@ It's also a good idea to consider if your change should include additional tests
Finally - always manually test your changes using the convenient staging environment deployed for each pull request. As much as local development attempts to replicate production, there can still be subtle differences in behavior. For larger features consider testing your change in multiple browsers as well. Finally - always manually test your changes using the convenient staging environment deployed for each pull request. As much as local development attempts to replicate production, there can still be subtle differences in behavior. For larger features consider testing your change in multiple browsers as well.
:::note
Some checks, such as the `lint` and `test`, require approval from the maintainers to run.
They will appear as `Expected — Waiting for status to be reported` in the PR checks when they are waiting for approval.
:::
## Translating ## Translating

View File

@ -6611,19 +6611,19 @@ semver@7.0.0:
integrity sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A== integrity sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==
semver@^5.4.1: semver@^5.4.1:
version "5.7.2" version "5.7.1"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.2.tgz#48d55db737c3287cd4835e17fa13feace1c41ef8" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
integrity sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g== integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.2.0, semver@^6.3.0: semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.2.0, semver@^6.3.0:
version "6.3.1" version "6.3.0"
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7: semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7:
version "7.5.4" version "7.3.7"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.7.tgz#12c5b649afdbf9049707796e22a4028814ce523f"
integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== integrity sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==
dependencies: dependencies:
lru-cache "^6.0.0" lru-cache "^6.0.0"

View File

@ -70,7 +70,6 @@
"@types/resize-observer-browser": "0.1.7", "@types/resize-observer-browser": "0.1.7",
"@types/socket.io-client": "1.4.36", "@types/socket.io-client": "1.4.36",
"@vitejs/plugin-react": "3.1.0", "@vitejs/plugin-react": "3.1.0",
"@vitest/coverage-v8": "0.33.0",
"@vitest/ui": "0.32.2", "@vitest/ui": "0.32.2",
"chai": "4.3.6", "chai": "4.3.6",
"dotenv": "16.0.1", "dotenv": "16.0.1",
@ -102,8 +101,8 @@
"private": true, "private": true,
"scripts": { "scripts": {
"build-node": "node ./scripts/build-node.js", "build-node": "node ./scripts/build-node.js",
"build:app:docker": "cross-env VITE_APP_DISABLE_SENTRY=true VITE_APP_DISABLE_TRACKING=true vite build", "build:app:docker": "cross-env REACT_APP_DISABLE_SENTRY=true REACT_APP_DISABLE_TRACKING=true vite build",
"build:app": "cross-env VITE_APP_GIT_SHA=$VERCEL_GIT_COMMIT_SHA vite build", "build:app": "cross-env REACT_APP_GIT_SHA=$VERCEL_GIT_COMMIT_SHA vite build",
"build:version": "node ./scripts/build-version.js", "build:version": "node ./scripts/build-version.js",
"build": "yarn build:app && yarn build:version", "build": "yarn build:app && yarn build:version",
"fix:code": "yarn test:code --fix", "fix:code": "yarn test:code --fix",
@ -115,15 +114,14 @@
"prettier": "prettier \"**/*.{css,scss,json,md,html,yml}\" --ignore-path=.eslintignore", "prettier": "prettier \"**/*.{css,scss,json,md,html,yml}\" --ignore-path=.eslintignore",
"start": "vite", "start": "vite",
"start:production": "npm run build && npx http-server build -a localhost -p 5001 -o", "start:production": "npm run build && npx http-server build -a localhost -p 5001 -o",
"test:all": "yarn test:typecheck && yarn test:code && yarn test:other && yarn test:app --watch=false", "test:all": "yarn test:typecheck && yarn test:code && yarn test:other && yarn test:app --watchAll=false",
"test:app": "vitest --config vitest.config.ts", "test:app": "vitest --config vitest.config.ts",
"test:code": "eslint --max-warnings=0 --ext .js,.ts,.tsx .", "test:code": "eslint --max-warnings=0 --ext .js,.ts,.tsx .",
"test:other": "yarn prettier --list-different", "test:other": "yarn prettier --list-different",
"test:typecheck": "tsc", "test:typecheck": "tsc",
"test:update": "yarn test:app --update --watch=false", "test:update": "yarn test:app --update --watch=false",
"test": "yarn test:app", "test": "yarn test:app",
"test:coverage": "vitest --coverage", "test:coverage": "vitest --coverage --watchAll",
"test:coverage:watch": "vitest --coverage --watch",
"test:ui": "yarn test --ui", "test:ui": "yarn test --ui",
"autorelease": "node scripts/autorelease.js", "autorelease": "node scripts/autorelease.js",
"prerelease": "node scripts/prerelease.js", "prerelease": "node scripts/prerelease.js",

View File

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

View File

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

View File

@ -298,6 +298,7 @@ import {
getApproxMinLineWidth, getApproxMinLineWidth,
getBoundTextElement, getBoundTextElement,
getContainerCenter, getContainerCenter,
getContainerDims,
getContainerElement, getContainerElement,
getDefaultLineHeight, getDefaultLineHeight,
getLineHeightInPx, getLineHeightInPx,
@ -346,10 +347,6 @@ import { activeConfirmDialogAtom } from "./ActiveConfirmDialog";
import { actionWrapTextInContainer } from "../actions/actionBoundText"; import { actionWrapTextInContainer } from "../actions/actionBoundText";
import BraveMeasureTextError from "./BraveMeasureTextError"; import BraveMeasureTextError from "./BraveMeasureTextError";
import { activeEyeDropperAtom } from "./EyeDropper"; import { activeEyeDropperAtom } from "./EyeDropper";
import {
ExcalidrawElementSkeleton,
convertToExcalidrawElements,
} from "../data/transform";
import { ValueOf } from "../utility-types"; import { ValueOf } from "../utility-types";
import { isSidebarDockedAtom } from "./Sidebar/Sidebar"; import { isSidebarDockedAtom } from "./Sidebar/Sidebar";
@ -944,7 +941,7 @@ class App extends React.Component<AppProps, AppState> {
title="Excalidraw Embedded Content" title="Excalidraw Embedded Content"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen={true} allowFullScreen={true}
sandbox="allow-same-origin allow-scripts allow-forms allow-popups allow-popups-to-escape-sandbox allow-presentation allow-downloads" sandbox="allow-same-origin allow-scripts allow-forms allow-popups allow-popups-to-escape-sandbox allow-presentation"
/> />
)} )}
</div> </div>
@ -2235,6 +2232,7 @@ class App extends React.Component<AppProps, AppState> {
let file = event?.clipboardData?.files[0]; let file = event?.clipboardData?.files[0];
const data = await parseClipboard(event, isPlainPaste); const data = await parseClipboard(event, isPlainPaste);
if (!file && data.text && !isPlainPaste) { if (!file && data.text && !isPlainPaste) {
const string = data.text.trim(); const string = data.text.trim();
if (string.startsWith("<svg") && string.endsWith("</svg>")) { if (string.startsWith("<svg") && string.endsWith("</svg>")) {
@ -2289,16 +2287,9 @@ class App extends React.Component<AppProps, AppState> {
}, },
}); });
} else if (data.elements) { } else if (data.elements) {
const elements = (
data.programmaticAPI
? convertToExcalidrawElements(
data.elements as ExcalidrawElementSkeleton[],
)
: data.elements
) as readonly ExcalidrawElement[];
// TODO remove formatting from elements if isPlainPaste // TODO remove formatting from elements if isPlainPaste
this.addElementsFromPasteOrLibrary({ this.addElementsFromPasteOrLibrary({
elements, elements: data.elements,
files: data.files || null, files: data.files || null,
position: "cursor", position: "cursor",
retainSeed: isPlainPaste, retainSeed: isPlainPaste,
@ -3557,8 +3548,9 @@ class App extends React.Component<AppProps, AppState> {
lineHeight, lineHeight,
); );
const minHeight = getApproxMinLineHeight(fontSize, lineHeight); const minHeight = getApproxMinLineHeight(fontSize, lineHeight);
const newHeight = Math.max(container.height, minHeight); const containerDims = getContainerDims(container);
const newWidth = Math.max(container.width, minWidth); const newHeight = Math.max(containerDims.height, minHeight);
const newWidth = Math.max(containerDims.width, minWidth);
mutateElement(container, { height: newHeight, width: newWidth }); mutateElement(container, { height: newHeight, width: newWidth });
sceneX = container.x + newWidth / 2; sceneX = container.x + newWidth / 2;
sceneY = container.y + newHeight / 2; sceneY = container.y + newHeight / 2;
@ -5405,7 +5397,7 @@ class App extends React.Component<AppProps, AppState> {
width: embedLink.aspectRatio.w, width: embedLink.aspectRatio.w,
height: embedLink.aspectRatio.h, height: embedLink.aspectRatio.h,
link, link,
validated: null, validated: undefined,
}); });
this.scene.replaceAllElements([ this.scene.replaceAllElements([
@ -5593,7 +5585,7 @@ class App extends React.Component<AppProps, AppState> {
} }
private createGenericElementOnPointerDown = ( private createGenericElementOnPointerDown = (
elementType: ExcalidrawGenericElement["type"] | "embeddable", elementType: ExcalidrawGenericElement["type"],
pointerDownState: PointerDownState, pointerDownState: PointerDownState,
): void => { ): void => {
const [gridX, gridY] = getGridPoint( const [gridX, gridY] = getGridPoint(
@ -5607,7 +5599,8 @@ class App extends React.Component<AppProps, AppState> {
y: gridY, y: gridY,
}); });
const baseElementAttributes = { const element = newElement({
type: elementType,
x: gridX, x: gridX,
y: gridY, y: gridY,
strokeColor: this.state.currentItemStrokeColor, strokeColor: this.state.currentItemStrokeColor,
@ -5620,21 +5613,8 @@ class App extends React.Component<AppProps, AppState> {
roundness: this.getCurrentItemRoundness(elementType), roundness: this.getCurrentItemRoundness(elementType),
locked: false, locked: false,
frameId: topLayerFrame ? topLayerFrame.id : null, frameId: topLayerFrame ? topLayerFrame.id : null,
} as const; ...(elementType === "embeddable" ? { validated: false } : {}),
});
let element;
if (elementType === "embeddable") {
element = newEmbeddableElement({
type: "embeddable",
validated: null,
...baseElementAttributes,
});
} else {
element = newElement({
type: elementType,
...baseElementAttributes,
});
}
if (element.type === "selection") { if (element.type === "selection") {
this.setState({ this.setState({

View File

@ -77,8 +77,8 @@ export const EyeDropper: React.FC<{
colorPreviewDiv.style.left = `${clientX + 20}px`; colorPreviewDiv.style.left = `${clientX + 20}px`;
const pixel = ctx.getImageData( const pixel = ctx.getImageData(
(clientX - appState.offsetLeft) * window.devicePixelRatio, clientX * window.devicePixelRatio - appState.offsetLeft,
(clientY - appState.offsetTop) * window.devicePixelRatio, clientY * window.devicePixelRatio - appState.offsetTop,
1, 1,
1, 1,
).data; ).data;

View File

@ -117,7 +117,6 @@ export const FRAME_STYLE = {
export const WINDOWS_EMOJI_FALLBACK_FONT = "Segoe UI Emoji"; export const WINDOWS_EMOJI_FALLBACK_FONT = "Segoe UI Emoji";
export const MIN_FONT_SIZE = 1;
export const DEFAULT_FONT_SIZE = 20; export const DEFAULT_FONT_SIZE = 20;
export const DEFAULT_FONT_FAMILY: FontFamilyValues = FONT_FAMILY.Virgil; export const DEFAULT_FONT_FAMILY: FontFamilyValues = FONT_FAMILY.Virgil;
export const DEFAULT_TEXT_ALIGN = "left"; export const DEFAULT_TEXT_ALIGN = "left";
@ -164,7 +163,6 @@ export const EXPORT_DATA_TYPES = {
excalidraw: "excalidraw", excalidraw: "excalidraw",
excalidrawClipboard: "excalidraw/clipboard", excalidrawClipboard: "excalidraw/clipboard",
excalidrawLibrary: "excalidrawlib", excalidrawLibrary: "excalidrawlib",
excalidrawClipboardWithAPI: "excalidraw-api/clipboard",
} as const; } as const;
export const EXPORT_SOURCE = export const EXPORT_SOURCE =
@ -241,8 +239,6 @@ export const VERSIONS = {
} as const; } as const;
export const BOUND_TEXT_PADDING = 5; export const BOUND_TEXT_PADDING = 5;
export const ARROW_LABEL_WIDTH_FRACTION = 0.7;
export const ARROW_LABEL_FONT_SIZE_TO_MIN_WIDTH_RATIO = 11;
export const VERTICAL_ALIGN = { export const VERTICAL_ALIGN = {
TOP: "top", TOP: "top",

File diff suppressed because it is too large Load Diff

View File

@ -29,7 +29,6 @@ import {
FONT_FAMILY, FONT_FAMILY,
ROUNDNESS, ROUNDNESS,
DEFAULT_SIDEBAR, DEFAULT_SIDEBAR,
DEFAULT_ELEMENT_PROPS,
} from "../constants"; } from "../constants";
import { getDefaultAppState } from "../appState"; import { getDefaultAppState } from "../appState";
import { LinearElementEditor } from "../element/linearElementEditor"; import { LinearElementEditor } from "../element/linearElementEditor";
@ -42,6 +41,7 @@ import {
getDefaultLineHeight, getDefaultLineHeight,
measureBaseline, measureBaseline,
} from "../element/textElement"; } from "../element/textElement";
import { COLOR_PALETTE } from "../colors";
import { normalizeLink } from "./url"; import { normalizeLink } from "./url";
type RestoredAppState = Omit< type RestoredAppState = Omit<
@ -122,18 +122,16 @@ const restoreElementWithProperties = <
versionNonce: element.versionNonce ?? 0, versionNonce: element.versionNonce ?? 0,
isDeleted: element.isDeleted ?? false, isDeleted: element.isDeleted ?? false,
id: element.id || randomId(), id: element.id || randomId(),
fillStyle: element.fillStyle || DEFAULT_ELEMENT_PROPS.fillStyle, fillStyle: element.fillStyle || "hachure",
strokeWidth: element.strokeWidth || DEFAULT_ELEMENT_PROPS.strokeWidth, strokeWidth: element.strokeWidth || 1,
strokeStyle: element.strokeStyle ?? DEFAULT_ELEMENT_PROPS.strokeStyle, strokeStyle: element.strokeStyle ?? "solid",
roughness: element.roughness ?? DEFAULT_ELEMENT_PROPS.roughness, roughness: element.roughness ?? 1,
opacity: opacity: element.opacity == null ? 100 : element.opacity,
element.opacity == null ? DEFAULT_ELEMENT_PROPS.opacity : element.opacity,
angle: element.angle || 0, angle: element.angle || 0,
x: extra.x ?? element.x ?? 0, x: extra.x ?? element.x ?? 0,
y: extra.y ?? element.y ?? 0, y: extra.y ?? element.y ?? 0,
strokeColor: element.strokeColor || DEFAULT_ELEMENT_PROPS.strokeColor, strokeColor: element.strokeColor || COLOR_PALETTE.black,
backgroundColor: backgroundColor: element.backgroundColor || COLOR_PALETTE.transparent,
element.backgroundColor || DEFAULT_ELEMENT_PROPS.backgroundColor,
width: element.width || 0, width: element.width || 0,
height: element.height || 0, height: element.height || 0,
seed: element.seed ?? 1, seed: element.seed ?? 1,
@ -248,6 +246,7 @@ const restoreElement = (
startArrowhead = null, startArrowhead = null,
endArrowhead = element.type === "arrow" ? "arrow" : null, endArrowhead = element.type === "arrow" ? "arrow" : null,
} = element; } = element;
let x = element.x; let x = element.x;
let y = element.y; let y = element.y;
let points = // migrate old arrow model to new one let points = // migrate old arrow model to new one
@ -287,7 +286,7 @@ const restoreElement = (
return restoreElementWithProperties(element, {}); return restoreElementWithProperties(element, {});
case "embeddable": case "embeddable":
return restoreElementWithProperties(element, { return restoreElementWithProperties(element, {
validated: null, validated: undefined,
}); });
case "frame": case "frame":
return restoreElementWithProperties(element, { return restoreElementWithProperties(element, {
@ -411,6 +410,7 @@ export const restoreElements = (
): ExcalidrawElement[] => { ): ExcalidrawElement[] => {
// used to detect duplicate top-level element ids // used to detect duplicate top-level element ids
const existingIds = new Set<string>(); const existingIds = new Set<string>();
const localElementsMap = localElements ? arrayToMap(localElements) : null; const localElementsMap = localElements ? arrayToMap(localElements) : null;
const restoredElements = (elements || []).reduce((elements, element) => { const restoredElements = (elements || []).reduce((elements, element) => {
// filtering out selection, which is legacy, no longer kept in elements, // filtering out selection, which is legacy, no longer kept in elements,
@ -429,7 +429,6 @@ export const restoreElements = (
migratedElement = { ...migratedElement, id: randomId() }; migratedElement = { ...migratedElement, id: randomId() };
} }
existingIds.add(migratedElement.id); existingIds.add(migratedElement.id);
elements.push(migratedElement); elements.push(migratedElement);
} }
} }

View File

@ -1,706 +0,0 @@
import { vi } from "vitest";
import {
ExcalidrawElementSkeleton,
convertToExcalidrawElements,
} from "./transform";
import { ExcalidrawArrowElement } from "../element/types";
describe("Test Transform", () => {
it("should transform regular shapes", () => {
const elements = [
{
type: "rectangle",
x: 100,
y: 100,
},
{
type: "ellipse",
x: 100,
y: 250,
},
{
type: "diamond",
x: 100,
y: 400,
},
{
type: "rectangle",
x: 300,
y: 100,
width: 200,
height: 100,
backgroundColor: "#c0eb75",
strokeWidth: 2,
},
{
type: "ellipse",
x: 300,
y: 250,
width: 200,
height: 100,
backgroundColor: "#ffc9c9",
strokeStyle: "dotted",
fillStyle: "solid",
strokeWidth: 2,
},
{
type: "diamond",
x: 300,
y: 400,
width: 200,
height: 100,
backgroundColor: "#a5d8ff",
strokeColor: "#1971c2",
strokeStyle: "dashed",
fillStyle: "cross-hatch",
strokeWidth: 2,
},
];
convertToExcalidrawElements(
elements as ExcalidrawElementSkeleton[],
).forEach((ele) => {
expect(ele).toMatchSnapshot({
seed: expect.any(Number),
versionNonce: expect.any(Number),
id: expect.any(String),
});
});
});
it("should transform text element", () => {
const elements = [
{
type: "text",
x: 100,
y: 100,
text: "HELLO WORLD!",
},
{
type: "text",
x: 100,
y: 150,
text: "STYLED HELLO WORLD!",
fontSize: 20,
strokeColor: "#5f3dc4",
},
];
convertToExcalidrawElements(
elements as ExcalidrawElementSkeleton[],
).forEach((ele) => {
expect(ele).toMatchSnapshot({
seed: expect.any(Number),
versionNonce: expect.any(Number),
id: expect.any(String),
});
});
});
it("should transform linear elements", () => {
const elements = [
{
type: "arrow",
x: 100,
y: 20,
},
{
type: "arrow",
x: 450,
y: 20,
startArrowhead: "dot",
endArrowhead: "triangle",
strokeColor: "#1971c2",
strokeWidth: 2,
},
{
type: "line",
x: 100,
y: 60,
},
{
type: "line",
x: 450,
y: 60,
strokeColor: "#2f9e44",
strokeWidth: 2,
strokeStyle: "dotted",
},
];
const excaldrawElements = convertToExcalidrawElements(
elements as ExcalidrawElementSkeleton[],
);
expect(excaldrawElements.length).toBe(4);
excaldrawElements.forEach((ele) => {
expect(ele).toMatchSnapshot({
seed: expect.any(Number),
versionNonce: expect.any(Number),
id: expect.any(String),
});
});
});
it("should transform to text containers when label provided", () => {
const elements = [
{
type: "rectangle",
x: 100,
y: 100,
label: {
text: "RECTANGLE TEXT CONTAINER",
},
},
{
type: "ellipse",
x: 500,
y: 100,
width: 200,
label: {
text: "ELLIPSE TEXT CONTAINER",
},
},
{
type: "diamond",
x: 100,
y: 150,
width: 280,
label: {
text: "DIAMOND\nTEXT CONTAINER",
},
},
{
type: "diamond",
x: 100,
y: 400,
width: 300,
backgroundColor: "#fff3bf",
strokeWidth: 2,
label: {
text: "STYLED DIAMOND TEXT CONTAINER",
strokeColor: "#099268",
fontSize: 20,
},
},
{
type: "rectangle",
x: 500,
y: 300,
width: 200,
strokeColor: "#c2255c",
label: {
text: "TOP LEFT ALIGNED RECTANGLE TEXT CONTAINER",
textAlign: "left",
verticalAlign: "top",
fontSize: 20,
},
},
{
type: "ellipse",
x: 500,
y: 500,
strokeColor: "#f08c00",
backgroundColor: "#ffec99",
width: 200,
label: {
text: "STYLED ELLIPSE TEXT CONTAINER",
strokeColor: "#c2255c",
},
},
];
const excaldrawElements = convertToExcalidrawElements(
elements as ExcalidrawElementSkeleton[],
);
expect(excaldrawElements.length).toBe(12);
excaldrawElements.forEach((ele) => {
expect(ele).toMatchSnapshot({
seed: expect.any(Number),
versionNonce: expect.any(Number),
id: expect.any(String),
});
});
});
it("should transform to labelled arrows when label provided for arrows", () => {
const elements = [
{
type: "arrow",
x: 100,
y: 100,
label: {
text: "LABELED ARROW",
},
},
{
type: "arrow",
x: 100,
y: 200,
label: {
text: "STYLED LABELED ARROW",
strokeColor: "#099268",
fontSize: 20,
},
},
{
type: "arrow",
x: 100,
y: 300,
strokeColor: "#1098ad",
strokeWidth: 2,
label: {
text: "ANOTHER STYLED LABELLED ARROW",
},
},
{
type: "arrow",
x: 100,
y: 400,
strokeColor: "#1098ad",
strokeWidth: 2,
label: {
text: "ANOTHER STYLED LABELLED ARROW",
strokeColor: "#099268",
},
},
];
const excaldrawElements = convertToExcalidrawElements(
elements as ExcalidrawElementSkeleton[],
);
expect(excaldrawElements.length).toBe(8);
excaldrawElements.forEach((ele) => {
expect(ele).toMatchSnapshot({
seed: expect.any(Number),
versionNonce: expect.any(Number),
id: expect.any(String),
});
});
});
describe("Test arrow bindings", () => {
it("should bind arrows to shapes when start / end provided without ids", () => {
const elements = [
{
type: "arrow",
x: 255,
y: 239,
label: {
text: "HELLO WORLD!!",
},
start: {
type: "rectangle",
},
end: {
type: "ellipse",
},
},
];
const excaldrawElements = convertToExcalidrawElements(
elements as ExcalidrawElementSkeleton[],
);
expect(excaldrawElements.length).toBe(4);
const [arrow, text, rectangle, ellipse] = excaldrawElements;
expect(arrow).toMatchObject({
type: "arrow",
x: 255,
y: 239,
boundElements: [{ id: text.id, type: "text" }],
startBinding: {
elementId: rectangle.id,
focus: 0,
gap: 1,
},
endBinding: {
elementId: ellipse.id,
focus: 0,
},
});
expect(text).toMatchObject({
x: 340,
y: 226.5,
type: "text",
text: "HELLO WORLD!!",
containerId: arrow.id,
});
expect(rectangle).toMatchObject({
x: 155,
y: 189,
type: "rectangle",
boundElements: [
{
id: arrow.id,
type: "arrow",
},
],
});
expect(ellipse).toMatchObject({
x: 555,
y: 189,
type: "ellipse",
boundElements: [
{
id: arrow.id,
type: "arrow",
},
],
});
excaldrawElements.forEach((ele) => {
expect(ele).toMatchSnapshot({
seed: expect.any(Number),
versionNonce: expect.any(Number),
id: expect.any(String),
});
});
});
it("should bind arrows to text when start / end provided without ids", () => {
const elements = [
{
type: "arrow",
x: 255,
y: 239,
label: {
text: "HELLO WORLD!!",
},
start: {
type: "text",
text: "HEYYYYY",
},
end: {
type: "text",
text: "WHATS UP ?",
},
},
];
const excaldrawElements = convertToExcalidrawElements(
elements as ExcalidrawElementSkeleton[],
);
expect(excaldrawElements.length).toBe(4);
const [arrow, text1, text2, text3] = excaldrawElements;
expect(arrow).toMatchObject({
type: "arrow",
x: 255,
y: 239,
boundElements: [{ id: text1.id, type: "text" }],
startBinding: {
elementId: text2.id,
focus: 0,
gap: 1,
},
endBinding: {
elementId: text3.id,
focus: 0,
},
});
expect(text1).toMatchObject({
x: 340,
y: 226.5,
type: "text",
text: "HELLO WORLD!!",
containerId: arrow.id,
});
expect(text2).toMatchObject({
x: 185,
y: 226.5,
type: "text",
boundElements: [
{
id: arrow.id,
type: "arrow",
},
],
});
expect(text3).toMatchObject({
x: 555,
y: 226.5,
type: "text",
boundElements: [
{
id: arrow.id,
type: "arrow",
},
],
});
excaldrawElements.forEach((ele) => {
expect(ele).toMatchSnapshot({
seed: expect.any(Number),
versionNonce: expect.any(Number),
id: expect.any(String),
});
});
});
it("should bind arrows to existing shapes when start / end provided with ids", () => {
const elements = [
{
type: "ellipse",
id: "ellipse-1",
strokeColor: "#66a80f",
x: 630,
y: 316,
width: 300,
height: 300,
backgroundColor: "#d8f5a2",
},
{
type: "diamond",
id: "diamond-1",
strokeColor: "#9c36b5",
width: 140,
x: 96,
y: 400,
},
{
type: "arrow",
x: 247,
y: 420,
width: 395,
height: 35,
strokeColor: "#1864ab",
start: {
type: "rectangle",
width: 300,
height: 300,
},
end: {
id: "ellipse-1",
},
},
{
type: "arrow",
x: 227,
y: 450,
width: 400,
strokeColor: "#e67700",
start: {
id: "diamond-1",
},
end: {
id: "ellipse-1",
},
},
];
const excaldrawElements = convertToExcalidrawElements(
elements as ExcalidrawElementSkeleton[],
);
expect(excaldrawElements.length).toBe(5);
excaldrawElements.forEach((ele) => {
expect(ele).toMatchSnapshot({
seed: expect.any(Number),
versionNonce: expect.any(Number),
id: expect.any(String),
});
});
});
it("should bind arrows to existing text elements when start / end provided with ids", () => {
const elements = [
{
x: 100,
y: 239,
type: "text",
text: "HEYYYYY",
id: "text-1",
strokeColor: "#c2255c",
},
{
type: "text",
id: "text-2",
x: 560,
y: 239,
text: "Whats up ?",
},
{
type: "arrow",
x: 255,
y: 239,
label: {
text: "HELLO WORLD!!",
},
start: {
id: "text-1",
},
end: {
id: "text-2",
},
},
];
const excaldrawElements = convertToExcalidrawElements(
elements as ExcalidrawElementSkeleton[],
);
expect(excaldrawElements.length).toBe(4);
excaldrawElements.forEach((ele) => {
expect(ele).toMatchSnapshot({
seed: expect.any(Number),
versionNonce: expect.any(Number),
id: expect.any(String),
});
});
});
it("should bind arrows to existing elements if ids are correct", () => {
const consoleErrorSpy = vi
.spyOn(console, "error")
.mockImplementationOnce(() => void 0);
const elements = [
{
x: 100,
y: 239,
type: "text",
text: "HEYYYYY",
id: "text-1",
strokeColor: "#c2255c",
},
{
type: "rectangle",
x: 560,
y: 139,
id: "rect-1",
width: 100,
height: 200,
backgroundColor: "#bac8ff",
},
{
type: "arrow",
x: 255,
y: 239,
label: {
text: "HELLO WORLD!!",
},
start: {
id: "text-13",
},
end: {
id: "rect-11",
},
},
];
const excaldrawElements = convertToExcalidrawElements(
elements as ExcalidrawElementSkeleton[],
);
expect(excaldrawElements.length).toBe(4);
const [, , arrow] = excaldrawElements;
expect(arrow).toMatchObject({
type: "arrow",
x: 255,
y: 239,
boundElements: [
{
id: "id46",
type: "text",
},
],
startBinding: null,
endBinding: null,
});
expect(consoleErrorSpy).toHaveBeenCalledTimes(2);
expect(consoleErrorSpy).toHaveBeenNthCalledWith(
1,
"No element for start binding with id text-13 found",
);
expect(consoleErrorSpy).toHaveBeenNthCalledWith(
2,
"No element for end binding with id rect-11 found",
);
});
it("should bind when ids referenced before the element data", () => {
const elements = [
{
type: "arrow",
x: 255,
y: 239,
end: {
id: "rect-1",
},
},
{
type: "rectangle",
x: 560,
y: 139,
id: "rect-1",
width: 100,
height: 200,
backgroundColor: "#bac8ff",
},
];
const excaldrawElements = convertToExcalidrawElements(
elements as ExcalidrawElementSkeleton[],
);
expect(excaldrawElements.length).toBe(2);
const [arrow, rect] = excaldrawElements;
expect((arrow as ExcalidrawArrowElement).endBinding).toStrictEqual({
elementId: "rect-1",
focus: 0,
gap: 5,
});
expect(rect.boundElements).toStrictEqual([
{
id: "id47",
type: "arrow",
},
]);
});
});
it("should not allow duplicate ids", () => {
const consoleErrorSpy = vi
.spyOn(console, "error")
.mockImplementationOnce(() => void 0);
const elements = [
{
type: "rectangle",
x: 300,
y: 100,
id: "rect-1",
width: 100,
height: 200,
},
{
type: "rectangle",
x: 100,
y: 200,
id: "rect-1",
width: 100,
height: 200,
},
];
const excaldrawElements = convertToExcalidrawElements(
elements as ExcalidrawElementSkeleton[],
);
expect(excaldrawElements.length).toBe(1);
expect(excaldrawElements[0]).toMatchSnapshot({
seed: expect.any(Number),
versionNonce: expect.any(Number),
});
expect(consoleErrorSpy).toHaveBeenCalledWith(
"Duplicate id found for rect-1",
);
});
});

View File

@ -1,561 +0,0 @@
import {
DEFAULT_FONT_FAMILY,
DEFAULT_FONT_SIZE,
TEXT_ALIGN,
VERTICAL_ALIGN,
} from "../constants";
import {
newElement,
newLinearElement,
redrawTextBoundingBox,
} from "../element";
import { bindLinearElement } from "../element/binding";
import {
ElementConstructorOpts,
newImageElement,
newTextElement,
} from "../element/newElement";
import {
getDefaultLineHeight,
measureText,
normalizeText,
} from "../element/textElement";
import {
ExcalidrawArrowElement,
ExcalidrawBindableElement,
ExcalidrawElement,
ExcalidrawEmbeddableElement,
ExcalidrawFrameElement,
ExcalidrawFreeDrawElement,
ExcalidrawGenericElement,
ExcalidrawImageElement,
ExcalidrawLinearElement,
ExcalidrawSelectionElement,
ExcalidrawTextElement,
FileId,
FontFamilyValues,
TextAlign,
VerticalAlign,
} from "../element/types";
import { MarkOptional } from "../utility-types";
import { assertNever, getFontString } from "../utils";
export type ValidLinearElement = {
type: "arrow" | "line";
x: number;
y: number;
label?: {
text: string;
fontSize?: number;
fontFamily?: FontFamilyValues;
textAlign?: TextAlign;
verticalAlign?: VerticalAlign;
} & MarkOptional<ElementConstructorOpts, "x" | "y">;
end?:
| (
| (
| {
type: Exclude<
ExcalidrawBindableElement["type"],
"image" | "text" | "frame" | "embeddable"
>;
id?: ExcalidrawGenericElement["id"];
}
| {
id: ExcalidrawGenericElement["id"];
type?: Exclude<
ExcalidrawBindableElement["type"],
"image" | "text" | "frame" | "embeddable"
>;
}
)
| ((
| {
type: "text";
text: string;
}
| {
type?: "text";
id: ExcalidrawTextElement["id"];
text: string;
}
) &
Partial<ExcalidrawTextElement>)
) &
MarkOptional<ElementConstructorOpts, "x" | "y">;
start?:
| (
| (
| {
type: Exclude<
ExcalidrawBindableElement["type"],
"image" | "text" | "frame" | "embeddable"
>;
id?: ExcalidrawGenericElement["id"];
}
| {
id: ExcalidrawGenericElement["id"];
type?: Exclude<
ExcalidrawBindableElement["type"],
"image" | "text" | "frame" | "embeddable"
>;
}
)
| ((
| {
type: "text";
text: string;
}
| {
type?: "text";
id: ExcalidrawTextElement["id"];
text: string;
}
) &
Partial<ExcalidrawTextElement>)
) &
MarkOptional<ElementConstructorOpts, "x" | "y">;
} & Partial<ExcalidrawLinearElement>;
export type ValidContainer =
| {
type: Exclude<ExcalidrawGenericElement["type"], "selection">;
id?: ExcalidrawGenericElement["id"];
label?: {
text: string;
fontSize?: number;
fontFamily?: FontFamilyValues;
textAlign?: TextAlign;
verticalAlign?: VerticalAlign;
} & MarkOptional<ElementConstructorOpts, "x" | "y">;
} & ElementConstructorOpts;
export type ExcalidrawElementSkeleton =
| Extract<
Exclude<ExcalidrawElement, ExcalidrawSelectionElement>,
| ExcalidrawEmbeddableElement
| ExcalidrawFreeDrawElement
| ExcalidrawFrameElement
>
| ({
type: Extract<ExcalidrawLinearElement["type"], "line">;
x: number;
y: number;
} & Partial<ExcalidrawLinearElement>)
| ValidContainer
| ValidLinearElement
| ({
type: "text";
text: string;
x: number;
y: number;
id?: ExcalidrawTextElement["id"];
} & Partial<ExcalidrawTextElement>)
| ({
type: Extract<ExcalidrawImageElement["type"], "image">;
x: number;
y: number;
fileId: FileId;
} & Partial<ExcalidrawImageElement>);
const DEFAULT_LINEAR_ELEMENT_PROPS = {
width: 300,
height: 0,
};
const DEFAULT_DIMENSION = 100;
const bindTextToContainer = (
container: ExcalidrawElement,
textProps: { text: string } & MarkOptional<ElementConstructorOpts, "x" | "y">,
) => {
const textElement: ExcalidrawTextElement = newTextElement({
x: 0,
y: 0,
textAlign: TEXT_ALIGN.CENTER,
verticalAlign: VERTICAL_ALIGN.MIDDLE,
...textProps,
containerId: container.id,
strokeColor: textProps.strokeColor || container.strokeColor,
});
Object.assign(container, {
boundElements: (container.boundElements || []).concat({
type: "text",
id: textElement.id,
}),
});
redrawTextBoundingBox(textElement, container);
return [container, textElement] as const;
};
const bindLinearElementToElement = (
linearElement: ExcalidrawArrowElement,
start: ValidLinearElement["start"],
end: ValidLinearElement["end"],
elementStore: ElementStore,
): {
linearElement: ExcalidrawLinearElement;
startBoundElement?: ExcalidrawElement;
endBoundElement?: ExcalidrawElement;
} => {
let startBoundElement;
let endBoundElement;
Object.assign(linearElement, {
startBinding: linearElement?.startBinding || null,
endBinding: linearElement.endBinding || null,
});
if (start) {
const width = start?.width ?? DEFAULT_DIMENSION;
const height = start?.height ?? DEFAULT_DIMENSION;
let existingElement;
if (start.id) {
existingElement = elementStore.getElement(start.id);
if (!existingElement) {
console.error(`No element for start binding with id ${start.id} found`);
}
}
const startX = start.x || linearElement.x - width;
const startY = start.y || linearElement.y - height / 2;
const startType = existingElement ? existingElement.type : start.type;
if (startType) {
if (startType === "text") {
let text = "";
if (existingElement && existingElement.type === "text") {
text = existingElement.text;
} else if (start.type === "text") {
text = start.text;
}
if (!text) {
console.error(
`No text found for start binding text element for ${linearElement.id}`,
);
}
startBoundElement = newTextElement({
x: startX,
y: startY,
type: "text",
...existingElement,
...start,
text,
});
// to position the text correctly when coordinates not provided
Object.assign(startBoundElement, {
x: start.x || linearElement.x - startBoundElement.width,
y: start.y || linearElement.y - startBoundElement.height / 2,
});
} else {
switch (startType) {
case "rectangle":
case "ellipse":
case "diamond": {
startBoundElement = newElement({
x: startX,
y: startY,
width,
height,
...existingElement,
...start,
type: startType,
});
break;
}
default: {
assertNever(
linearElement as never,
`Unhandled element start type "${start.type}"`,
true,
);
}
}
}
bindLinearElement(
linearElement,
startBoundElement as ExcalidrawBindableElement,
"start",
);
}
}
if (end) {
const height = end?.height ?? DEFAULT_DIMENSION;
const width = end?.width ?? DEFAULT_DIMENSION;
let existingElement;
if (end.id) {
existingElement = elementStore.getElement(end.id);
if (!existingElement) {
console.error(`No element for end binding with id ${end.id} found`);
}
}
const endX = end.x || linearElement.x + linearElement.width;
const endY = end.y || linearElement.y - height / 2;
const endType = existingElement ? existingElement.type : end.type;
if (endType) {
if (endType === "text") {
let text = "";
if (existingElement && existingElement.type === "text") {
text = existingElement.text;
} else if (end.type === "text") {
text = end.text;
}
if (!text) {
console.error(
`No text found for end binding text element for ${linearElement.id}`,
);
}
endBoundElement = newTextElement({
x: endX,
y: endY,
type: "text",
...existingElement,
...end,
text,
});
// to position the text correctly when coordinates not provided
Object.assign(endBoundElement, {
y: end.y || linearElement.y - endBoundElement.height / 2,
});
} else {
switch (endType) {
case "rectangle":
case "ellipse":
case "diamond": {
endBoundElement = newElement({
x: endX,
y: endY,
width,
height,
...existingElement,
...end,
type: endType,
});
break;
}
default: {
assertNever(
linearElement as never,
`Unhandled element end type "${endType}"`,
true,
);
}
}
}
bindLinearElement(
linearElement,
endBoundElement as ExcalidrawBindableElement,
"end",
);
}
}
return {
linearElement,
startBoundElement,
endBoundElement,
};
};
class ElementStore {
excalidrawElements = new Map<string, ExcalidrawElement>();
add = (ele?: ExcalidrawElement) => {
if (!ele) {
return;
}
this.excalidrawElements.set(ele.id, ele);
};
getElements = () => {
return Array.from(this.excalidrawElements.values());
};
getElement = (id: string) => {
return this.excalidrawElements.get(id);
};
}
export const convertToExcalidrawElements = (
elements: ExcalidrawElementSkeleton[] | null,
) => {
if (!elements) {
return [];
}
const elementStore = new ElementStore();
const elementsWithIds = new Map<string, ExcalidrawElementSkeleton>();
// Create individual elements
for (const element of elements) {
let excalidrawElement: ExcalidrawElement;
switch (element.type) {
case "rectangle":
case "ellipse":
case "diamond": {
const width =
element?.label?.text && element.width === undefined
? 0
: element?.width || DEFAULT_DIMENSION;
const height =
element?.label?.text && element.height === undefined
? 0
: element?.height || DEFAULT_DIMENSION;
excalidrawElement = newElement({
...element,
width,
height,
});
break;
}
case "line": {
const width = element.width || DEFAULT_LINEAR_ELEMENT_PROPS.width;
const height = element.height || DEFAULT_LINEAR_ELEMENT_PROPS.height;
excalidrawElement = newLinearElement({
width,
height,
points: [
[0, 0],
[width, height],
],
...element,
});
break;
}
case "arrow": {
const width = element.width || DEFAULT_LINEAR_ELEMENT_PROPS.width;
const height = element.height || DEFAULT_LINEAR_ELEMENT_PROPS.height;
excalidrawElement = newLinearElement({
width,
height,
endArrowhead: "arrow",
points: [
[0, 0],
[width, height],
],
...element,
});
break;
}
case "text": {
const fontFamily = element?.fontFamily || DEFAULT_FONT_FAMILY;
const fontSize = element?.fontSize || DEFAULT_FONT_SIZE;
const lineHeight =
element?.lineHeight || getDefaultLineHeight(fontFamily);
const text = element.text ?? "";
const normalizedText = normalizeText(text);
const metrics = measureText(
normalizedText,
getFontString({ fontFamily, fontSize }),
lineHeight,
);
excalidrawElement = newTextElement({
width: metrics.width,
height: metrics.height,
fontFamily,
fontSize,
...element,
});
break;
}
case "image": {
excalidrawElement = newImageElement({
width: element?.width || DEFAULT_DIMENSION,
height: element?.height || DEFAULT_DIMENSION,
...element,
});
break;
}
case "freedraw":
case "frame":
case "embeddable": {
excalidrawElement = element;
break;
}
default: {
excalidrawElement = element;
assertNever(
element,
`Unhandled element type "${(element as any).type}"`,
true,
);
}
}
const existingElement = elementStore.getElement(excalidrawElement.id);
if (existingElement) {
console.error(`Duplicate id found for ${excalidrawElement.id}`);
} else {
elementStore.add(excalidrawElement);
elementsWithIds.set(excalidrawElement.id, element);
}
}
// Add labels and arrow bindings
for (const [id, element] of elementsWithIds) {
const excalidrawElement = elementStore.getElement(id)!;
switch (element.type) {
case "rectangle":
case "ellipse":
case "diamond":
case "arrow": {
if (element.label?.text) {
let [container, text] = bindTextToContainer(
excalidrawElement,
element?.label,
);
elementStore.add(container);
elementStore.add(text);
if (container.type === "arrow") {
const originalStart =
element.type === "arrow" ? element?.start : undefined;
const originalEnd =
element.type === "arrow" ? element?.end : undefined;
const { linearElement, startBoundElement, endBoundElement } =
bindLinearElementToElement(
container as ExcalidrawArrowElement,
originalStart,
originalEnd,
elementStore,
);
container = linearElement;
elementStore.add(linearElement);
elementStore.add(startBoundElement);
elementStore.add(endBoundElement);
}
} else {
switch (element.type) {
case "arrow": {
const { linearElement, startBoundElement, endBoundElement } =
bindLinearElementToElement(
excalidrawElement as ExcalidrawArrowElement,
element.start,
element.end,
elementStore,
);
elementStore.add(linearElement);
elementStore.add(startBoundElement);
elementStore.add(endBoundElement);
break;
}
}
}
break;
}
}
}
return elementStore.getElements();
};

View File

@ -190,7 +190,7 @@ export const maybeBindLinearElement = (
} }
}; };
export const bindLinearElement = ( const bindLinearElement = (
linearElement: NonDeleted<ExcalidrawLinearElement>, linearElement: NonDeleted<ExcalidrawLinearElement>,
hoveredElement: ExcalidrawBindableElement, hoveredElement: ExcalidrawBindableElement,
startOrEnd: "start" | "end", startOrEnd: "start" | "end",

View File

@ -40,9 +40,6 @@ const RE_TWITTER = /(?:http(?:s)?:\/\/)?(?:(?:w){3}.)?twitter.com/;
const RE_TWITTER_EMBED = const RE_TWITTER_EMBED =
/^<blockquote[\s\S]*?\shref=["'](https:\/\/twitter.com\/[^"']*)/i; /^<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 = const RE_GENERIC_EMBED =
/^<(?:iframe|blockquote)[\s\S]*?\s(?:src|href)=["']([^"']*)["'][\s\S]*?>$/i; /^<(?:iframe|blockquote)[\s\S]*?\s(?:src|href)=["']([^"']*)["'][\s\S]*?>$/i;
@ -55,9 +52,7 @@ const ALLOWED_DOMAINS = new Set([
"link.excalidraw.com", "link.excalidraw.com",
"gist.github.com", "gist.github.com",
"twitter.com", "twitter.com",
"*.simplepdf.eu",
"stackblitz.com", "stackblitz.com",
"val.town",
]); ]);
const createSrcDoc = (body: string) => { const createSrcDoc = (body: string) => {
@ -127,14 +122,6 @@ export const getEmbedLink = (link: string | null | undefined): EmbeddedLink => {
return { 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)) { if (RE_TWITTER.test(link)) {
let ret: EmbeddedLink; let ret: EmbeddedLink;
// assume embed code // assume embed code
@ -275,16 +262,9 @@ const validateHostname = (
const { hostname } = new URL(url); const { hostname } = new URL(url);
const bareDomain = hostname.replace(/^www\./, ""); const bareDomain = hostname.replace(/^www\./, "");
const bareDomainWithFirstSubdomainWildcarded = bareDomain.replace(
/^([^.]+)/,
"*",
);
if (allowedHostnames instanceof Set) { if (allowedHostnames instanceof Set) {
return ( return ALLOWED_DOMAINS.has(bareDomain);
ALLOWED_DOMAINS.has(bareDomain) ||
ALLOWED_DOMAINS.has(bareDomainWithFirstSubdomainWildcarded)
);
} }
if (bareDomain === allowedHostnames.replace(/^www\./, "")) { if (bareDomain === allowedHostnames.replace(/^www\./, "")) {

View File

@ -264,11 +264,11 @@ export class LinearElementEditor {
}; };
}), }),
); );
}
const boundTextElement = getBoundTextElement(element); const boundTextElement = getBoundTextElement(element);
if (boundTextElement) { if (boundTextElement) {
handleBindTextResize(element, false); handleBindTextResize(element, false);
}
} }
// suggest bindings for first and last point if selected // suggest bindings for first and last point if selected

View File

@ -46,7 +46,7 @@ import {
} from "../constants"; } from "../constants";
import { MarkOptional, Merge, Mutable } from "../utility-types"; import { MarkOptional, Merge, Mutable } from "../utility-types";
export type ElementConstructorOpts = MarkOptional< type ElementConstructorOpts = MarkOptional<
Omit<ExcalidrawGenericElement, "id" | "type" | "isDeleted" | "updated">, Omit<ExcalidrawGenericElement, "id" | "type" | "isDeleted" | "updated">,
| "width" | "width"
| "height" | "height"
@ -134,7 +134,7 @@ export const newElement = (
export const newEmbeddableElement = ( export const newEmbeddableElement = (
opts: { opts: {
type: "embeddable"; type: "embeddable";
validated: ExcalidrawEmbeddableElement["validated"]; validated: boolean | undefined;
} & ElementConstructorOpts, } & ElementConstructorOpts,
): NonDeleted<ExcalidrawEmbeddableElement> => { ): NonDeleted<ExcalidrawEmbeddableElement> => {
return { return {
@ -187,7 +187,7 @@ export const newTextElement = (
fontFamily?: FontFamilyValues; fontFamily?: FontFamilyValues;
textAlign?: TextAlign; textAlign?: TextAlign;
verticalAlign?: VerticalAlign; verticalAlign?: VerticalAlign;
containerId?: ExcalidrawTextContainer["id"] | null; containerId?: ExcalidrawTextContainer["id"];
lineHeight?: ExcalidrawTextElement["lineHeight"]; lineHeight?: ExcalidrawTextElement["lineHeight"];
strokeWidth?: ExcalidrawTextElement["strokeWidth"]; strokeWidth?: ExcalidrawTextElement["strokeWidth"];
} & ElementConstructorOpts, } & ElementConstructorOpts,
@ -361,8 +361,8 @@ export const newFreeDrawElement = (
export const newLinearElement = ( export const newLinearElement = (
opts: { opts: {
type: ExcalidrawLinearElement["type"]; type: ExcalidrawLinearElement["type"];
startArrowhead?: Arrowhead | null; startArrowhead: Arrowhead | null;
endArrowhead?: Arrowhead | null; endArrowhead: Arrowhead | null;
points?: ExcalidrawLinearElement["points"]; points?: ExcalidrawLinearElement["points"];
} & ElementConstructorOpts, } & ElementConstructorOpts,
): NonDeleted<ExcalidrawLinearElement> => { ): NonDeleted<ExcalidrawLinearElement> => {
@ -372,8 +372,8 @@ export const newLinearElement = (
lastCommittedPoint: null, lastCommittedPoint: null,
startBinding: null, startBinding: null,
endBinding: null, endBinding: null,
startArrowhead: opts.startArrowhead || null, startArrowhead: opts.startArrowhead,
endArrowhead: opts.endArrowhead || null, endArrowhead: opts.endArrowhead,
}; };
}; };
@ -477,7 +477,7 @@ export const deepCopyElement = <T extends ExcalidrawElement>(
* utility wrapper to generate new id. In test env it reuses the old + postfix * utility wrapper to generate new id. In test env it reuses the old + postfix
* for test assertions. * for test assertions.
*/ */
export const regenerateId = ( const regenerateId = (
/** supply null if no previous id exists */ /** supply null if no previous id exists */
previousId: string | null, previousId: string | null,
) => { ) => {

View File

@ -1,4 +1,4 @@
import { MIN_FONT_SIZE, SHIFT_LOCKING_ANGLE } from "../constants"; import { SHIFT_LOCKING_ANGLE } from "../constants";
import { rescalePoints } from "../points"; import { rescalePoints } from "../points";
import { import {
@ -204,6 +204,8 @@ const rescalePointsInElement = (
} }
: {}; : {};
const MIN_FONT_SIZE = 1;
const measureFontSizeFromWidth = ( const measureFontSizeFromWidth = (
element: NonDeleted<ExcalidrawTextElement>, element: NonDeleted<ExcalidrawTextElement>,
nextWidth: number, nextWidth: number,
@ -587,42 +589,24 @@ export const resizeSingleElement = (
}); });
} }
if (
isArrowElement(element) &&
boundTextElement &&
shouldMaintainAspectRatio
) {
const fontSize =
(resizedElement.width / element.width) * boundTextElement.fontSize;
if (fontSize < MIN_FONT_SIZE) {
return;
}
boundTextFont.fontSize = fontSize;
}
if ( if (
resizedElement.width !== 0 && resizedElement.width !== 0 &&
resizedElement.height !== 0 && resizedElement.height !== 0 &&
Number.isFinite(resizedElement.x) && Number.isFinite(resizedElement.x) &&
Number.isFinite(resizedElement.y) Number.isFinite(resizedElement.y)
) { ) {
mutateElement(element, resizedElement);
updateBoundElements(element, { updateBoundElements(element, {
newSize: { width: resizedElement.width, height: resizedElement.height }, newSize: { width: resizedElement.width, height: resizedElement.height },
}); });
mutateElement(element, resizedElement);
if (boundTextElement && boundTextFont != null) { if (boundTextElement && boundTextFont != null) {
mutateElement(boundTextElement, { mutateElement(boundTextElement, {
fontSize: boundTextFont.fontSize, fontSize: boundTextFont.fontSize,
baseline: boundTextFont.baseline, baseline: boundTextFont.baseline,
}); });
} }
handleBindTextResize( handleBindTextResize(element, transformHandleDirection);
element,
transformHandleDirection,
shouldMaintainAspectRatio,
);
} }
}; };
@ -738,8 +722,12 @@ export const resizeMultipleElements = (
fontSize?: ExcalidrawTextElement["fontSize"]; fontSize?: ExcalidrawTextElement["fontSize"];
baseline?: ExcalidrawTextElement["baseline"]; baseline?: ExcalidrawTextElement["baseline"];
scale?: ExcalidrawImageElement["scale"]; scale?: ExcalidrawImageElement["scale"];
boundTextFontSize?: ExcalidrawTextElement["fontSize"];
}; };
boundText: {
element: ExcalidrawTextElementWithContainer;
fontSize: ExcalidrawTextElement["fontSize"];
baseline: ExcalidrawTextElement["baseline"];
} | null;
}[] = []; }[] = [];
for (const { orig, latest } of targetElements) { for (const { orig, latest } of targetElements) {
@ -810,39 +798,50 @@ export const resizeMultipleElements = (
} }
} }
if (isTextElement(orig)) { let boundText: typeof elementsAndUpdates[0]["boundText"] = null;
const metrics = measureFontSizeFromWidth(orig, width, height);
const boundTextElement = getBoundTextElement(latest);
if (boundTextElement || isTextElement(orig)) {
const updatedElement = {
...latest,
width,
height,
};
const metrics = measureFontSizeFromWidth(
boundTextElement ?? (orig as ExcalidrawTextElement),
boundTextElement
? getBoundTextMaxWidth(updatedElement)
: updatedElement.width,
boundTextElement
? getBoundTextMaxHeight(updatedElement, boundTextElement)
: updatedElement.height,
);
if (!metrics) { if (!metrics) {
return; return;
} }
update.fontSize = metrics.size;
update.baseline = metrics.baseline;
}
const boundTextElement = pointerDownState.originalElements.get( if (isTextElement(orig)) {
getBoundTextElementId(orig) ?? "", update.fontSize = metrics.size;
) as ExcalidrawTextElementWithContainer | undefined; update.baseline = metrics.baseline;
}
if (boundTextElement) {
const newFontSize = boundTextElement.fontSize * scale; if (boundTextElement) {
if (newFontSize < MIN_FONT_SIZE) { boundText = {
return; element: boundTextElement,
fontSize: metrics.size,
baseline: metrics.baseline,
};
} }
update.boundTextFontSize = newFontSize;
} }
elementsAndUpdates.push({ elementsAndUpdates.push({ element: latest, update, boundText });
element: latest,
update,
});
} }
const elementsToUpdate = elementsAndUpdates.map(({ element }) => element); const elementsToUpdate = elementsAndUpdates.map(({ element }) => element);
for (const { for (const { element, update, boundText } of elementsAndUpdates) {
element,
update: { boundTextFontSize, ...update },
} of elementsAndUpdates) {
const { width, height, angle } = update; const { width, height, angle } = update;
mutateElement(element, update, false); mutateElement(element, update, false);
@ -852,17 +851,17 @@ export const resizeMultipleElements = (
newSize: { width, height }, newSize: { width, height },
}); });
const boundTextElement = getBoundTextElement(element); if (boundText) {
if (boundTextElement && boundTextFontSize) { const { element: boundTextElement, ...boundTextUpdates } = boundText;
mutateElement( mutateElement(
boundTextElement, boundTextElement,
{ {
fontSize: boundTextFontSize, ...boundTextUpdates,
angle: isLinearElement(element) ? undefined : angle, angle: isLinearElement(element) ? undefined : angle,
}, },
false, false,
); );
handleBindTextResize(element, transformHandleType, true); handleBindTextResize(element, transformHandleType);
} }
} }

View File

@ -10,8 +10,6 @@ import {
} from "./types"; } from "./types";
import { mutateElement } from "./mutateElement"; import { mutateElement } from "./mutateElement";
import { import {
ARROW_LABEL_FONT_SIZE_TO_MIN_WIDTH_RATIO,
ARROW_LABEL_WIDTH_FRACTION,
BOUND_TEXT_PADDING, BOUND_TEXT_PADDING,
DEFAULT_FONT_FAMILY, DEFAULT_FONT_FAMILY,
DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE,
@ -67,7 +65,7 @@ export const redrawTextBoundingBox = (
boundTextUpdates.text = textElement.text; boundTextUpdates.text = textElement.text;
if (container) { if (container) {
maxWidth = getBoundTextMaxWidth(container, textElement); maxWidth = getBoundTextMaxWidth(container);
boundTextUpdates.text = wrapText( boundTextUpdates.text = wrapText(
textElement.originalText, textElement.originalText,
getFontString(textElement), getFontString(textElement),
@ -85,27 +83,21 @@ export const redrawTextBoundingBox = (
boundTextUpdates.baseline = metrics.baseline; boundTextUpdates.baseline = metrics.baseline;
if (container) { if (container) {
const containerDims = getContainerDims(container);
const maxContainerHeight = getBoundTextMaxHeight( const maxContainerHeight = getBoundTextMaxHeight(
container, container,
textElement as ExcalidrawTextElementWithContainer, textElement as ExcalidrawTextElementWithContainer,
); );
const maxContainerWidth = getBoundTextMaxWidth(container);
let nextHeight = containerDims.height;
if (metrics.height > maxContainerHeight) { if (metrics.height > maxContainerHeight) {
const nextHeight = computeContainerDimensionForBoundText( nextHeight = computeContainerDimensionForBoundText(
metrics.height, metrics.height,
container.type, container.type,
); );
mutateElement(container, { height: nextHeight }); mutateElement(container, { height: nextHeight });
updateOriginalContainerCache(container.id, nextHeight); updateOriginalContainerCache(container.id, nextHeight);
} }
if (metrics.width > maxContainerWidth) {
const nextWidth = computeContainerDimensionForBoundText(
metrics.width,
container.type,
);
mutateElement(container, { width: nextWidth });
}
const updatedTextElement = { const updatedTextElement = {
...textElement, ...textElement,
...boundTextUpdates, ...boundTextUpdates,
@ -163,7 +155,6 @@ export const bindTextToShapeAfterDuplication = (
export const handleBindTextResize = ( export const handleBindTextResize = (
container: NonDeletedExcalidrawElement, container: NonDeletedExcalidrawElement,
transformHandleType: MaybeTransformHandleType, transformHandleType: MaybeTransformHandleType,
shouldMaintainAspectRatio = false,
) => { ) => {
const boundTextElementId = getBoundTextElementId(container); const boundTextElementId = getBoundTextElementId(container);
if (!boundTextElementId) { if (!boundTextElementId) {
@ -184,17 +175,15 @@ export const handleBindTextResize = (
let text = textElement.text; let text = textElement.text;
let nextHeight = textElement.height; let nextHeight = textElement.height;
let nextWidth = textElement.width; let nextWidth = textElement.width;
const containerDims = getContainerDims(container);
const maxWidth = getBoundTextMaxWidth(container); const maxWidth = getBoundTextMaxWidth(container);
const maxHeight = getBoundTextMaxHeight( const maxHeight = getBoundTextMaxHeight(
container, container,
textElement as ExcalidrawTextElementWithContainer, textElement as ExcalidrawTextElementWithContainer,
); );
let containerHeight = container.height; let containerHeight = containerDims.height;
let nextBaseLine = textElement.baseline; let nextBaseLine = textElement.baseline;
if ( if (transformHandleType !== "n" && transformHandleType !== "s") {
shouldMaintainAspectRatio ||
(transformHandleType !== "n" && transformHandleType !== "s")
) {
if (text) { if (text) {
text = wrapText( text = wrapText(
textElement.originalText, textElement.originalText,
@ -218,7 +207,7 @@ export const handleBindTextResize = (
container.type, container.type,
); );
const diff = containerHeight - container.height; const diff = containerHeight - containerDims.height;
// fix the y coord when resizing from ne/nw/n // fix the y coord when resizing from ne/nw/n
const updatedY = const updatedY =
!isArrowElement(container) && !isArrowElement(container) &&
@ -698,6 +687,16 @@ export const getContainerElement = (
return null; return null;
}; };
export const getContainerDims = (element: ExcalidrawElement) => {
const MIN_WIDTH = 300;
if (isArrowElement(element)) {
const width = Math.max(element.width, MIN_WIDTH);
const height = element.height;
return { width, height };
}
return { width: element.width, height: element.height };
};
export const getContainerCenter = ( export const getContainerCenter = (
container: ExcalidrawElement, container: ExcalidrawElement,
appState: AppState, appState: AppState,
@ -866,9 +865,8 @@ const VALID_CONTAINER_TYPES = new Set([
"arrow", "arrow",
]); ]);
export const isValidTextContainer = (element: { export const isValidTextContainer = (element: ExcalidrawElement) =>
type: ExcalidrawElement["type"]; VALID_CONTAINER_TYPES.has(element.type);
}) => VALID_CONTAINER_TYPES.has(element.type);
export const computeContainerDimensionForBoundText = ( export const computeContainerDimensionForBoundText = (
dimension: number, dimension: number,
@ -889,19 +887,12 @@ export const computeContainerDimensionForBoundText = (
return dimension + padding; return dimension + padding;
}; };
export const getBoundTextMaxWidth = ( export const getBoundTextMaxWidth = (container: ExcalidrawElement) => {
container: ExcalidrawElement, const width = getContainerDims(container).width;
boundTextElement: ExcalidrawTextElement | null = getBoundTextElement(
container,
),
) => {
const { width } = container;
if (isArrowElement(container)) { if (isArrowElement(container)) {
const minWidth = return width - BOUND_TEXT_PADDING * 8 * 2;
(boundTextElement?.fontSize ?? DEFAULT_FONT_SIZE) *
ARROW_LABEL_FONT_SIZE_TO_MIN_WIDTH_RATIO;
return Math.max(ARROW_LABEL_WIDTH_FRACTION * width, minWidth);
} }
if (container.type === "ellipse") { if (container.type === "ellipse") {
// The width of the largest rectangle inscribed inside an ellipse is // The width of the largest rectangle inscribed inside an ellipse is
// Math.round((ellipse.width / 2) * Math.sqrt(2)) which is derived from // Math.round((ellipse.width / 2) * Math.sqrt(2)) which is derived from
@ -920,7 +911,7 @@ export const getBoundTextMaxHeight = (
container: ExcalidrawElement, container: ExcalidrawElement,
boundTextElement: ExcalidrawTextElementWithContainer, boundTextElement: ExcalidrawTextElementWithContainer,
) => { ) => {
const { height } = container; const height = getContainerDims(container).height;
if (isArrowElement(container)) { if (isArrowElement(container)) {
const containerHeight = height - BOUND_TEXT_PADDING * 8 * 2; const containerHeight = height - BOUND_TEXT_PADDING * 8 * 2;
if (containerHeight <= 0) { if (containerHeight <= 0) {

View File

@ -23,6 +23,7 @@ import { AppState } from "../types";
import { mutateElement } from "./mutateElement"; import { mutateElement } from "./mutateElement";
import { import {
getBoundTextElementId, getBoundTextElementId,
getContainerDims,
getContainerElement, getContainerElement,
getTextElementAngle, getTextElementAngle,
getTextWidth, getTextWidth,
@ -176,19 +177,20 @@ export const textWysiwyg = ({
updatedTextElement, updatedTextElement,
editable, editable,
); );
const containerDims = getContainerDims(container);
let originalContainerData; let originalContainerData;
if (propertiesUpdated) { if (propertiesUpdated) {
originalContainerData = updateOriginalContainerCache( originalContainerData = updateOriginalContainerCache(
container.id, container.id,
container.height, containerDims.height,
); );
} else { } else {
originalContainerData = originalContainerCache[container.id]; originalContainerData = originalContainerCache[container.id];
if (!originalContainerData) { if (!originalContainerData) {
originalContainerData = updateOriginalContainerCache( originalContainerData = updateOriginalContainerCache(
container.id, container.id,
container.height, containerDims.height,
); );
} }
} }
@ -212,7 +214,7 @@ export const textWysiwyg = ({
// autoshrink container height until original container height // autoshrink container height until original container height
// is reached when text is removed // is reached when text is removed
!isArrowElement(container) && !isArrowElement(container) &&
container.height > originalContainerData.height && containerDims.height > originalContainerData.height &&
textElementHeight < maxHeight textElementHeight < maxHeight
) { ) {
const targetContainerHeight = computeContainerDimensionForBoundText( const targetContainerHeight = computeContainerDimensionForBoundText(

View File

@ -86,15 +86,15 @@ export type ExcalidrawEllipseElement = _ExcalidrawElementBase & {
export type ExcalidrawEmbeddableElement = _ExcalidrawElementBase & export type ExcalidrawEmbeddableElement = _ExcalidrawElementBase &
Readonly<{ Readonly<{
type: "embeddable";
/** /**
* indicates whether the embeddable src (url) has been validated for rendering. * indicates whether the embeddable src (url) has been validated for rendering.
* null value indicates that the validation is pending. We reset the * nullish value indicates that the validation is pending. We reset the
* value on each restore (or url change) so that we can guarantee * value on each restore (or url change) so that we can guarantee
* the validation came from a trusted source (the editor). Also because we * the validation came from a trusted source (the editor). Also because we
* may not have access to host-app supplied url validator during restore. * may not have access to host-app supplied url validator during restore.
*/ */
validated: boolean | null; validated?: boolean;
type: "embeddable";
}>; }>;
export type ExcalidrawImageElement = _ExcalidrawElementBase & export type ExcalidrawImageElement = _ExcalidrawElementBase &
@ -123,6 +123,7 @@ export type ExcalidrawFrameElement = _ExcalidrawElementBase & {
export type ExcalidrawGenericElement = export type ExcalidrawGenericElement =
| ExcalidrawSelectionElement | ExcalidrawSelectionElement
| ExcalidrawRectangleElement | ExcalidrawRectangleElement
| ExcalidrawEmbeddableElement
| ExcalidrawDiamondElement | ExcalidrawDiamondElement
| ExcalidrawEllipseElement; | ExcalidrawEllipseElement;
@ -137,8 +138,7 @@ export type ExcalidrawElement =
| ExcalidrawLinearElement | ExcalidrawLinearElement
| ExcalidrawFreeDrawElement | ExcalidrawFreeDrawElement
| ExcalidrawImageElement | ExcalidrawImageElement
| ExcalidrawFrameElement | ExcalidrawFrameElement;
| ExcalidrawEmbeddableElement;
export type NonDeleted<TElement extends ExcalidrawElement> = TElement & { export type NonDeleted<TElement extends ExcalidrawElement> = TElement & {
isDeleted: boolean; isDeleted: boolean;

View File

@ -6,11 +6,12 @@ const SentryEnvHostnameMap: { [key: string]: string } = {
"vercel.app": "staging", "vercel.app": "staging",
}; };
const SENTRY_DISABLED = import.meta.env.VITE_APP_DISABLE_SENTRY === "true"; const REACT_APP_DISABLE_SENTRY =
import.meta.env.VITE_APP_DISABLE_SENTRY === "true";
// Disable Sentry locally or inside the Docker to avoid noise/respect privacy // Disable Sentry locally or inside the Docker to avoid noise/respect privacy
const onlineEnv = const onlineEnv =
!SENTRY_DISABLED && !REACT_APP_DISABLE_SENTRY &&
Object.keys(SentryEnvHostnameMap).find( Object.keys(SentryEnvHostnameMap).find(
(item) => window.location.hostname.indexOf(item) >= 0, (item) => window.location.hostname.indexOf(item) >= 0,
); );

View File

@ -1,14 +1,11 @@
import { StrictMode } from "react"; import { StrictMode } from "react";
import { createRoot } from "react-dom/client"; import { createRoot } from "react-dom/client";
import ExcalidrawApp from "./excalidraw-app"; import ExcalidrawApp from "./excalidraw-app";
import { registerSW } from "virtual:pwa-register";
import "./excalidraw-app/sentry"; import "./excalidraw-app/sentry";
window.__EXCALIDRAW_SHA__ = import.meta.env.VITE_APP_GIT_SHA; window.__EXCALIDRAW_SHA__ = import.meta.env.VITE_APP_GIT_SHA;
const rootElement = document.getElementById("root")!; const rootElement = document.getElementById("root")!;
const root = createRoot(rootElement); const root = createRoot(rootElement);
registerSW();
root.render( root.render(
<StrictMode> <StrictMode>
<ExcalidrawApp /> <ExcalidrawApp />

View File

@ -75,7 +75,6 @@ const {
WelcomeScreen, WelcomeScreen,
MainMenu, MainMenu,
LiveCollaborationTrigger, LiveCollaborationTrigger,
convertToExcalidrawElements,
} = window.ExcalidrawLib; } = window.ExcalidrawLib;
const COMMENT_ICON_DIMENSION = 32; const COMMENT_ICON_DIMENSION = 32;
@ -141,10 +140,7 @@ export default function App({ appTitle, useCustom, customArgs }: AppProps) {
]; ];
//@ts-ignore //@ts-ignore
initialStatePromiseRef.current.promise.resolve({ initialStatePromiseRef.current.promise.resolve(initialData);
...initialData,
elements: convertToExcalidrawElements(initialData.elements),
});
excalidrawAPI.addFiles(imagesArray); excalidrawAPI.addFiles(imagesArray);
}; };
}; };
@ -188,40 +184,38 @@ export default function App({ appTitle, useCustom, customArgs }: AppProps) {
const updateScene = () => { const updateScene = () => {
const sceneData = { const sceneData = {
elements: restoreElements( elements: restoreElements(
convertToExcalidrawElements([ [
{ {
type: "rectangle", type: "rectangle",
id: "rect-1", version: 141,
versionNonce: 361174001,
isDeleted: false,
id: "oDVXy8D6rom3H1-LLH2-f",
fillStyle: "hachure", fillStyle: "hachure",
strokeWidth: 1, strokeWidth: 1,
strokeStyle: "solid", strokeStyle: "solid",
roughness: 1, roughness: 1,
opacity: 100,
angle: 0, angle: 0,
x: 100.50390625, x: 100.50390625,
y: 93.67578125, y: 93.67578125,
strokeColor: "#c92a2a", strokeColor: "#c92a2a",
backgroundColor: "transparent",
width: 186.47265625, width: 186.47265625,
height: 141.9765625, height: 141.9765625,
seed: 1968410350, seed: 1968410350,
groupIds: [],
frameId: null,
boundElements: null,
locked: false,
link: null,
updated: 1,
roundness: { roundness: {
type: ROUNDNESS.ADAPTIVE_RADIUS, type: ROUNDNESS.ADAPTIVE_RADIUS,
value: 32, value: 32,
}, },
}, },
{ ],
type: "arrow",
x: 300,
y: 150,
start: { id: "rect-1" },
end: { type: "ellipse" },
},
{
type: "text",
x: 300,
y: 100,
text: "HELLO WORLD!",
},
]),
null, null,
), ),
appState: { appState: {

View File

@ -253,4 +253,3 @@ export { LiveCollaborationTrigger };
export { DefaultSidebar } from "../../components/DefaultSidebar"; export { DefaultSidebar } from "../../components/DefaultSidebar";
export { normalizeLink } from "../../data/url"; export { normalizeLink } from "../../data/url";
export { convertToExcalidrawElements } from "../../data/transform";

View File

@ -2,8 +2,8 @@ const path = require("path");
const webpack = require("webpack"); const webpack = require("webpack");
const autoprefixer = require("autoprefixer"); const autoprefixer = require("autoprefixer");
const { parseEnvVariables } = require("./env"); const { parseEnvVariables } = require("./env");
const outputDir = process.env.EXAMPLE === "true" ? "example/public" : "dist";
const outputDir = process.env.EXAMPLE === "true" ? "example/public" : "dist";
module.exports = { module.exports = {
mode: "development", mode: "development",
devtool: false, devtool: false,
@ -17,6 +17,7 @@ module.exports = {
filename: "[name].js", filename: "[name].js",
chunkFilename: "excalidraw-assets-dev/[name]-[contenthash].js", chunkFilename: "excalidraw-assets-dev/[name]-[contenthash].js",
assetModuleFilename: "excalidraw-assets-dev/[name][ext]", assetModuleFilename: "excalidraw-assets-dev/[name][ext]",
publicPath: "", publicPath: "",
}, },
resolve: { resolve: {
@ -44,7 +45,7 @@ module.exports = {
{ {
test: /\.(ts|tsx|js|jsx|mjs)$/, test: /\.(ts|tsx|js|jsx|mjs)$/,
exclude: exclude:
/node_modules[\\/](?!(browser-fs-access|canvas-roundrect-polyfill))/, /node_modules\/(?!(browser-fs-access|canvas-roundrect-polyfill))/,
use: [ use: [
{ {
loader: "import-meta-loader", loader: "import-meta-loader",

View File

@ -1,10 +1,10 @@
const path = require("path"); const path = require("path");
const webpack = require("webpack");
const autoprefixer = require("autoprefixer");
const { parseEnvVariables } = require("./env");
const TerserPlugin = require("terser-webpack-plugin"); const TerserPlugin = require("terser-webpack-plugin");
const BundleAnalyzerPlugin = const BundleAnalyzerPlugin =
require("webpack-bundle-analyzer").BundleAnalyzerPlugin; require("webpack-bundle-analyzer").BundleAnalyzerPlugin;
const autoprefixer = require("autoprefixer");
const webpack = require("webpack");
const { parseEnvVariables } = require("./env");
module.exports = { module.exports = {
mode: "production", mode: "production",
@ -47,7 +47,8 @@ module.exports = {
{ {
test: /\.(ts|tsx|js|jsx|mjs)$/, test: /\.(ts|tsx|js|jsx|mjs)$/,
exclude: exclude:
/node_modules[\\/](?!(browser-fs-access|canvas-roundrect-polyfill))/, /node_modules\/(?!(browser-fs-access|canvas-roundrect-polyfill))/,
use: [ use: [
{ {
loader: "import-meta-loader", loader: "import-meta-loader",

View File

@ -15,6 +15,18 @@ import Scene from "./Scene";
export const SVG_EXPORT_TAG = `<!-- svg-source:excalidraw -->`; export const SVG_EXPORT_TAG = `<!-- svg-source:excalidraw -->`;
const createScene = (
elements: readonly NonDeletedExcalidrawElement[],
): Scene | null => {
if (!elements || Scene.getScene(elements[0])) {
return null;
}
const scene = new Scene();
scene.replaceAllElements(elements);
elements?.forEach((el) => Scene.mapElementToScene(el, scene));
return scene;
};
export const exportToCanvas = async ( export const exportToCanvas = async (
elements: readonly NonDeletedExcalidrawElement[], elements: readonly NonDeletedExcalidrawElement[],
appState: AppState, appState: AppState,
@ -38,6 +50,7 @@ export const exportToCanvas = async (
return { canvas, scale: appState.exportScale }; return { canvas, scale: appState.exportScale };
}, },
) => { ) => {
const scene = createScene(elements);
const [minX, minY, width, height] = getCanvasSize(elements, exportPadding); const [minX, minY, width, height] = getCanvasSize(elements, exportPadding);
const { canvas, scale = 1 } = createCanvas(width, height); const { canvas, scale = 1 } = createCanvas(width, height);
@ -79,6 +92,7 @@ export const exportToCanvas = async (
}, },
}); });
scene?.destroy();
return canvas; return canvas;
}; };
@ -105,6 +119,7 @@ export const exportToSvg = async (
exportScale = 1, exportScale = 1,
exportEmbedScene, exportEmbedScene,
} = appState; } = appState;
const scene = createScene(elements);
let metadata = ""; let metadata = "";
if (exportEmbedScene) { if (exportEmbedScene) {
try { try {
@ -217,6 +232,7 @@ export const exportToSvg = async (
renderEmbeddables: opts?.renderEmbeddables, renderEmbeddables: opts?.renderEmbeddables,
}); });
scene?.destroy();
return svgRoot; return svgRoot;
}; };

View File

@ -140,8 +140,9 @@ describe("restoreElements", () => {
expect(restoredArrow).toMatchSnapshot({ seed: expect.any(Number) }); expect(restoredArrow).toMatchSnapshot({ seed: expect.any(Number) });
}); });
it('should set arrow element endArrowHead as "arrow" when arrow element endArrowHead is null', () => { it("when arrow element has defined endArrowHead", () => {
const arrowElement = API.createElement({ type: "arrow" }); const arrowElement = API.createElement({ type: "arrow" });
const restoredElements = restore.restoreElements([arrowElement], null); const restoredElements = restore.restoreElements([arrowElement], null);
const restoredArrow = restoredElements[0] as ExcalidrawLinearElement; const restoredArrow = restoredElements[0] as ExcalidrawLinearElement;
@ -149,7 +150,7 @@ describe("restoreElements", () => {
expect(arrowElement.endArrowhead).toBe(restoredArrow.endArrowhead); expect(arrowElement.endArrowhead).toBe(restoredArrow.endArrowhead);
}); });
it('should set arrow element endArrowHead as "arrow" when arrow element endArrowHead is undefined', () => { it("when arrow element has undefined endArrowHead", () => {
const arrowElement = API.createElement({ type: "arrow" }); const arrowElement = API.createElement({ type: "arrow" });
Object.defineProperty(arrowElement, "endArrowhead", { Object.defineProperty(arrowElement, "endArrowhead", {
get: vi.fn(() => undefined), get: vi.fn(() => undefined),

View File

@ -34,7 +34,6 @@ export const rectangleFixture: ExcalidrawElement = {
export const embeddableFixture: ExcalidrawElement = { export const embeddableFixture: ExcalidrawElement = {
...elementBase, ...elementBase,
type: "embeddable", type: "embeddable",
validated: null,
}; };
export const ellipseFixture: ExcalidrawElement = { export const ellipseFixture: ExcalidrawElement = {
...elementBase, ...elementBase,

View File

@ -15,11 +15,7 @@ import fs from "fs";
import util from "util"; import util from "util";
import path from "path"; import path from "path";
import { getMimeType } from "../../data/blob"; import { getMimeType } from "../../data/blob";
import { import { newFreeDrawElement, newImageElement } from "../../element/newElement";
newEmbeddableElement,
newFreeDrawElement,
newImageElement,
} from "../../element/newElement";
import { Point } from "../../types"; import { Point } from "../../types";
import { getSelectedElements } from "../../scene/selection"; import { getSelectedElements } from "../../scene/selection";
import { isLinearElementType } from "../../element/typeChecks"; import { isLinearElementType } from "../../element/typeChecks";
@ -182,20 +178,14 @@ export class API {
case "rectangle": case "rectangle":
case "diamond": case "diamond":
case "ellipse": case "ellipse":
case "embeddable":
element = newElement({ element = newElement({
type: type as "rectangle" | "diamond" | "ellipse", type: type as "rectangle" | "diamond" | "ellipse" | "embeddable",
width, width,
height, height,
...base, ...base,
}); });
break; break;
case "embeddable":
element = newEmbeddableElement({
type: "embeddable",
...base,
validated: null,
});
break;
case "text": case "text":
const fontSize = rest.fontSize ?? appState.currentItemFontSize; const fontSize = rest.fontSize ?? appState.currentItemFontSize;
const fontFamily = rest.fontFamily ?? appState.currentItemFontFamily; const fontFamily = rest.fontFamily ?? appState.currentItemFontFamily;

View File

@ -1128,7 +1128,7 @@ describe("Test Linear Elements", () => {
height: 500, height: 500,
}); });
const arrow = UI.createElement("arrow", { const arrow = UI.createElement("arrow", {
x: -10, x: 210,
y: 250, y: 250,
width: 400, width: 400,
height: 1, height: 1,
@ -1152,8 +1152,8 @@ describe("Test Linear Elements", () => {
expect( expect(
wrapText(textElement.originalText, font, getBoundTextMaxWidth(arrow)), wrapText(textElement.originalText, font, getBoundTextMaxWidth(arrow)),
).toMatchInlineSnapshot(` ).toMatchInlineSnapshot(`
"Online whiteboard "Online whiteboard collaboration
collaboration made easy" made easy"
`); `);
const handleBindTextResizeSpy = vi.spyOn( const handleBindTextResizeSpy = vi.spyOn(
textElementUtils, textElementUtils,
@ -1165,7 +1165,7 @@ describe("Test Linear Elements", () => {
mouse.moveTo(200, 0); mouse.moveTo(200, 0);
mouse.upAt(200, 0); mouse.upAt(200, 0);
expect(arrow.width).toBe(200); expect(arrow.width).toBe(170);
expect(rect.x).toBe(200); expect(rect.x).toBe(200);
expect(rect.y).toBe(0); expect(rect.y).toBe(0);
expect(handleBindTextResizeSpy).toHaveBeenCalledWith( expect(handleBindTextResizeSpy).toHaveBeenCalledWith(

View File

@ -914,16 +914,3 @@ export const isOnlyExportingSingleFrame = (
) )
); );
}; };
export const assertNever = (
value: never,
message: string,
softAssert?: boolean,
): never => {
if (softAssert) {
console.error(message);
return value;
}
throw new Error(message);
};

3
src/vite-env.d.ts vendored
View File

@ -1,7 +1,8 @@
/// <reference types="vite/client" /> /// <reference types="vite/client" />
/// <reference types="vite-plugin-pwa/vanillajs" /> /// <reference types="vite-plugin-pwa/react" />
/// <reference types="vite-plugin-pwa/info" /> /// <reference types="vite-plugin-pwa/info" />
/// <reference types="vite-plugin-svgr/client" /> /// <reference types="vite-plugin-svgr/client" />
interface ImportMetaEnv { interface ImportMetaEnv {
// The port to run the dev server // The port to run the dev server
VITE_APP_PORT: string; VITE_APP_PORT: string;

View File

@ -53,7 +53,6 @@ export default defineConfig({
svgrPlugin(), svgrPlugin(),
ViteEjsPlugin(), ViteEjsPlugin(),
VitePWA({ VitePWA({
registerType: "autoUpdate",
devOptions: { devOptions: {
/* set this flag to true to enable in Development mode */ /* set this flag to true to enable in Development mode */
enabled: false, enabled: false,
@ -61,7 +60,7 @@ export default defineConfig({
workbox: { workbox: {
// Don't push fonts and locales to app precache // Don't push fonts and locales to app precache
globIgnores: ["fonts.css", "**/locales/**", "service-worker.js"], globIgnores: ["fonts.css", "**/locales/**"],
runtimeCaching: [ runtimeCaching: [
{ {
urlPattern: new RegExp("/.+.(ttf|woff2|otf)"), urlPattern: new RegExp("/.+.(ttf|woff2|otf)"),

View File

@ -5,12 +5,5 @@ export default defineConfig({
setupFiles: ["./src/setupTests.ts"], setupFiles: ["./src/setupTests.ts"],
globals: true, globals: true,
environment: "jsdom", environment: "jsdom",
coverage: {
reporter: ["text", "json-summary", "json"],
lines: 70,
branches: 70,
functions: 68,
statements: 70,
},
}, },
}); });

117
yarn.lock
View File

@ -2,7 +2,7 @@
# yarn lockfile v1 # yarn lockfile v1
"@ampproject/remapping@^2.2.0", "@ampproject/remapping@^2.2.1": "@ampproject/remapping@^2.2.0":
version "2.2.1" version "2.2.1"
resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.2.1.tgz#99e8e11851128b8702cd57c33684f1d0f260b630" resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.2.1.tgz#99e8e11851128b8702cd57c33684f1d0f260b630"
integrity sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg== integrity sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==
@ -1260,11 +1260,6 @@
"@babel/helper-validator-identifier" "^7.22.5" "@babel/helper-validator-identifier" "^7.22.5"
to-fast-properties "^2.0.0" to-fast-properties "^2.0.0"
"@bcoe/v8-coverage@^0.2.3":
version "0.2.3"
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
"@braintree/sanitize-url@6.0.2": "@braintree/sanitize-url@6.0.2":
version "6.0.2" version "6.0.2"
resolved "https://registry.yarnpkg.com/@braintree/sanitize-url/-/sanitize-url-6.0.2.tgz#6110f918d273fe2af8ea1c4398a88774bb9fc12f" resolved "https://registry.yarnpkg.com/@braintree/sanitize-url/-/sanitize-url-6.0.2.tgz#6110f918d273fe2af8ea1c4398a88774bb9fc12f"
@ -1817,11 +1812,6 @@
resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45" resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45"
integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA== integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==
"@istanbuljs/schema@^0.1.2":
version "0.1.3"
resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.3.tgz#e45e384e4b8ec16bce2fd903af78450f6bf7ec98"
integrity sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==
"@jest/expect-utils@^29.5.0": "@jest/expect-utils@^29.5.0":
version "29.5.0" version "29.5.0"
resolved "https://registry.yarnpkg.com/@jest/expect-utils/-/expect-utils-29.5.0.tgz#f74fad6b6e20f924582dc8ecbf2cb800fe43a036" resolved "https://registry.yarnpkg.com/@jest/expect-utils/-/expect-utils-29.5.0.tgz#f74fad6b6e20f924582dc8ecbf2cb800fe43a036"
@ -1880,12 +1870,12 @@
resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24" resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24"
integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw== integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==
"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.13", "@jridgewell/sourcemap-codec@^1.4.15": "@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.13":
version "1.4.15" version "1.4.15"
resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32" resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32"
integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==
"@jridgewell/trace-mapping@^0.3.12", "@jridgewell/trace-mapping@^0.3.17", "@jridgewell/trace-mapping@^0.3.9": "@jridgewell/trace-mapping@^0.3.17", "@jridgewell/trace-mapping@^0.3.9":
version "0.3.18" version "0.3.18"
resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz#25783b2086daf6ff1dcb53c9249ae480e4dd4cd6" resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz#25783b2086daf6ff1dcb53c9249ae480e4dd4cd6"
integrity sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA== integrity sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==
@ -2510,7 +2500,7 @@
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.1.tgz#aa22750962f3bf0e79d753d3cc067f010c95f194" resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.1.tgz#aa22750962f3bf0e79d753d3cc067f010c95f194"
integrity sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA== integrity sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==
"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1": "@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0":
version "2.0.4" version "2.0.4"
resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz#8467d4b3c087805d63580480890791277ce35c44" resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz#8467d4b3c087805d63580480890791277ce35c44"
integrity sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g== integrity sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==
@ -2796,23 +2786,6 @@
magic-string "^0.27.0" magic-string "^0.27.0"
react-refresh "^0.14.0" react-refresh "^0.14.0"
"@vitest/coverage-v8@0.33.0":
version "0.33.0"
resolved "https://registry.yarnpkg.com/@vitest/coverage-v8/-/coverage-v8-0.33.0.tgz#dfcb36cf51624a89d33ab0962a6eec8a41346ef2"
integrity sha512-Rj5IzoLF7FLj6yR7TmqsfRDSeaFki6NAJ/cQexqhbWkHEV2htlVGrmuOde3xzvFsCbLCagf4omhcIaVmfU8Okg==
dependencies:
"@ampproject/remapping" "^2.2.1"
"@bcoe/v8-coverage" "^0.2.3"
istanbul-lib-coverage "^3.2.0"
istanbul-lib-report "^3.0.0"
istanbul-lib-source-maps "^4.0.1"
istanbul-reports "^3.1.5"
magic-string "^0.30.1"
picocolors "^1.0.0"
std-env "^3.3.3"
test-exclude "^6.0.0"
v8-to-istanbul "^9.1.0"
"@vitest/expect@0.32.2": "@vitest/expect@0.32.2":
version "0.32.2" version "0.32.2"
resolved "https://registry.yarnpkg.com/@vitest/expect/-/expect-0.32.2.tgz#8111f6ab1ff3b203efbe3a25e8bb2d160ce4b720" resolved "https://registry.yarnpkg.com/@vitest/expect/-/expect-0.32.2.tgz#8111f6ab1ff3b203efbe3a25e8bb2d160ce4b720"
@ -3529,7 +3502,7 @@ confusing-browser-globals@^1.0.11:
resolved "https://registry.yarnpkg.com/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz#ae40e9b57cdd3915408a2805ebd3a5585608dc81" resolved "https://registry.yarnpkg.com/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz#ae40e9b57cdd3915408a2805ebd3a5585608dc81"
integrity sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA== integrity sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA==
convert-source-map@^1.6.0, convert-source-map@^1.7.0: convert-source-map@^1.7.0:
version "1.9.0" version "1.9.0"
resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.9.0.tgz#7faae62353fb4213366d0ca98358d22e8368b05f" resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.9.0.tgz#7faae62353fb4213366d0ca98358d22e8368b05f"
integrity sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A== integrity sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==
@ -4586,7 +4559,7 @@ glob-parent@^5.1.2, glob-parent@~5.1.2:
dependencies: dependencies:
is-glob "^4.0.1" is-glob "^4.0.1"
glob@^7.1.3, glob@^7.1.4, glob@^7.1.6: glob@^7.1.3, glob@^7.1.6:
version "7.2.3" version "7.2.3"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b"
integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==
@ -4721,11 +4694,6 @@ html-encoding-sniffer@^3.0.0:
dependencies: dependencies:
whatwg-encoding "^2.0.0" whatwg-encoding "^2.0.0"
html-escaper@^2.0.0:
version "2.0.2"
resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453"
integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==
http-parser-js@>=0.5.1: http-parser-js@>=0.5.1:
version "0.5.8" version "0.5.8"
resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.5.8.tgz#af23090d9ac4e24573de6f6aecc9d84a48bf20e3" resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.5.8.tgz#af23090d9ac4e24573de6f6aecc9d84a48bf20e3"
@ -5115,37 +5083,6 @@ isexe@^2.0.0:
resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==
istanbul-lib-coverage@^3.0.0, istanbul-lib-coverage@^3.2.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz#189e7909d0a39fa5a3dfad5b03f71947770191d3"
integrity sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==
istanbul-lib-report@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz#908305bac9a5bd175ac6a74489eafd0fc2445a7d"
integrity sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==
dependencies:
istanbul-lib-coverage "^3.0.0"
make-dir "^4.0.0"
supports-color "^7.1.0"
istanbul-lib-source-maps@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz#895f3a709fcfba34c6de5a42939022f3e4358551"
integrity sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==
dependencies:
debug "^4.1.1"
istanbul-lib-coverage "^3.0.0"
source-map "^0.6.1"
istanbul-reports@^3.1.5:
version "3.1.6"
resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-3.1.6.tgz#2544bcab4768154281a2f0870471902704ccaa1a"
integrity sha512-TLgnMkKg3iTDsQ9PbPTdpfAK2DzjF9mqUG7RMgcQl8oFjad8ob4laGxv5XV5U9MAfx8D6tSJiUyuAwzLicaxlg==
dependencies:
html-escaper "^2.0.0"
istanbul-lib-report "^3.0.0"
jake@^10.8.5: jake@^10.8.5:
version "10.8.5" version "10.8.5"
resolved "https://registry.yarnpkg.com/jake/-/jake-10.8.5.tgz#f2183d2c59382cb274226034543b9c03b8164c46" resolved "https://registry.yarnpkg.com/jake/-/jake-10.8.5.tgz#f2183d2c59382cb274226034543b9c03b8164c46"
@ -5568,20 +5505,6 @@ magic-string@^0.30.0:
dependencies: dependencies:
"@jridgewell/sourcemap-codec" "^1.4.13" "@jridgewell/sourcemap-codec" "^1.4.13"
magic-string@^0.30.1:
version "0.30.2"
resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.2.tgz#dcf04aad3d0d1314bc743d076c50feb29b3c7aca"
integrity sha512-lNZdu7pewtq/ZvWUp9Wpf/x7WzMTsR26TWV03BRZrXFsv+BI6dy8RAiKgm1uM/kyR0rCfUcqvOlXKG66KhIGug==
dependencies:
"@jridgewell/sourcemap-codec" "^1.4.15"
make-dir@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-4.0.0.tgz#c3c2307a771277cd9638305f915c29ae741b614e"
integrity sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==
dependencies:
semver "^7.5.3"
md5-hex@^3.0.1: md5-hex@^3.0.1:
version "3.0.1" version "3.0.1"
resolved "https://registry.yarnpkg.com/md5-hex/-/md5-hex-3.0.1.tgz#be3741b510591434b2784d79e556eefc2c9a8e5c" resolved "https://registry.yarnpkg.com/md5-hex/-/md5-hex-3.0.1.tgz#be3741b510591434b2784d79e556eefc2c9a8e5c"
@ -6532,7 +6455,7 @@ semver@^7.2.1, semver@^7.3.2, semver@^7.3.7:
dependencies: dependencies:
lru-cache "^6.0.0" lru-cache "^6.0.0"
semver@^7.3.4, semver@^7.5.0, semver@^7.5.3: semver@^7.3.4, semver@^7.5.0:
version "7.5.4" version "7.5.4"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e"
integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==
@ -6703,7 +6626,7 @@ stackback@0.0.2:
resolved "https://registry.yarnpkg.com/stackback/-/stackback-0.0.2.tgz#1ac8a0d9483848d1695e418b6d031a3c3ce68e3b" resolved "https://registry.yarnpkg.com/stackback/-/stackback-0.0.2.tgz#1ac8a0d9483848d1695e418b6d031a3c3ce68e3b"
integrity sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw== integrity sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==
std-env@^3.3.2, std-env@^3.3.3: std-env@^3.3.2:
version "3.3.3" version "3.3.3"
resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.3.3.tgz#a54f06eb245fdcfef53d56f3c0251f1d5c3d01fe" resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.3.3.tgz#a54f06eb245fdcfef53d56f3c0251f1d5c3d01fe"
integrity sha512-Rz6yejtVyWnVjC1RFvNmYL10kgjC49EOghxWn0RFqlCHGFpQx+Xe7yW3I4ceK1SGrWIGMjD5Kbue8W/udkbMJg== integrity sha512-Rz6yejtVyWnVjC1RFvNmYL10kgjC49EOghxWn0RFqlCHGFpQx+Xe7yW3I4ceK1SGrWIGMjD5Kbue8W/udkbMJg==
@ -6911,15 +6834,6 @@ terser@^5.0.0:
commander "^2.20.0" commander "^2.20.0"
source-map-support "~0.5.20" source-map-support "~0.5.20"
test-exclude@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-6.0.0.tgz#04a8698661d805ea6fa293b6cb9e63ac044ef15e"
integrity sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==
dependencies:
"@istanbuljs/schema" "^0.1.2"
glob "^7.1.4"
minimatch "^3.0.4"
text-table@^0.2.0: text-table@^0.2.0:
version "0.2.0" version "0.2.0"
resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
@ -7222,15 +7136,6 @@ v8-compile-cache@^2.0.3:
resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee" resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee"
integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA== integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==
v8-to-istanbul@^9.1.0:
version "9.1.0"
resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-9.1.0.tgz#1b83ed4e397f58c85c266a570fc2558b5feb9265"
integrity sha512-6z3GW9x8G1gd+JIIgQQQxXuiJtCXeAjp6RaPEPLv62mH3iPHPxV6W3robxtCzNErRo6ZwTmzWhsbNvjyEBKzKA==
dependencies:
"@jridgewell/trace-mapping" "^0.3.12"
"@types/istanbul-lib-coverage" "^2.0.1"
convert-source-map "^1.6.0"
vite-node@0.32.2: vite-node@0.32.2:
version "0.32.2" version "0.32.2"
resolved "https://registry.yarnpkg.com/vite-node/-/vite-node-0.32.2.tgz#bfccdfeb708b2309ea9e5fe424951c75bb9c0096" resolved "https://registry.yarnpkg.com/vite-node/-/vite-node-0.32.2.tgz#bfccdfeb708b2309ea9e5fe424951c75bb9c0096"
@ -7534,9 +7439,9 @@ why-is-node-running@^2.2.2:
stackback "0.0.2" stackback "0.0.2"
word-wrap@^1.2.3: word-wrap@^1.2.3:
version "1.2.5" version "1.2.3"
resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34" resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c"
integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==
workbox-background-sync@7.0.0: workbox-background-sync@7.0.0:
version "7.0.0" version "7.0.0"