Merge branch 'master' into ryan-di/line-snapping

# Conflicts:
#	packages/element/src/linearElementEditor.ts
#	packages/element/src/snapping.ts
#	packages/excalidraw/components/App.tsx
This commit is contained in:
dwelle 2025-07-31 22:39:46 +02:00
commit 09b18cacec
69 changed files with 1810 additions and 856 deletions

View File

@ -24,4 +24,4 @@ jobs:
- name: Auto release - name: Auto release
run: | run: |
yarn add @actions/core -W yarn add @actions/core -W
yarn autorelease yarn release --tag=next --non-interactive

View File

@ -1,55 +0,0 @@
name: Auto release excalidraw preview
on:
issue_comment:
types: [created, edited]
jobs:
Auto-release-excalidraw-preview:
name: Auto release preview
if: github.event.comment.body == '@excalibot trigger release' && github.event.issue.pull_request
runs-on: ubuntu-latest
steps:
- name: React to release comment
uses: peter-evans/create-or-update-comment@v1
with:
token: ${{ secrets.PUSH_TRANSLATIONS_COVERAGE_PAT }}
comment-id: ${{ github.event.comment.id }}
reactions: "+1"
- name: Get PR SHA
id: sha
uses: actions/github-script@v4
with:
result-encoding: string
script: |
const { owner, repo, number } = context.issue;
const pr = await github.pulls.get({
owner,
repo,
pull_number: number,
});
return pr.data.head.sha
- uses: actions/checkout@v2
with:
ref: ${{ steps.sha.outputs.result }}
fetch-depth: 2
- name: Setup Node.js 18.x
uses: actions/setup-node@v2
with:
node-version: 18.x
- name: Set up publish access
run: |
npm config set //registry.npmjs.org/:_authToken ${NPM_TOKEN}
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Auto release preview
id: "autorelease"
run: |
yarn add @actions/core -W
yarn autorelease preview ${{ github.event.issue.number }}
- name: Post comment post release
if: always()
uses: peter-evans/create-or-update-comment@v1
with:
token: ${{ secrets.PUSH_TRANSLATIONS_COVERAGE_PAT }}
issue-number: ${{ github.event.issue.number }}
body: "@${{ github.event.comment.user.login }} ${{ steps.autorelease.outputs.result }}"

View File

@ -28,32 +28,12 @@ To start the example app using the `@excalidraw/excalidraw` package, follow the
## Releasing ## Releasing
### Create a test release
You can create a test release by posting the below comment in your pull request:
```bash
@excalibot trigger release
```
Once the version is released `@excalibot` will post a comment with the release version.
### Creating a production release ### Creating a production release
To release the next stable version follow the below steps: To release the next stable version follow the below steps:
```bash ```bash
yarn prerelease:excalidraw yarn release --tag=latest --version=0.19.0
``` ```
You need to pass the `version` for which you want to create the release. This will make the changes needed before making the release like updating `package.json`, `changelog` and more. You will need to pass the `latest` tag with `version` for which you want to create the release. This will make the changes needed before publishing the packages into NPM, like updating dependencies of all `@excalidraw/*` packages, generating new entries in `CHANGELOG.md` and more.
The next step is to run the `release` script:
```bash
yarn release:excalidraw
```
This will publish the package.
Right now there are two steps to create a production release but once this works fine these scripts will be combined and more automation will be done.

View File

@ -33,6 +33,7 @@ const ExcalidrawScope = {
initialData, initialData,
useI18n: ExcalidrawComp.useI18n, useI18n: ExcalidrawComp.useI18n,
convertToExcalidrawElements: ExcalidrawComp.convertToExcalidrawElements, convertToExcalidrawElements: ExcalidrawComp.convertToExcalidrawElements,
CaptureUpdateAction: ExcalidrawComp.CaptureUpdateAction,
}; };
export default ExcalidrawScope; export default ExcalidrawScope;

View File

@ -3,7 +3,8 @@
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"scripts": { "scripts": {
"build:workspace": "yarn workspace @excalidraw/excalidraw run build:esm && yarn copy:assets", "build:packages": "yarn --cwd ../../ build:packages",
"build:workspace": "yarn build:packages && yarn copy:assets",
"copy:assets": "cp -r ../../packages/excalidraw/dist/prod/fonts ./public", "copy:assets": "cp -r ../../packages/excalidraw/dist/prod/fonts ./public",
"dev": "yarn build:workspace && next dev -p 3005", "dev": "yarn build:workspace && next dev -p 3005",
"build": "yarn build:workspace && next build", "build": "yarn build:workspace && next build",

View File

@ -17,6 +17,6 @@
"build": "vite build", "build": "vite build",
"preview": "vite preview --port 5002", "preview": "vite preview --port 5002",
"build:preview": "yarn build && yarn preview", "build:preview": "yarn build && yarn preview",
"build:package": "yarn workspace @excalidraw/excalidraw run build:esm" "build:packages": "yarn --cwd ../../ build:packages"
} }
} }

View File

@ -1,5 +1,5 @@
{ {
"outputDirectory": "dist", "outputDirectory": "dist",
"installCommand": "yarn install", "installCommand": "yarn install",
"buildCommand": "yarn build:package && yarn build" "buildCommand": "yarn build:packages && yarn build"
} }

View File

@ -9,7 +9,7 @@ import {
} from "@excalidraw/excalidraw/renderer/helpers"; } from "@excalidraw/excalidraw/renderer/helpers";
import { type AppState } from "@excalidraw/excalidraw/types"; import { type AppState } from "@excalidraw/excalidraw/types";
import { throttleRAF } from "@excalidraw/common"; import { throttleRAF } from "@excalidraw/common";
import { useCallback, useImperativeHandle, useRef } from "react"; import { useCallback } from "react";
import { import {
isLineSegment, isLineSegment,
@ -18,6 +18,8 @@ import {
} from "@excalidraw/math"; } from "@excalidraw/math";
import { isCurve } from "@excalidraw/math/curve"; import { isCurve } from "@excalidraw/math/curve";
import React from "react";
import type { Curve } from "@excalidraw/math"; import type { Curve } from "@excalidraw/math";
import type { DebugElement } from "@excalidraw/utils/visualdebug"; import type { DebugElement } from "@excalidraw/utils/visualdebug";
@ -113,10 +115,6 @@ const _debugRenderer = (
scale, scale,
); );
if (appState.height !== canvas.height || appState.width !== canvas.width) {
refresh();
}
const context = bootstrapCanvas({ const context = bootstrapCanvas({
canvas, canvas,
scale, scale,
@ -314,19 +312,12 @@ export const DebugFooter = ({ onChange }: { onChange: () => void }) => {
interface DebugCanvasProps { interface DebugCanvasProps {
appState: AppState; appState: AppState;
scale: number; scale: number;
ref?: React.Ref<HTMLCanvasElement>;
} }
const DebugCanvas = ({ appState, scale, ref }: DebugCanvasProps) => { const DebugCanvas = React.forwardRef<HTMLCanvasElement, DebugCanvasProps>(
({ appState, scale }, ref) => {
const { width, height } = appState; const { width, height } = appState;
const canvasRef = useRef<HTMLCanvasElement>(null);
useImperativeHandle<HTMLCanvasElement | null, HTMLCanvasElement | null>(
ref,
() => canvasRef.current,
[canvasRef],
);
return ( return (
<canvas <canvas
style={{ style={{
@ -338,11 +329,12 @@ const DebugCanvas = ({ appState, scale, ref }: DebugCanvasProps) => {
}} }}
width={width * scale} width={width * scale}
height={height * scale} height={height * scale}
ref={canvasRef} ref={ref}
> >
Debug Canvas Debug Canvas
</canvas> </canvas>
); );
}; },
);
export default DebugCanvas; export default DebugCanvas;

View File

@ -52,13 +52,17 @@
"build-node": "node ./scripts/build-node.js", "build-node": "node ./scripts/build-node.js",
"build:app:docker": "yarn --cwd ./excalidraw-app build:app:docker", "build:app:docker": "yarn --cwd ./excalidraw-app build:app:docker",
"build:app": "yarn --cwd ./excalidraw-app build:app", "build:app": "yarn --cwd ./excalidraw-app build:app",
"build:package": "yarn --cwd ./packages/excalidraw build:esm", "build:common": "yarn --cwd ./packages/common build:esm",
"build:element": "yarn --cwd ./packages/element build:esm",
"build:excalidraw": "yarn --cwd ./packages/excalidraw build:esm",
"build:math": "yarn --cwd ./packages/math build:esm",
"build:packages": "yarn build:common && yarn build:math && yarn build:element && yarn build:excalidraw",
"build:version": "yarn --cwd ./excalidraw-app build:version", "build:version": "yarn --cwd ./excalidraw-app build:version",
"build": "yarn --cwd ./excalidraw-app build", "build": "yarn --cwd ./excalidraw-app build",
"build:preview": "yarn --cwd ./excalidraw-app build:preview", "build:preview": "yarn --cwd ./excalidraw-app build:preview",
"start": "yarn --cwd ./excalidraw-app start", "start": "yarn --cwd ./excalidraw-app start",
"start:production": "yarn --cwd ./excalidraw-app start:production", "start:production": "yarn --cwd ./excalidraw-app start:production",
"start:example": "yarn build:package && yarn --cwd ./examples/with-script-in-browser start", "start:example": "yarn build:packages && yarn --cwd ./examples/with-script-in-browser start",
"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 --watch=false",
"test:app": "vitest", "test:app": "vitest",
"test:code": "eslint --max-warnings=0 --ext .js,.ts,.tsx .", "test:code": "eslint --max-warnings=0 --ext .js,.ts,.tsx .",
@ -76,9 +80,10 @@
"locales-coverage:description": "node scripts/locales-coverage-description.js", "locales-coverage:description": "node scripts/locales-coverage-description.js",
"prepare": "husky install", "prepare": "husky install",
"prettier": "prettier \"**/*.{css,scss,json,md,html,yml}\" --ignore-path=.eslintignore", "prettier": "prettier \"**/*.{css,scss,json,md,html,yml}\" --ignore-path=.eslintignore",
"autorelease": "node scripts/autorelease.js", "release": "node scripts/release.js",
"prerelease:excalidraw": "node scripts/prerelease.js", "release:test": "node scripts/release.js --tag=test",
"release:excalidraw": "node scripts/release.js", "release:next": "node scripts/release.js --tag=next",
"release:latest": "node scripts/release.js --tag=latest",
"rm:build": "rimraf --glob excalidraw-app/build excalidraw-app/dist excalidraw-app/dev-dist packages/*/dist packages/*/build examples/*/build examples/*/dist", "rm:build": "rimraf --glob excalidraw-app/build excalidraw-app/dist excalidraw-app/dev-dist packages/*/dist packages/*/build examples/*/build examples/*/dist",
"rm:node_modules": "rimraf --glob node_modules excalidraw-app/node_modules packages/*/node_modules", "rm:node_modules": "rimraf --glob node_modules excalidraw-app/node_modules packages/*/node_modules",
"clean-install": "yarn rm:node_modules && yarn install" "clean-install": "yarn rm:node_modules && yarn install"

View File

@ -1,6 +1,6 @@
{ {
"name": "@excalidraw/common", "name": "@excalidraw/common",
"version": "0.1.0", "version": "0.18.0",
"type": "module", "type": "module",
"types": "./dist/types/common/src/index.d.ts", "types": "./dist/types/common/src/index.d.ts",
"main": "./dist/prod/index.js", "main": "./dist/prod/index.js",
@ -13,7 +13,10 @@
"default": "./dist/prod/index.js" "default": "./dist/prod/index.js"
}, },
"./*": { "./*": {
"types": "./dist/types/common/src/*.d.ts" "types": "./dist/types/common/src/*.d.ts",
"development": "./dist/dev/index.js",
"production": "./dist/prod/index.js",
"default": "./dist/prod/index.js"
} }
}, },
"files": [ "files": [

View File

@ -36,6 +36,7 @@ export const APP_NAME = "Excalidraw";
// (happens a lot with fast clicks with the text tool) // (happens a lot with fast clicks with the text tool)
export const TEXT_AUTOWRAP_THRESHOLD = 36; // px export const TEXT_AUTOWRAP_THRESHOLD = 36; // px
export const DRAGGING_THRESHOLD = 10; // px export const DRAGGING_THRESHOLD = 10; // px
export const MINIMUM_ARROW_SIZE = 20; // px
export const LINE_CONFIRM_THRESHOLD = 8; // px export const LINE_CONFIRM_THRESHOLD = 8; // px
export const ELEMENT_SHIFT_TRANSLATE_AMOUNT = 5; export const ELEMENT_SHIFT_TRANSLATE_AMOUNT = 5;
export const ELEMENT_TRANSLATE_AMOUNT = 1; export const ELEMENT_TRANSLATE_AMOUNT = 1;

View File

@ -1,6 +1,6 @@
{ {
"name": "@excalidraw/element", "name": "@excalidraw/element",
"version": "0.1.0", "version": "0.18.0",
"type": "module", "type": "module",
"types": "./dist/types/element/src/index.d.ts", "types": "./dist/types/element/src/index.d.ts",
"main": "./dist/prod/index.js", "main": "./dist/prod/index.js",
@ -13,7 +13,10 @@
"default": "./dist/prod/index.js" "default": "./dist/prod/index.js"
}, },
"./*": { "./*": {
"types": "./dist/types/element/src/*.d.ts" "types": "./dist/types/element/src/*.d.ts",
"development": "./dist/dev/index.js",
"production": "./dist/prod/index.js",
"default": "./dist/prod/index.js"
} }
}, },
"files": [ "files": [
@ -52,5 +55,9 @@
"scripts": { "scripts": {
"gen:types": "rimraf types && tsc", "gen:types": "rimraf types && tsc",
"build:esm": "rimraf dist && node ../../scripts/buildBase.js && yarn gen:types" "build:esm": "rimraf dist && node ../../scripts/buildBase.js && yarn gen:types"
},
"dependencies": {
"@excalidraw/common": "0.18.0",
"@excalidraw/math": "0.18.0"
} }
} }

View File

@ -1,6 +1,8 @@
import type { AppState } from "@excalidraw/excalidraw/types";
import { updateBoundElements } from "./binding"; import { updateBoundElements } from "./binding";
import { getCommonBoundingBox } from "./bounds"; import { getCommonBoundingBox } from "./bounds";
import { getMaximumGroups } from "./groups"; import { getSelectedElementsByGroup } from "./groups";
import type { Scene } from "./Scene"; import type { Scene } from "./Scene";
@ -16,11 +18,12 @@ export const alignElements = (
selectedElements: ExcalidrawElement[], selectedElements: ExcalidrawElement[],
alignment: Alignment, alignment: Alignment,
scene: Scene, scene: Scene,
appState: Readonly<AppState>,
): ExcalidrawElement[] => { ): ExcalidrawElement[] => {
const elementsMap = scene.getNonDeletedElementsMap(); const groups: ExcalidrawElement[][] = getSelectedElementsByGroup(
const groups: ExcalidrawElement[][] = getMaximumGroups(
selectedElements, selectedElements,
elementsMap, scene.getNonDeletedElementsMap(),
appState,
); );
const selectionBoundingBox = getCommonBoundingBox(selectedElements); const selectionBoundingBox = getCommonBoundingBox(selectedElements);

View File

@ -2,6 +2,7 @@ import {
arrayToMap, arrayToMap,
arrayToObject, arrayToObject,
assertNever, assertNever,
invariant,
isDevEnv, isDevEnv,
isShallowEqual, isShallowEqual,
isTestEnv, isTestEnv,
@ -548,7 +549,7 @@ export class AppStateDelta implements DeltaContainer<AppState> {
selectedElementIds: addedSelectedElementIds = {}, selectedElementIds: addedSelectedElementIds = {},
selectedGroupIds: addedSelectedGroupIds = {}, selectedGroupIds: addedSelectedGroupIds = {},
selectedLinearElementId, selectedLinearElementId,
editingLinearElementId, selectedLinearElementIsEditing,
...directlyApplicablePartial ...directlyApplicablePartial
} = this.delta.inserted; } = this.delta.inserted;
@ -564,39 +565,46 @@ export class AppStateDelta implements DeltaContainer<AppState> {
removedSelectedGroupIds, removedSelectedGroupIds,
); );
const selectedLinearElement = let selectedLinearElement = appState.selectedLinearElement;
selectedLinearElementId && nextElements.has(selectedLinearElementId)
? new LinearElementEditor( if (selectedLinearElementId === null) {
// Unselect linear element (visible change)
selectedLinearElement = null;
} else if (
selectedLinearElementId &&
nextElements.has(selectedLinearElementId)
) {
selectedLinearElement = new LinearElementEditor(
nextElements.get( nextElements.get(
selectedLinearElementId, selectedLinearElementId,
) as NonDeleted<ExcalidrawLinearElement>, ) as NonDeleted<ExcalidrawLinearElement>,
nextElements, nextElements,
) selectedLinearElementIsEditing === true, // Can be unknown which is defaulted to false
: null; );
}
const editingLinearElement = if (
editingLinearElementId && nextElements.has(editingLinearElementId) // Value being 'null' is equivaluent to unknown in this case because it only gets set
? new LinearElementEditor( // to null when 'selectedLinearElementId' is set to null
nextElements.get( selectedLinearElementIsEditing != null
editingLinearElementId, ) {
) as NonDeleted<ExcalidrawLinearElement>, invariant(
nextElements, selectedLinearElement,
) `selectedLinearElement is null when selectedLinearElementIsEditing is set to ${selectedLinearElementIsEditing}`,
: null; );
selectedLinearElement = {
...selectedLinearElement,
isEditing: selectedLinearElementIsEditing,
};
}
const nextAppState = { const nextAppState = {
...appState, ...appState,
...directlyApplicablePartial, ...directlyApplicablePartial,
selectedElementIds: mergedSelectedElementIds, selectedElementIds: mergedSelectedElementIds,
selectedGroupIds: mergedSelectedGroupIds, selectedGroupIds: mergedSelectedGroupIds,
selectedLinearElement: selectedLinearElement,
typeof selectedLinearElementId !== "undefined"
? selectedLinearElement // element was either inserted or deleted
: appState.selectedLinearElement, // otherwise assign what we had before
editingLinearElement:
typeof editingLinearElementId !== "undefined"
? editingLinearElement // element was either inserted or deleted
: appState.editingLinearElement, // otherwise assign what we had before
}; };
const constainsVisibleChanges = this.filterInvisibleChanges( const constainsVisibleChanges = this.filterInvisibleChanges(
@ -725,8 +733,7 @@ export class AppStateDelta implements DeltaContainer<AppState> {
} }
break; break;
case "selectedLinearElementId": case "selectedLinearElementId": {
case "editingLinearElementId":
const appStateKey = AppStateDelta.convertToAppStateKey(key); const appStateKey = AppStateDelta.convertToAppStateKey(key);
const linearElement = nextAppState[appStateKey]; const linearElement = nextAppState[appStateKey];
@ -746,6 +753,19 @@ export class AppStateDelta implements DeltaContainer<AppState> {
} }
break; break;
}
case "selectedLinearElementIsEditing": {
// Changes in editing state are always visible
const prevIsEditing =
prevAppState.selectedLinearElement?.isEditing ?? false;
const nextIsEditing =
nextAppState.selectedLinearElement?.isEditing ?? false;
if (prevIsEditing !== nextIsEditing) {
visibleDifferenceFlag.value = true;
}
break;
}
case "lockedMultiSelections": { case "lockedMultiSelections": {
const prevLockedUnits = prevAppState[key] || {}; const prevLockedUnits = prevAppState[key] || {};
const nextLockedUnits = nextAppState[key] || {}; const nextLockedUnits = nextAppState[key] || {};
@ -779,16 +799,11 @@ export class AppStateDelta implements DeltaContainer<AppState> {
} }
private static convertToAppStateKey( private static convertToAppStateKey(
key: keyof Pick< key: keyof Pick<ObservedElementsAppState, "selectedLinearElementId">,
ObservedElementsAppState, ): keyof Pick<AppState, "selectedLinearElement"> {
"selectedLinearElementId" | "editingLinearElementId"
>,
): keyof Pick<AppState, "selectedLinearElement" | "editingLinearElement"> {
switch (key) { switch (key) {
case "selectedLinearElementId": case "selectedLinearElementId":
return "selectedLinearElement"; return "selectedLinearElement";
case "editingLinearElementId":
return "editingLinearElement";
} }
} }
@ -856,8 +871,8 @@ export class AppStateDelta implements DeltaContainer<AppState> {
editingGroupId, editingGroupId,
selectedGroupIds, selectedGroupIds,
selectedElementIds, selectedElementIds,
editingLinearElementId,
selectedLinearElementId, selectedLinearElementId,
selectedLinearElementIsEditing,
croppingElementId, croppingElementId,
lockedMultiSelections, lockedMultiSelections,
activeLockedId, activeLockedId,

View File

@ -1,7 +1,9 @@
import type { AppState } from "@excalidraw/excalidraw/types";
import { getCommonBoundingBox } from "./bounds"; import { getCommonBoundingBox } from "./bounds";
import { newElementWith } from "./mutateElement"; import { newElementWith } from "./mutateElement";
import { getMaximumGroups } from "./groups"; import { getSelectedElementsByGroup } from "./groups";
import type { ElementsMap, ExcalidrawElement } from "./types"; import type { ElementsMap, ExcalidrawElement } from "./types";
@ -14,6 +16,7 @@ export const distributeElements = (
selectedElements: ExcalidrawElement[], selectedElements: ExcalidrawElement[],
elementsMap: ElementsMap, elementsMap: ElementsMap,
distribution: Distribution, distribution: Distribution,
appState: Readonly<AppState>,
): ExcalidrawElement[] => { ): ExcalidrawElement[] => {
const [start, mid, end, extent] = const [start, mid, end, extent] =
distribution.axis === "x" distribution.axis === "x"
@ -21,7 +24,11 @@ export const distributeElements = (
: (["minY", "midY", "maxY", "height"] as const); : (["minY", "midY", "maxY", "height"] as const);
const bounds = getCommonBoundingBox(selectedElements); const bounds = getCommonBoundingBox(selectedElements);
const groups = getMaximumGroups(selectedElements, elementsMap) const groups = getSelectedElementsByGroup(
selectedElements,
elementsMap,
appState,
)
.map((group) => [group, getCommonBoundingBox(group)] as const) .map((group) => [group, getCommonBoundingBox(group)] as const)
.sort((a, b) => a[1][mid] - b[1][mid]); .sort((a, b) => a[1][mid] - b[1][mid]);

View File

@ -23,7 +23,7 @@ type IframeDataWithSandbox = MarkRequired<IframeData, "sandbox">;
const embeddedLinkCache = new Map<string, IframeDataWithSandbox>(); const embeddedLinkCache = new Map<string, IframeDataWithSandbox>();
const RE_YOUTUBE = const RE_YOUTUBE =
/^(?:http(?:s)?:\/\/)?(?:www\.)?youtu(?:be\.com|\.be)\/(embed\/|watch\?v=|shorts\/|playlist\?list=|embed\/videoseries\?list=)?([a-zA-Z0-9_-]+)(?:\?t=|&t=|\?start=|&start=)?([a-zA-Z0-9_-]+)?[^\s]*$/; /^(?:http(?:s)?:\/\/)?(?:www\.)?youtu(?:be\.com|\.be)\/(embed\/|watch\?v=|shorts\/|playlist\?list=|embed\/videoseries\?list=)?([a-zA-Z0-9_-]+)/;
const RE_VIMEO = const RE_VIMEO =
/^(?:http(?:s)?:\/\/)?(?:(?:w){3}\.)?(?:player\.)?vimeo\.com\/(?:video\/)?([^?\s]+)(?:\?.*)?$/; /^(?:http(?:s)?:\/\/)?(?:(?:w){3}\.)?(?:player\.)?vimeo\.com\/(?:video\/)?([^?\s]+)(?:\?.*)?$/;
@ -56,6 +56,35 @@ const RE_REDDIT =
const RE_REDDIT_EMBED = const RE_REDDIT_EMBED =
/^<blockquote[\s\S]*?\shref=["'](https?:\/\/(?:www\.)?reddit\.com\/[^"']*)/i; /^<blockquote[\s\S]*?\shref=["'](https?:\/\/(?:www\.)?reddit\.com\/[^"']*)/i;
const parseYouTubeTimestamp = (url: string): number => {
let timeParam: string | null | undefined;
try {
const urlObj = new URL(url.startsWith("http") ? url : `https://${url}`);
timeParam =
urlObj.searchParams.get("t") || urlObj.searchParams.get("start");
} catch (error) {
const timeMatch = url.match(/[?&#](?:t|start)=([^&#\s]+)/);
timeParam = timeMatch?.[1];
}
if (!timeParam) {
return 0;
}
if (/^\d+$/.test(timeParam)) {
return parseInt(timeParam, 10);
}
const timeMatch = timeParam.match(/^(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s)?$/);
if (!timeMatch) {
return 0;
}
const [, hours = "0", minutes = "0", seconds = "0"] = timeMatch;
return parseInt(hours) * 3600 + parseInt(minutes) * 60 + parseInt(seconds);
};
const ALLOWED_DOMAINS = new Set([ const ALLOWED_DOMAINS = new Set([
"youtube.com", "youtube.com",
"youtu.be", "youtu.be",
@ -113,7 +142,8 @@ export const getEmbedLink = (
let aspectRatio = { w: 560, h: 840 }; let aspectRatio = { w: 560, h: 840 };
const ytLink = link.match(RE_YOUTUBE); const ytLink = link.match(RE_YOUTUBE);
if (ytLink?.[2]) { if (ytLink?.[2]) {
const time = ytLink[3] ? `&start=${ytLink[3]}` : ``; const startTime = parseYouTubeTimestamp(originalLink);
const time = startTime > 0 ? `&start=${startTime}` : ``;
const isPortrait = link.includes("shorts"); const isPortrait = link.includes("shorts");
type = "video"; type = "video";
switch (ytLink[1]) { switch (ytLink[1]) {

View File

@ -7,6 +7,8 @@ import type { Mutable } from "@excalidraw/common/utility-types";
import { getBoundTextElement } from "./textElement"; import { getBoundTextElement } from "./textElement";
import { isBoundToContainer } from "./typeChecks";
import { makeNextSelectedElementIds, getSelectedElements } from "./selection"; import { makeNextSelectedElementIds, getSelectedElements } from "./selection";
import type { import type {
@ -402,3 +404,78 @@ export const getNewGroupIdsForDuplication = (
return copy; return copy;
}; };
// given a list of selected elements, return the element grouped by their immediate group selected state
// in the case if only one group is selected and all elements selected are within the group, it will respect group hierarchy in accordance to their nested grouping order
export const getSelectedElementsByGroup = (
selectedElements: ExcalidrawElement[],
elementsMap: ElementsMap,
appState: Readonly<AppState>,
): ExcalidrawElement[][] => {
const selectedGroupIds = getSelectedGroupIds(appState);
const unboundElements = selectedElements.filter(
(element) => !isBoundToContainer(element),
);
const groups: Map<string, ExcalidrawElement[]> = new Map();
const elements: Map<string, ExcalidrawElement[]> = new Map();
// helper function to add an element to the elements map
const addToElementsMap = (element: ExcalidrawElement) => {
// elements
const currentElementMembers = elements.get(element.id) || [];
const boundTextElement = getBoundTextElement(element, elementsMap);
if (boundTextElement) {
currentElementMembers.push(boundTextElement);
}
elements.set(element.id, [...currentElementMembers, element]);
};
// helper function to add an element to the groups map
const addToGroupsMap = (element: ExcalidrawElement, groupId: string) => {
// groups
const currentGroupMembers = groups.get(groupId) || [];
const boundTextElement = getBoundTextElement(element, elementsMap);
if (boundTextElement) {
currentGroupMembers.push(boundTextElement);
}
groups.set(groupId, [...currentGroupMembers, element]);
};
// helper function to handle the case where a single group is selected
// and all elements selected are within the group, it will respect group hierarchy in accordance to
// their nested grouping order
const handleSingleSelectedGroupCase = (
element: ExcalidrawElement,
selectedGroupId: GroupId,
) => {
const indexOfSelectedGroupId = element.groupIds.indexOf(selectedGroupId, 0);
const nestedGroupCount = element.groupIds.slice(
0,
indexOfSelectedGroupId,
).length;
return nestedGroupCount > 0
? addToGroupsMap(element, element.groupIds[indexOfSelectedGroupId - 1])
: addToElementsMap(element);
};
const isAllInSameGroup = selectedElements.every((element) =>
isSelectedViaGroup(appState, element),
);
unboundElements.forEach((element) => {
const selectedGroupId = getSelectedGroupIdForElement(
element,
appState.selectedGroupIds,
);
if (!selectedGroupId) {
addToElementsMap(element);
} else if (selectedGroupIds.length === 1 && isAllInSameGroup) {
handleSingleSelectedGroupCase(element, selectedGroupId);
} else {
addToGroupsMap(element, selectedGroupId);
}
});
return Array.from(groups.values()).concat(Array.from(elements.values()));
};

View File

@ -153,10 +153,12 @@ export class LinearElementEditor {
public readonly segmentMidPointHoveredCoords: GlobalPoint | null; public readonly segmentMidPointHoveredCoords: GlobalPoint | null;
public readonly elbowed: boolean; public readonly elbowed: boolean;
public readonly customLineAngle: number | null; public readonly customLineAngle: number | null;
public readonly isEditing: boolean;
constructor( constructor(
element: NonDeleted<ExcalidrawLinearElement>, element: NonDeleted<ExcalidrawLinearElement>,
elementsMap: ElementsMap, elementsMap: ElementsMap,
isEditing: boolean = false,
) { ) {
this.elementId = element.id as string & { this.elementId = element.id as string & {
_brand: "excalidrawLinearElementId"; _brand: "excalidrawLinearElementId";
@ -191,6 +193,7 @@ export class LinearElementEditor {
this.segmentMidPointHoveredCoords = null; this.segmentMidPointHoveredCoords = null;
this.elbowed = isElbowArrow(element) && element.elbowed; this.elbowed = isElbowArrow(element) && element.elbowed;
this.customLineAngle = null; this.customLineAngle = null;
this.isEditing = isEditing;
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@ -198,6 +201,7 @@ export class LinearElementEditor {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
static POINT_HANDLE_SIZE = 10; static POINT_HANDLE_SIZE = 10;
/** /**
* @param id the `elementId` from the instance of this class (so that we can * @param id the `elementId` from the instance of this class (so that we can
* statically guarantee this method returns an ExcalidrawLinearElement) * statically guarantee this method returns an ExcalidrawLinearElement)
@ -219,11 +223,14 @@ export class LinearElementEditor {
setState: React.Component<any, AppState>["setState"], setState: React.Component<any, AppState>["setState"],
elementsMap: NonDeletedSceneElementsMap, elementsMap: NonDeletedSceneElementsMap,
) { ) {
if (!appState.editingLinearElement || !appState.selectionElement) { if (
!appState.selectedLinearElement?.isEditing ||
!appState.selectionElement
) {
return false; return false;
} }
const { editingLinearElement } = appState; const { selectedLinearElement } = appState;
const { selectedPointsIndices, elementId } = editingLinearElement; const { selectedPointsIndices, elementId } = selectedLinearElement;
const element = LinearElementEditor.getElement(elementId, elementsMap); const element = LinearElementEditor.getElement(elementId, elementsMap);
if (!element) { if (!element) {
@ -264,8 +271,8 @@ export class LinearElementEditor {
}); });
setState({ setState({
editingLinearElement: { selectedLinearElement: {
...editingLinearElement, ...selectedLinearElement,
selectedPointsIndices: nextSelectedPoints.length selectedPointsIndices: nextSelectedPoints.length
? nextSelectedPoints ? nextSelectedPoints
: null, : null,
@ -597,9 +604,6 @@ export class LinearElementEditor {
return { return {
...app.state, ...app.state,
editingLinearElement: app.state.editingLinearElement
? newLinearElementEditor
: null,
selectedLinearElement: newLinearElementEditor, selectedLinearElement: newLinearElementEditor,
suggestedBindings, suggestedBindings,
snapLines: _snapLines, snapLines: _snapLines,
@ -737,7 +741,7 @@ export class LinearElementEditor {
// Since its not needed outside editor unless 2 pointer lines or bound text // Since its not needed outside editor unless 2 pointer lines or bound text
if ( if (
!isElbowArrow(element) && !isElbowArrow(element) &&
!appState.editingLinearElement && !appState.selectedLinearElement?.isEditing &&
element.points.length > 2 && element.points.length > 2 &&
!boundText !boundText
) { ) {
@ -803,7 +807,7 @@ export class LinearElementEditor {
); );
if ( if (
points.length >= 3 && points.length >= 3 &&
!appState.editingLinearElement && !appState.selectedLinearElement?.isEditing &&
!isElbowArrow(element) !isElbowArrow(element)
) { ) {
return null; return null;
@ -1000,7 +1004,7 @@ export class LinearElementEditor {
segmentMidpoint, segmentMidpoint,
elementsMap, elementsMap,
); );
} else if (event.altKey && appState.editingLinearElement) { } else if (event.altKey && appState.selectedLinearElement?.isEditing) {
if (linearElementEditor.lastUncommittedPoint == null) { if (linearElementEditor.lastUncommittedPoint == null) {
scene.mutateElement(element, { scene.mutateElement(element, {
points: [ points: [
@ -1141,19 +1145,19 @@ export class LinearElementEditor {
scenePointerY: number, scenePointerY: number,
app: AppClassProperties, app: AppClassProperties,
): { ): {
linearElementEditor: LinearElementEditor; editingLinearElement: LinearElementEditor;
snapLines: readonly SnapLine[]; snapLines: readonly SnapLine[];
} | null { } | null {
const appState = app.state; const appState = app.state;
if (!appState.editingLinearElement) { if (!appState.selectedLinearElement?.isEditing) {
return null; return null;
} }
const { elementId, lastUncommittedPoint } = appState.editingLinearElement; const { elementId, lastUncommittedPoint } = appState.selectedLinearElement;
const elementsMap = app.scene.getNonDeletedElementsMap(); const elementsMap = app.scene.getNonDeletedElementsMap();
const element = LinearElementEditor.getElement(elementId, elementsMap); const element = LinearElementEditor.getElement(elementId, elementsMap);
if (!element) { if (!element) {
return { return {
linearElementEditor: appState.editingLinearElement, editingLinearElement: appState.selectedLinearElement,
snapLines: appState.snapLines, snapLines: appState.snapLines,
}; };
} }
@ -1166,8 +1170,8 @@ export class LinearElementEditor {
LinearElementEditor.deletePoints(element, app, [points.length - 1]); LinearElementEditor.deletePoints(element, app, [points.length - 1]);
} }
return { return {
linearElementEditor: { editingLinearElement: {
...appState.editingLinearElement, ...appState.selectedLinearElement,
lastUncommittedPoint: null, lastUncommittedPoint: null,
isDragging: false, isDragging: false,
pointerOffset: { x: 0, y: 0 }, pointerOffset: { x: 0, y: 0 },
@ -1298,9 +1302,9 @@ export class LinearElementEditor {
); );
} else { } else {
const originalPointerX = const originalPointerX =
scenePointerX - appState.editingLinearElement.pointerOffset.x; scenePointerX - appState.selectedLinearElement.pointerOffset.x;
const originalPointerY = const originalPointerY =
scenePointerY - appState.editingLinearElement.pointerOffset.y; scenePointerY - appState.selectedLinearElement.pointerOffset.y;
const { snapOffset, snapLines: snappingLines } = snapLinearElementPoint( const { snapOffset, snapLines: snappingLines } = snapLinearElementPoint(
app.scene.getNonDeletedElements(), app.scene.getNonDeletedElements(),
@ -1347,8 +1351,8 @@ export class LinearElementEditor {
} }
return { return {
linearElementEditor: { editingLinearElement: {
...appState.editingLinearElement, ...appState.selectedLinearElement,
lastUncommittedPoint: element.points[element.points.length - 1], lastUncommittedPoint: element.points[element.points.length - 1],
}, },
snapLines, snapLines,
@ -1509,12 +1513,12 @@ export class LinearElementEditor {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
static duplicateSelectedPoints(appState: AppState, scene: Scene): AppState { static duplicateSelectedPoints(appState: AppState, scene: Scene): AppState {
invariant( invariant(
appState.editingLinearElement, appState.selectedLinearElement?.isEditing,
"Not currently editing a linear element", "Not currently editing a linear element",
); );
const elementsMap = scene.getNonDeletedElementsMap(); const elementsMap = scene.getNonDeletedElementsMap();
const { selectedPointsIndices, elementId } = appState.editingLinearElement; const { selectedPointsIndices, elementId } = appState.selectedLinearElement;
const element = LinearElementEditor.getElement(elementId, elementsMap); const element = LinearElementEditor.getElement(elementId, elementsMap);
invariant( invariant(
@ -1576,8 +1580,8 @@ export class LinearElementEditor {
return { return {
...appState, ...appState,
editingLinearElement: { selectedLinearElement: {
...appState.editingLinearElement, ...appState.selectedLinearElement,
selectedPointsIndices: nextSelectedIndices, selectedPointsIndices: nextSelectedIndices,
}, },
}; };
@ -1589,7 +1593,8 @@ export class LinearElementEditor {
pointIndices: readonly number[], pointIndices: readonly number[],
) { ) {
const isUncommittedPoint = const isUncommittedPoint =
app.state.editingLinearElement?.lastUncommittedPoint === app.state.selectedLinearElement?.isEditing &&
app.state.selectedLinearElement?.lastUncommittedPoint ===
element.points[element.points.length - 1]; element.points[element.points.length - 1];
const nextPoints = element.points.filter((_, idx) => { const nextPoints = element.points.filter((_, idx) => {
@ -1763,7 +1768,7 @@ export class LinearElementEditor {
pointFrom(pointerCoords.x, pointerCoords.y), pointFrom(pointerCoords.x, pointerCoords.y),
); );
if ( if (
!appState.editingLinearElement && !appState.selectedLinearElement?.isEditing &&
dist < DRAGGING_THRESHOLD / appState.zoom.value dist < DRAGGING_THRESHOLD / appState.zoom.value
) { ) {
return false; return false;

View File

@ -106,6 +106,11 @@ const getCanvasPadding = (element: ExcalidrawElement) => {
return element.strokeWidth * 12; return element.strokeWidth * 12;
case "text": case "text":
return element.fontSize / 2; return element.fontSize / 2;
case "arrow":
if (element.endArrowhead || element.endArrowhead) {
return 40;
}
return 20;
default: default:
return 20; return 20;
} }

View File

@ -14,11 +14,7 @@ import {
getDraggedElementsBounds, getDraggedElementsBounds,
getElementAbsoluteCoords, getElementAbsoluteCoords,
} from "@excalidraw/element"; } from "@excalidraw/element";
import { import { isBoundToContainer, isElbowArrow } from "@excalidraw/element";
isBoundToContainer,
isFrameLikeElement,
isElbowArrow,
} from "@excalidraw/element";
import { getMaximumGroups } from "@excalidraw/element"; import { getMaximumGroups } from "@excalidraw/element";
@ -379,20 +375,13 @@ const getReferenceElements = (
selectedElements: NonDeletedExcalidrawElement[], selectedElements: NonDeletedExcalidrawElement[],
appState: AppState, appState: AppState,
elementsMap: ElementsMap, elementsMap: ElementsMap,
) => { ) =>
const selectedFrames = selectedElements getVisibleAndNonSelectedElements(
.filter((element) => isFrameLikeElement(element))
.map((frame) => frame.id);
return getVisibleAndNonSelectedElements(
elements, elements,
selectedElements, selectedElements,
appState, appState,
elementsMap, elementsMap,
).filter(
(element) => !(element.frameId && selectedFrames.includes(element.frameId)),
); );
};
export const getVisibleGaps = ( export const getVisibleGaps = (
elements: readonly NonDeletedExcalidrawElement[], elements: readonly NonDeletedExcalidrawElement[],

View File

@ -27,6 +27,8 @@ import {
isImageElement, isImageElement,
} from "./index"; } from "./index";
import type { ApplyToOptions } from "./delta";
import type { import type {
ExcalidrawElement, ExcalidrawElement,
OrderedExcalidrawElement, OrderedExcalidrawElement,
@ -570,9 +572,15 @@ export class StoreDelta {
delta: StoreDelta, delta: StoreDelta,
elements: SceneElementsMap, elements: SceneElementsMap,
appState: AppState, appState: AppState,
options: ApplyToOptions = {
excludedProperties: new Set(),
},
): [SceneElementsMap, AppState, boolean] { ): [SceneElementsMap, AppState, boolean] {
const [nextElements, elementsContainVisibleChange] = const [nextElements, elementsContainVisibleChange] = delta.elements.applyTo(
delta.elements.applyTo(elements); elements,
StoreSnapshot.empty().elements,
options,
);
const [nextAppState, appStateContainsVisibleChange] = const [nextAppState, appStateContainsVisibleChange] =
delta.appState.applyTo(appState, nextElements); delta.appState.applyTo(appState, nextElements);
@ -970,8 +978,8 @@ const getDefaultObservedAppState = (): ObservedAppState => {
viewBackgroundColor: COLOR_PALETTE.white, viewBackgroundColor: COLOR_PALETTE.white,
selectedElementIds: {}, selectedElementIds: {},
selectedGroupIds: {}, selectedGroupIds: {},
editingLinearElementId: null,
selectedLinearElementId: null, selectedLinearElementId: null,
selectedLinearElementIsEditing: null,
croppingElementId: null, croppingElementId: null,
activeLockedId: null, activeLockedId: null,
lockedMultiSelections: {}, lockedMultiSelections: {},
@ -990,14 +998,14 @@ export const getObservedAppState = (
croppingElementId: appState.croppingElementId, croppingElementId: appState.croppingElementId,
activeLockedId: appState.activeLockedId, activeLockedId: appState.activeLockedId,
lockedMultiSelections: appState.lockedMultiSelections, lockedMultiSelections: appState.lockedMultiSelections,
editingLinearElementId:
(appState as AppState).editingLinearElement?.elementId ?? // prefer app state, as it's likely newer
(appState as ObservedAppState).editingLinearElementId ?? // fallback to observed app state, as it's likely older coming from a previous snapshot
null,
selectedLinearElementId: selectedLinearElementId:
(appState as AppState).selectedLinearElement?.elementId ?? (appState as AppState).selectedLinearElement?.elementId ??
(appState as ObservedAppState).selectedLinearElementId ?? (appState as ObservedAppState).selectedLinearElementId ??
null, null,
selectedLinearElementIsEditing:
(appState as AppState).selectedLinearElement?.isEditing ??
(appState as ObservedAppState).selectedLinearElementIsEditing ??
null,
}; };
Reflect.defineProperty(observedAppState, hiddenObservedAppStateProp, { Reflect.defineProperty(observedAppState, hiddenObservedAppStateProp, {

View File

@ -330,7 +330,7 @@ export const shouldShowBoundingBox = (
elements: readonly NonDeletedExcalidrawElement[], elements: readonly NonDeletedExcalidrawElement[],
appState: InteractiveCanvasAppState, appState: InteractiveCanvasAppState,
) => { ) => {
if (appState.editingLinearElement) { if (appState.selectedLinearElement?.isEditing) {
return false; return false;
} }
if (elements.length > 1) { if (elements.length > 1) {

View File

@ -589,4 +589,424 @@ describe("aligning", () => {
expect(API.getSelectedElements()[2].x).toEqual(250); expect(API.getSelectedElements()[2].x).toEqual(250);
expect(API.getSelectedElements()[3].x).toEqual(150); expect(API.getSelectedElements()[3].x).toEqual(150);
}); });
const createGroupAndSelectInEditGroupMode = () => {
UI.clickTool("rectangle");
mouse.down();
mouse.up(100, 100);
UI.clickTool("rectangle");
mouse.down(0, 0);
mouse.up(100, 100);
// select the first element.
// The second rectangle is already reselected because it was the last element created
mouse.reset();
Keyboard.withModifierKeys({ shift: true }, () => {
mouse.moveTo(10, 0);
mouse.click();
});
API.executeAction(actionGroup);
mouse.reset();
mouse.moveTo(10, 0);
mouse.doubleClick();
Keyboard.withModifierKeys({ shift: true }, () => {
mouse.click();
mouse.moveTo(100, 100);
mouse.click();
});
};
it("aligns elements within a group while in group edit mode correctly to the top", () => {
createGroupAndSelectInEditGroupMode();
expect(API.getSelectedElements()[0].y).toEqual(0);
expect(API.getSelectedElements()[1].y).toEqual(100);
API.executeAction(actionAlignTop);
expect(API.getSelectedElements()[0].y).toEqual(0);
expect(API.getSelectedElements()[1].y).toEqual(0);
});
it("aligns elements within a group while in group edit mode correctly to the bottom", () => {
createGroupAndSelectInEditGroupMode();
expect(API.getSelectedElements()[0].y).toEqual(0);
expect(API.getSelectedElements()[1].y).toEqual(100);
API.executeAction(actionAlignBottom);
expect(API.getSelectedElements()[0].y).toEqual(100);
expect(API.getSelectedElements()[1].y).toEqual(100);
});
it("aligns elements within a group while in group edit mode correctly to the left", () => {
createGroupAndSelectInEditGroupMode();
expect(API.getSelectedElements()[0].x).toEqual(0);
expect(API.getSelectedElements()[1].x).toEqual(100);
API.executeAction(actionAlignLeft);
expect(API.getSelectedElements()[0].x).toEqual(0);
expect(API.getSelectedElements()[1].x).toEqual(0);
});
it("aligns elements within a group while in group edit mode correctly to the right", () => {
createGroupAndSelectInEditGroupMode();
expect(API.getSelectedElements()[0].x).toEqual(0);
expect(API.getSelectedElements()[1].x).toEqual(100);
API.executeAction(actionAlignRight);
expect(API.getSelectedElements()[0].x).toEqual(100);
expect(API.getSelectedElements()[1].x).toEqual(100);
});
it("aligns elements within a group while in group edit mode correctly to the vertical center", () => {
createGroupAndSelectInEditGroupMode();
expect(API.getSelectedElements()[0].y).toEqual(0);
expect(API.getSelectedElements()[1].y).toEqual(100);
API.executeAction(actionAlignVerticallyCentered);
expect(API.getSelectedElements()[0].y).toEqual(50);
expect(API.getSelectedElements()[1].y).toEqual(50);
});
it("aligns elements within a group while in group edit mode correctly to the horizontal center", () => {
createGroupAndSelectInEditGroupMode();
expect(API.getSelectedElements()[0].x).toEqual(0);
expect(API.getSelectedElements()[1].x).toEqual(100);
API.executeAction(actionAlignHorizontallyCentered);
expect(API.getSelectedElements()[0].x).toEqual(50);
expect(API.getSelectedElements()[1].x).toEqual(50);
});
const createNestedGroupAndSelectInEditGroupMode = () => {
UI.clickTool("rectangle");
mouse.down();
mouse.up(100, 100);
UI.clickTool("rectangle");
mouse.down(0, 0);
mouse.up(100, 100);
// Select the first element.
// The second rectangle is already reselected because it was the last element created
mouse.reset();
Keyboard.withModifierKeys({ shift: true }, () => {
mouse.moveTo(10, 0);
mouse.click();
});
API.executeAction(actionGroup);
mouse.reset();
mouse.moveTo(200, 200);
// create third element
UI.clickTool("rectangle");
mouse.down(0, 0);
mouse.up(100, 100);
// third element is already selected, select the initial group and group together
mouse.reset();
Keyboard.withModifierKeys({ shift: true }, () => {
mouse.moveTo(10, 0);
mouse.click();
});
API.executeAction(actionGroup);
// double click to enter edit mode
mouse.doubleClick();
// select nested group and other element within the group
Keyboard.withModifierKeys({ shift: true }, () => {
mouse.moveTo(200, 200);
mouse.click();
});
};
it("aligns element and nested group while in group edit mode correctly to the top", () => {
createNestedGroupAndSelectInEditGroupMode();
expect(API.getSelectedElements()[0].y).toEqual(0);
expect(API.getSelectedElements()[1].y).toEqual(100);
expect(API.getSelectedElements()[2].y).toEqual(200);
API.executeAction(actionAlignTop);
expect(API.getSelectedElements()[0].y).toEqual(0);
expect(API.getSelectedElements()[1].y).toEqual(100);
expect(API.getSelectedElements()[2].y).toEqual(0);
});
it("aligns element and nested group while in group edit mode correctly to the bottom", () => {
createNestedGroupAndSelectInEditGroupMode();
expect(API.getSelectedElements()[0].y).toEqual(0);
expect(API.getSelectedElements()[1].y).toEqual(100);
expect(API.getSelectedElements()[2].y).toEqual(200);
API.executeAction(actionAlignBottom);
expect(API.getSelectedElements()[0].y).toEqual(100);
expect(API.getSelectedElements()[1].y).toEqual(200);
expect(API.getSelectedElements()[2].y).toEqual(200);
});
it("aligns element and nested group while in group edit mode correctly to the left", () => {
createNestedGroupAndSelectInEditGroupMode();
expect(API.getSelectedElements()[0].x).toEqual(0);
expect(API.getSelectedElements()[1].x).toEqual(100);
expect(API.getSelectedElements()[2].x).toEqual(200);
API.executeAction(actionAlignLeft);
expect(API.getSelectedElements()[0].x).toEqual(0);
expect(API.getSelectedElements()[1].x).toEqual(100);
expect(API.getSelectedElements()[2].x).toEqual(0);
});
it("aligns element and nested group while in group edit mode correctly to the right", () => {
createNestedGroupAndSelectInEditGroupMode();
expect(API.getSelectedElements()[0].x).toEqual(0);
expect(API.getSelectedElements()[1].x).toEqual(100);
expect(API.getSelectedElements()[2].x).toEqual(200);
API.executeAction(actionAlignRight);
expect(API.getSelectedElements()[0].x).toEqual(100);
expect(API.getSelectedElements()[1].x).toEqual(200);
expect(API.getSelectedElements()[2].x).toEqual(200);
});
it("aligns element and nested group while in group edit mode correctly to the vertical center", () => {
createNestedGroupAndSelectInEditGroupMode();
expect(API.getSelectedElements()[0].y).toEqual(0);
expect(API.getSelectedElements()[1].y).toEqual(100);
expect(API.getSelectedElements()[2].y).toEqual(200);
API.executeAction(actionAlignVerticallyCentered);
expect(API.getSelectedElements()[0].y).toEqual(50);
expect(API.getSelectedElements()[1].y).toEqual(150);
expect(API.getSelectedElements()[2].y).toEqual(100);
});
it("aligns elements and nested group within a group while in group edit mode correctly to the horizontal center", () => {
createNestedGroupAndSelectInEditGroupMode();
expect(API.getSelectedElements()[0].x).toEqual(0);
expect(API.getSelectedElements()[1].x).toEqual(100);
expect(API.getSelectedElements()[2].x).toEqual(200);
API.executeAction(actionAlignHorizontallyCentered);
expect(API.getSelectedElements()[0].x).toEqual(50);
expect(API.getSelectedElements()[1].x).toEqual(150);
expect(API.getSelectedElements()[2].x).toEqual(100);
});
const createAndSelectSingleGroup = () => {
UI.clickTool("rectangle");
mouse.down();
mouse.up(100, 100);
UI.clickTool("rectangle");
mouse.down(0, 0);
mouse.up(100, 100);
// Select the first element.
// The second rectangle is already reselected because it was the last element created
mouse.reset();
Keyboard.withModifierKeys({ shift: true }, () => {
mouse.moveTo(10, 0);
mouse.click();
});
API.executeAction(actionGroup);
};
it("aligns elements within a single-selected group correctly to the top", () => {
createAndSelectSingleGroup();
expect(API.getSelectedElements()[0].y).toEqual(0);
expect(API.getSelectedElements()[1].y).toEqual(100);
API.executeAction(actionAlignTop);
expect(API.getSelectedElements()[0].y).toEqual(0);
expect(API.getSelectedElements()[1].y).toEqual(0);
});
it("aligns elements within a single-selected group correctly to the bottom", () => {
createAndSelectSingleGroup();
expect(API.getSelectedElements()[0].y).toEqual(0);
expect(API.getSelectedElements()[1].y).toEqual(100);
API.executeAction(actionAlignBottom);
expect(API.getSelectedElements()[0].y).toEqual(100);
expect(API.getSelectedElements()[1].y).toEqual(100);
});
it("aligns elements within a single-selected group correctly to the left", () => {
createAndSelectSingleGroup();
expect(API.getSelectedElements()[0].x).toEqual(0);
expect(API.getSelectedElements()[1].x).toEqual(100);
API.executeAction(actionAlignLeft);
expect(API.getSelectedElements()[0].x).toEqual(0);
expect(API.getSelectedElements()[1].x).toEqual(0);
});
it("aligns elements within a single-selected group correctly to the right", () => {
createAndSelectSingleGroup();
expect(API.getSelectedElements()[0].x).toEqual(0);
expect(API.getSelectedElements()[1].x).toEqual(100);
API.executeAction(actionAlignRight);
expect(API.getSelectedElements()[0].x).toEqual(100);
expect(API.getSelectedElements()[1].x).toEqual(100);
});
it("aligns elements within a single-selected group correctly to the vertical center", () => {
createAndSelectSingleGroup();
expect(API.getSelectedElements()[0].y).toEqual(0);
expect(API.getSelectedElements()[1].y).toEqual(100);
API.executeAction(actionAlignVerticallyCentered);
expect(API.getSelectedElements()[0].y).toEqual(50);
expect(API.getSelectedElements()[1].y).toEqual(50);
});
it("aligns elements within a single-selected group correctly to the horizontal center", () => {
createAndSelectSingleGroup();
expect(API.getSelectedElements()[0].x).toEqual(0);
expect(API.getSelectedElements()[1].x).toEqual(100);
API.executeAction(actionAlignHorizontallyCentered);
expect(API.getSelectedElements()[0].x).toEqual(50);
expect(API.getSelectedElements()[1].x).toEqual(50);
});
const createAndSelectSingleGroupWithNestedGroup = () => {
UI.clickTool("rectangle");
mouse.down();
mouse.up(100, 100);
UI.clickTool("rectangle");
mouse.down(0, 0);
mouse.up(100, 100);
// Select the first element.
// The second rectangle is already reselected because it was the last element created
mouse.reset();
Keyboard.withModifierKeys({ shift: true }, () => {
mouse.moveTo(10, 0);
mouse.click();
});
API.executeAction(actionGroup);
mouse.reset();
UI.clickTool("rectangle");
mouse.down(200, 200);
mouse.up(100, 100);
// Add group to current selection
mouse.restorePosition(10, 0);
Keyboard.withModifierKeys({ shift: true }, () => {
mouse.click();
});
// Create the nested group
API.executeAction(actionGroup);
};
it("aligns elements within a single-selected group containing a nested group correctly to the top", () => {
createAndSelectSingleGroupWithNestedGroup();
expect(API.getSelectedElements()[0].y).toEqual(0);
expect(API.getSelectedElements()[1].y).toEqual(100);
expect(API.getSelectedElements()[2].y).toEqual(200);
API.executeAction(actionAlignTop);
expect(API.getSelectedElements()[0].y).toEqual(0);
expect(API.getSelectedElements()[1].y).toEqual(100);
expect(API.getSelectedElements()[2].y).toEqual(0);
});
it("aligns elements within a single-selected group containing a nested group correctly to the bottom", () => {
createAndSelectSingleGroupWithNestedGroup();
expect(API.getSelectedElements()[0].y).toEqual(0);
expect(API.getSelectedElements()[1].y).toEqual(100);
expect(API.getSelectedElements()[2].y).toEqual(200);
API.executeAction(actionAlignBottom);
expect(API.getSelectedElements()[0].y).toEqual(100);
expect(API.getSelectedElements()[1].y).toEqual(200);
expect(API.getSelectedElements()[2].y).toEqual(200);
});
it("aligns elements within a single-selected group containing a nested group correctly to the left", () => {
createAndSelectSingleGroupWithNestedGroup();
expect(API.getSelectedElements()[0].x).toEqual(0);
expect(API.getSelectedElements()[1].x).toEqual(100);
expect(API.getSelectedElements()[2].x).toEqual(200);
API.executeAction(actionAlignLeft);
expect(API.getSelectedElements()[0].x).toEqual(0);
expect(API.getSelectedElements()[1].x).toEqual(100);
expect(API.getSelectedElements()[2].x).toEqual(0);
});
it("aligns elements within a single-selected group containing a nested group correctly to the right", () => {
createAndSelectSingleGroupWithNestedGroup();
expect(API.getSelectedElements()[0].x).toEqual(0);
expect(API.getSelectedElements()[1].x).toEqual(100);
expect(API.getSelectedElements()[2].x).toEqual(200);
API.executeAction(actionAlignRight);
expect(API.getSelectedElements()[0].x).toEqual(100);
expect(API.getSelectedElements()[1].x).toEqual(200);
expect(API.getSelectedElements()[2].x).toEqual(200);
});
it("aligns elements within a single-selected group containing a nested group correctly to the vertical center", () => {
createAndSelectSingleGroupWithNestedGroup();
expect(API.getSelectedElements()[0].y).toEqual(0);
expect(API.getSelectedElements()[1].y).toEqual(100);
expect(API.getSelectedElements()[2].y).toEqual(200);
API.executeAction(actionAlignVerticallyCentered);
expect(API.getSelectedElements()[0].y).toEqual(50);
expect(API.getSelectedElements()[1].y).toEqual(150);
expect(API.getSelectedElements()[2].y).toEqual(100);
});
it("aligns elements within a single-selected group containing a nested group correctly to the horizontal center", () => {
createAndSelectSingleGroupWithNestedGroup();
expect(API.getSelectedElements()[0].x).toEqual(0);
expect(API.getSelectedElements()[1].x).toEqual(100);
expect(API.getSelectedElements()[2].x).toEqual(200);
API.executeAction(actionAlignHorizontallyCentered);
expect(API.getSelectedElements()[0].x).toEqual(50);
expect(API.getSelectedElements()[1].x).toEqual(150);
expect(API.getSelectedElements()[2].x).toEqual(100);
});
}); });

View File

@ -155,10 +155,10 @@ describe("element binding", () => {
// NOTE this mouse down/up + await needs to be done in order to repro // NOTE this mouse down/up + await needs to be done in order to repro
// the issue, due to https://github.com/excalidraw/excalidraw/blob/46bff3daceb602accf60c40a84610797260fca94/src/components/App.tsx#L740 // the issue, due to https://github.com/excalidraw/excalidraw/blob/46bff3daceb602accf60c40a84610797260fca94/src/components/App.tsx#L740
mouse.reset(); mouse.reset();
expect(h.state.editingLinearElement).not.toBe(null); expect(h.state.selectedLinearElement?.isEditing).toBe(true);
mouse.down(0, 0); mouse.down(0, 0);
await new Promise((r) => setTimeout(r, 100)); await new Promise((r) => setTimeout(r, 100));
expect(h.state.editingLinearElement).toBe(null); expect(h.state.selectedLinearElement?.isEditing).toBe(false);
expect(API.getSelectedElement().type).toBe("rectangle"); expect(API.getSelectedElement().type).toBe("rectangle");
mouse.up(); mouse.up();
expect(API.getSelectedElement().type).toBe("rectangle"); expect(API.getSelectedElement().type).toBe("rectangle");

View File

@ -16,6 +16,7 @@ describe("AppStateDelta", () => {
editingGroupId: null, editingGroupId: null,
croppingElementId: null, croppingElementId: null,
editingLinearElementId: null, editingLinearElementId: null,
selectedLinearElementIsEditing: null,
lockedMultiSelections: {}, lockedMultiSelections: {},
activeLockedId: null, activeLockedId: null,
}; };
@ -58,6 +59,7 @@ describe("AppStateDelta", () => {
editingGroupId: null, editingGroupId: null,
croppingElementId: null, croppingElementId: null,
selectedLinearElementId: null, selectedLinearElementId: null,
selectedLinearElementIsEditing: null,
editingLinearElementId: null, editingLinearElementId: null,
activeLockedId: null, activeLockedId: null,
lockedMultiSelections: {}, lockedMultiSelections: {},
@ -105,6 +107,7 @@ describe("AppStateDelta", () => {
editingGroupId: null, editingGroupId: null,
croppingElementId: null, croppingElementId: null,
selectedLinearElementId: null, selectedLinearElementId: null,
selectedLinearElementIsEditing: null,
editingLinearElementId: null, editingLinearElementId: null,
activeLockedId: null, activeLockedId: null,
lockedMultiSelections: {}, lockedMultiSelections: {},

View File

@ -0,0 +1,128 @@
import {
distributeHorizontally,
distributeVertically,
} from "@excalidraw/excalidraw/actions";
import { defaultLang, setLanguage } from "@excalidraw/excalidraw/i18n";
import { Excalidraw } from "@excalidraw/excalidraw";
import { API } from "@excalidraw/excalidraw/tests/helpers/api";
import { UI, Pointer, Keyboard } from "@excalidraw/excalidraw/tests/helpers/ui";
import {
act,
unmountComponent,
render,
} from "@excalidraw/excalidraw/tests/test-utils";
const mouse = new Pointer("mouse");
// Scenario: three rectangles that will be distributed with gaps
const createAndSelectThreeRectanglesWithGap = () => {
UI.clickTool("rectangle");
mouse.down();
mouse.up(100, 100);
mouse.reset();
UI.clickTool("rectangle");
mouse.down(10, 10);
mouse.up(100, 100);
mouse.reset();
UI.clickTool("rectangle");
mouse.down(300, 300);
mouse.up(100, 100);
mouse.reset();
// Last rectangle is selected by default
Keyboard.withModifierKeys({ shift: true }, () => {
mouse.click(0, 10);
mouse.click(10, 0);
});
};
// Scenario: three rectangles that will be distributed by their centers
const createAndSelectThreeRectanglesWithoutGap = () => {
UI.clickTool("rectangle");
mouse.down();
mouse.up(100, 100);
mouse.reset();
UI.clickTool("rectangle");
mouse.down(10, 10);
mouse.up(200, 200);
mouse.reset();
UI.clickTool("rectangle");
mouse.down(200, 200);
mouse.up(100, 100);
mouse.reset();
// Last rectangle is selected by default
Keyboard.withModifierKeys({ shift: true }, () => {
mouse.click(0, 10);
mouse.click(10, 0);
});
};
describe("distributing", () => {
beforeEach(async () => {
unmountComponent();
mouse.reset();
await act(() => {
return setLanguage(defaultLang);
});
await render(<Excalidraw handleKeyboardGlobally={true} />);
});
it("should distribute selected elements horizontally", async () => {
createAndSelectThreeRectanglesWithGap();
expect(API.getSelectedElements()[0].x).toEqual(0);
expect(API.getSelectedElements()[1].x).toEqual(10);
expect(API.getSelectedElements()[2].x).toEqual(300);
API.executeAction(distributeHorizontally);
expect(API.getSelectedElements()[0].x).toEqual(0);
expect(API.getSelectedElements()[1].x).toEqual(150);
expect(API.getSelectedElements()[2].x).toEqual(300);
});
it("should distribute selected elements vertically", async () => {
createAndSelectThreeRectanglesWithGap();
expect(API.getSelectedElements()[0].y).toEqual(0);
expect(API.getSelectedElements()[1].y).toEqual(10);
expect(API.getSelectedElements()[2].y).toEqual(300);
API.executeAction(distributeVertically);
expect(API.getSelectedElements()[0].y).toEqual(0);
expect(API.getSelectedElements()[1].y).toEqual(150);
expect(API.getSelectedElements()[2].y).toEqual(300);
});
it("should distribute selected elements horizontally based on their centers", async () => {
createAndSelectThreeRectanglesWithoutGap();
expect(API.getSelectedElements()[0].x).toEqual(0);
expect(API.getSelectedElements()[1].x).toEqual(10);
expect(API.getSelectedElements()[2].x).toEqual(200);
API.executeAction(distributeHorizontally);
expect(API.getSelectedElements()[0].x).toEqual(0);
expect(API.getSelectedElements()[1].x).toEqual(50);
expect(API.getSelectedElements()[2].x).toEqual(200);
});
it("should distribute selected elements vertically with based on their centers", async () => {
createAndSelectThreeRectanglesWithoutGap();
expect(API.getSelectedElements()[0].y).toEqual(0);
expect(API.getSelectedElements()[1].y).toEqual(10);
expect(API.getSelectedElements()[2].y).toEqual(200);
API.executeAction(distributeVertically);
expect(API.getSelectedElements()[0].y).toEqual(0);
expect(API.getSelectedElements()[1].y).toEqual(50);
expect(API.getSelectedElements()[2].y).toEqual(200);
});
});

View File

@ -0,0 +1,153 @@
import { getEmbedLink } from "../src/embeddable";
describe("YouTube timestamp parsing", () => {
it("should parse YouTube URLs with timestamp in seconds", () => {
const testCases = [
{
url: "https://www.youtube.com/watch?v=dQw4w9WgXcQ&t=90",
expectedStart: 90,
},
{
url: "https://youtu.be/dQw4w9WgXcQ?t=120",
expectedStart: 120,
},
{
url: "https://www.youtube.com/watch?v=dQw4w9WgXcQ&start=150",
expectedStart: 150,
},
];
testCases.forEach(({ url, expectedStart }) => {
const result = getEmbedLink(url);
expect(result).toBeTruthy();
expect(result?.type).toBe("video");
if (result?.type === "video" || result?.type === "generic") {
expect(result.link).toContain(`start=${expectedStart}`);
}
});
});
it("should parse YouTube URLs with timestamp in time format", () => {
const testCases = [
{
url: "https://www.youtube.com/watch?v=dQw4w9WgXcQ&t=1m30s",
expectedStart: 90, // 1*60 + 30
},
{
url: "https://youtu.be/dQw4w9WgXcQ?t=2m45s",
expectedStart: 165, // 2*60 + 45
},
{
url: "https://www.youtube.com/watch?v=dQw4w9WgXcQ&t=1h2m3s",
expectedStart: 3723, // 1*3600 + 2*60 + 3
},
{
url: "https://www.youtube.com/watch?v=dQw4w9WgXcQ&t=45s",
expectedStart: 45,
},
{
url: "https://www.youtube.com/watch?v=dQw4w9WgXcQ&t=5m",
expectedStart: 300, // 5*60
},
{
url: "https://www.youtube.com/watch?v=dQw4w9WgXcQ&t=2h",
expectedStart: 7200, // 2*3600
},
];
testCases.forEach(({ url, expectedStart }) => {
const result = getEmbedLink(url);
expect(result).toBeTruthy();
expect(result?.type).toBe("video");
if (result?.type === "video" || result?.type === "generic") {
expect(result.link).toContain(`start=${expectedStart}`);
}
});
});
it("should handle YouTube URLs without timestamps", () => {
const testCases = [
"https://www.youtube.com/watch?v=dQw4w9WgXcQ",
"https://youtu.be/dQw4w9WgXcQ",
"https://www.youtube.com/embed/dQw4w9WgXcQ",
];
testCases.forEach((url) => {
const result = getEmbedLink(url);
expect(result).toBeTruthy();
expect(result?.type).toBe("video");
if (result?.type === "video" || result?.type === "generic") {
expect(result.link).not.toContain("start=");
}
});
});
it("should handle YouTube shorts URLs with timestamps", () => {
const url = "https://www.youtube.com/shorts/dQw4w9WgXcQ?t=30";
const result = getEmbedLink(url);
expect(result).toBeTruthy();
expect(result?.type).toBe("video");
if (result?.type === "video" || result?.type === "generic") {
expect(result.link).toContain("start=30");
}
// Shorts should have portrait aspect ratio
expect(result?.intrinsicSize).toEqual({ w: 315, h: 560 });
});
it("should handle playlist URLs with timestamps", () => {
const url =
"https://www.youtube.com/playlist?list=PLrAXtmRdnEQy1KbG5lbfgQ0-PKQY6FKYZ&t=60";
const result = getEmbedLink(url);
expect(result).toBeTruthy();
expect(result?.type).toBe("video");
if (result?.type === "video" || result?.type === "generic") {
expect(result.link).toContain("start=60");
expect(result.link).toContain("list=PLrAXtmRdnEQy1KbG5lbfgQ0-PKQY6FKYZ");
}
});
it("should handle malformed or edge case timestamps", () => {
const testCases = [
{
url: "https://www.youtube.com/watch?v=dQw4w9WgXcQ&t=abc",
expectedStart: 0, // Invalid timestamp should default to 0
},
{
url: "https://www.youtube.com/watch?v=dQw4w9WgXcQ&t=",
expectedStart: 0, // Empty timestamp should default to 0
},
{
url: "https://www.youtube.com/watch?v=dQw4w9WgXcQ&t=0",
expectedStart: 0, // Zero timestamp should be handled
},
];
testCases.forEach(({ url, expectedStart }) => {
const result = getEmbedLink(url);
expect(result).toBeTruthy();
expect(result?.type).toBe("video");
if (result?.type === "video" || result?.type === "generic") {
if (expectedStart === 0) {
expect(result.link).not.toContain("start=");
} else {
expect(result.link).toContain(`start=${expectedStart}`);
}
}
});
});
it("should preserve other URL parameters", () => {
const url =
"https://www.youtube.com/watch?v=dQw4w9WgXcQ&t=90&feature=youtu.be&list=PLtest";
const result = getEmbedLink(url);
expect(result).toBeTruthy();
expect(result?.type).toBe("video");
if (result?.type === "video" || result?.type === "generic") {
expect(result.link).toContain("start=90");
expect(result.link).toContain("enablejsapi=1");
}
});
});

View File

@ -136,7 +136,8 @@ describe("Test Linear Elements", () => {
Keyboard.withModifierKeys({ ctrl: true }, () => { Keyboard.withModifierKeys({ ctrl: true }, () => {
Keyboard.keyPress(KEYS.ENTER); Keyboard.keyPress(KEYS.ENTER);
}); });
expect(h.state.editingLinearElement?.elementId).toEqual(line.id); expect(h.state.selectedLinearElement?.isEditing).toBe(true);
expect(h.state.selectedLinearElement?.elementId).toEqual(line.id);
}; };
const drag = (startPoint: GlobalPoint, endPoint: GlobalPoint) => { const drag = (startPoint: GlobalPoint, endPoint: GlobalPoint) => {
@ -253,75 +254,82 @@ describe("Test Linear Elements", () => {
}); });
fireEvent.click(queryByText(contextMenu as HTMLElement, "Edit line")!); fireEvent.click(queryByText(contextMenu as HTMLElement, "Edit line")!);
expect(h.state.editingLinearElement?.elementId).toEqual(h.elements[0].id); expect(h.state.selectedLinearElement?.isEditing).toBe(true);
expect(h.state.selectedLinearElement?.elementId).toEqual(h.elements[0].id);
}); });
it("should enter line editor via enter (line)", () => { it("should enter line editor via enter (line)", () => {
createTwoPointerLinearElement("line"); createTwoPointerLinearElement("line");
expect(h.state.editingLinearElement?.elementId).toBeUndefined(); expect(h.state.selectedLinearElement?.isEditing).toBe(false);
mouse.clickAt(midpoint[0], midpoint[1]); mouse.clickAt(midpoint[0], midpoint[1]);
Keyboard.keyPress(KEYS.ENTER); Keyboard.keyPress(KEYS.ENTER);
expect(h.state.editingLinearElement?.elementId).toEqual(h.elements[0].id); expect(h.state.selectedLinearElement?.isEditing).toBe(true);
expect(h.state.selectedLinearElement?.elementId).toEqual(h.elements[0].id);
}); });
// ctrl+enter alias (to align with arrows) // ctrl+enter alias (to align with arrows)
it("should enter line editor via ctrl+enter (line)", () => { it("should enter line editor via ctrl+enter (line)", () => {
createTwoPointerLinearElement("line"); createTwoPointerLinearElement("line");
expect(h.state.editingLinearElement?.elementId).toBeUndefined(); expect(h.state.selectedLinearElement?.isEditing).toBe(false);
mouse.clickAt(midpoint[0], midpoint[1]); mouse.clickAt(midpoint[0], midpoint[1]);
Keyboard.withModifierKeys({ ctrl: true }, () => { Keyboard.withModifierKeys({ ctrl: true }, () => {
Keyboard.keyPress(KEYS.ENTER); Keyboard.keyPress(KEYS.ENTER);
}); });
expect(h.state.editingLinearElement?.elementId).toEqual(h.elements[0].id); expect(h.state.selectedLinearElement?.isEditing).toBe(true);
expect(h.state.selectedLinearElement?.elementId).toEqual(h.elements[0].id);
}); });
it("should enter line editor via ctrl+enter (arrow)", () => { it("should enter line editor via ctrl+enter (arrow)", () => {
createTwoPointerLinearElement("arrow"); createTwoPointerLinearElement("arrow");
expect(h.state.editingLinearElement?.elementId).toBeUndefined(); expect(h.state.selectedLinearElement?.isEditing).toBe(false);
mouse.clickAt(midpoint[0], midpoint[1]); mouse.clickAt(midpoint[0], midpoint[1]);
Keyboard.withModifierKeys({ ctrl: true }, () => { Keyboard.withModifierKeys({ ctrl: true }, () => {
Keyboard.keyPress(KEYS.ENTER); Keyboard.keyPress(KEYS.ENTER);
}); });
expect(h.state.editingLinearElement?.elementId).toEqual(h.elements[0].id); expect(h.state.selectedLinearElement?.isEditing).toBe(true);
expect(h.state.selectedLinearElement?.elementId).toEqual(h.elements[0].id);
}); });
it("should enter line editor on ctrl+dblclick (simple arrow)", () => { it("should enter line editor on ctrl+dblclick (simple arrow)", () => {
createTwoPointerLinearElement("arrow"); createTwoPointerLinearElement("arrow");
expect(h.state.editingLinearElement?.elementId).toBeUndefined(); expect(h.state.selectedLinearElement?.isEditing).toBe(false);
Keyboard.withModifierKeys({ ctrl: true }, () => { Keyboard.withModifierKeys({ ctrl: true }, () => {
mouse.doubleClick(); mouse.doubleClick();
}); });
expect(h.state.editingLinearElement?.elementId).toEqual(h.elements[0].id); expect(h.state.selectedLinearElement?.isEditing).toBe(true);
expect(h.state.selectedLinearElement?.elementId).toEqual(h.elements[0].id);
}); });
it("should enter line editor on ctrl+dblclick (line)", () => { it("should enter line editor on ctrl+dblclick (line)", () => {
createTwoPointerLinearElement("line"); createTwoPointerLinearElement("line");
expect(h.state.editingLinearElement?.elementId).toBeUndefined(); expect(h.state.selectedLinearElement?.isEditing).toBe(false);
Keyboard.withModifierKeys({ ctrl: true }, () => { Keyboard.withModifierKeys({ ctrl: true }, () => {
mouse.doubleClick(); mouse.doubleClick();
}); });
expect(h.state.editingLinearElement?.elementId).toEqual(h.elements[0].id); expect(h.state.selectedLinearElement?.isEditing).toBe(true);
expect(h.state.selectedLinearElement?.elementId).toEqual(h.elements[0].id);
}); });
it("should enter line editor on dblclick (line)", () => { it("should enter line editor on dblclick (line)", () => {
createTwoPointerLinearElement("line"); createTwoPointerLinearElement("line");
expect(h.state.editingLinearElement?.elementId).toBeUndefined(); expect(h.state.selectedLinearElement?.isEditing).toBe(false);
mouse.doubleClick(); mouse.doubleClick();
expect(h.state.editingLinearElement?.elementId).toEqual(h.elements[0].id); expect(h.state.selectedLinearElement?.isEditing).toBe(true);
expect(h.state.selectedLinearElement?.elementId).toEqual(h.elements[0].id);
}); });
it("should not enter line editor on dblclick (arrow)", async () => { it("should not enter line editor on dblclick (arrow)", async () => {
createTwoPointerLinearElement("arrow"); createTwoPointerLinearElement("arrow");
expect(h.state.editingLinearElement?.elementId).toBeUndefined(); expect(h.state.selectedLinearElement?.isEditing).toBe(false);
mouse.doubleClick(); mouse.doubleClick();
expect(h.state.editingLinearElement).toEqual(null); expect(h.state.selectedLinearElement).toBe(null);
await getTextEditor(); await getTextEditor();
}); });
@ -330,10 +338,12 @@ describe("Test Linear Elements", () => {
const arrow = h.elements[0] as ExcalidrawLinearElement; const arrow = h.elements[0] as ExcalidrawLinearElement;
enterLineEditingMode(arrow); enterLineEditingMode(arrow);
expect(h.state.editingLinearElement?.elementId).toEqual(arrow.id); expect(h.state.selectedLinearElement?.isEditing).toBe(true);
expect(h.state.selectedLinearElement?.elementId).toEqual(arrow.id);
mouse.doubleClick(); mouse.doubleClick();
expect(h.state.editingLinearElement?.elementId).toEqual(arrow.id); expect(h.state.selectedLinearElement?.isEditing).toBe(true);
expect(h.state.selectedLinearElement?.elementId).toEqual(arrow.id);
expect(h.elements.length).toEqual(1); expect(h.elements.length).toEqual(1);
expect(document.querySelector(TEXT_EDITOR_SELECTOR)).toBe(null); expect(document.querySelector(TEXT_EDITOR_SELECTOR)).toBe(null);
@ -367,7 +377,7 @@ describe("Test Linear Elements", () => {
// drag line from midpoint // drag line from midpoint
drag(midpoint, pointFrom(midpoint[0] + delta, midpoint[1] + delta)); drag(midpoint, pointFrom(midpoint[0] + delta, midpoint[1] + delta));
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot( expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
`12`, `11`,
); );
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`6`); expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`6`);
@ -469,7 +479,7 @@ describe("Test Linear Elements", () => {
drag(startPoint, endPoint); drag(startPoint, endPoint);
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot( expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
`12`, `11`,
); );
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`7`); expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`7`);
@ -537,7 +547,7 @@ describe("Test Linear Elements", () => {
); );
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot( expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
`16`, `14`,
); );
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`7`); expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`7`);
@ -588,7 +598,7 @@ describe("Test Linear Elements", () => {
drag(hitCoords, pointFrom(hitCoords[0] - delta, hitCoords[1] - delta)); drag(hitCoords, pointFrom(hitCoords[0] - delta, hitCoords[1] - delta));
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot( expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
`12`, `11`,
); );
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`6`); expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`6`);
@ -629,7 +639,7 @@ describe("Test Linear Elements", () => {
drag(hitCoords, pointFrom(hitCoords[0] + delta, hitCoords[1] + delta)); drag(hitCoords, pointFrom(hitCoords[0] + delta, hitCoords[1] + delta));
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot( expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
`12`, `11`,
); );
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`6`); expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`6`);
@ -677,7 +687,7 @@ describe("Test Linear Elements", () => {
deletePoint(points[2]); deletePoint(points[2]);
expect(line.points.length).toEqual(3); expect(line.points.length).toEqual(3);
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot( expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
`18`, `17`,
); );
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`7`); expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`7`);
@ -735,7 +745,7 @@ describe("Test Linear Elements", () => {
), ),
); );
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot( expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
`16`, `14`,
); );
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`7`); expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`7`);
expect(line.points.length).toEqual(5); expect(line.points.length).toEqual(5);
@ -833,7 +843,7 @@ describe("Test Linear Elements", () => {
drag(hitCoords, pointFrom(hitCoords[0] + delta, hitCoords[1] + delta)); drag(hitCoords, pointFrom(hitCoords[0] + delta, hitCoords[1] + delta));
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot( expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
`12`, `11`,
); );
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`6`); expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`6`);

View File

@ -10,6 +10,8 @@ import { alignElements } from "@excalidraw/element";
import { CaptureUpdateAction } from "@excalidraw/element"; import { CaptureUpdateAction } from "@excalidraw/element";
import { getSelectedElementsByGroup } from "@excalidraw/element";
import type { ExcalidrawElement } from "@excalidraw/element/types"; import type { ExcalidrawElement } from "@excalidraw/element/types";
import type { Alignment } from "@excalidraw/element"; import type { Alignment } from "@excalidraw/element";
@ -38,7 +40,11 @@ export const alignActionsPredicate = (
) => { ) => {
const selectedElements = app.scene.getSelectedElements(appState); const selectedElements = app.scene.getSelectedElements(appState);
return ( return (
selectedElements.length > 1 && getSelectedElementsByGroup(
selectedElements,
app.scene.getNonDeletedElementsMap(),
appState as Readonly<AppState>,
).length > 1 &&
// TODO enable aligning frames when implemented properly // TODO enable aligning frames when implemented properly
!selectedElements.some((el) => isFrameLikeElement(el)) !selectedElements.some((el) => isFrameLikeElement(el))
); );
@ -52,7 +58,12 @@ const alignSelectedElements = (
) => { ) => {
const selectedElements = app.scene.getSelectedElements(appState); const selectedElements = app.scene.getSelectedElements(appState);
const updatedElements = alignElements(selectedElements, alignment, app.scene); const updatedElements = alignElements(
selectedElements,
alignment,
app.scene,
appState,
);
const updatedElementsMap = arrayToMap(updatedElements); const updatedElementsMap = arrayToMap(updatedElements);

View File

@ -205,16 +205,19 @@ export const actionDeleteSelected = register({
icon: TrashIcon, icon: TrashIcon,
trackEvent: { category: "element", action: "delete" }, trackEvent: { category: "element", action: "delete" },
perform: (elements, appState, formData, app) => { perform: (elements, appState, formData, app) => {
if (appState.editingLinearElement) { if (appState.selectedLinearElement?.isEditing) {
const { const {
elementId, elementId,
selectedPointsIndices, selectedPointsIndices,
startBindingElement, startBindingElement,
endBindingElement, endBindingElement,
} = appState.editingLinearElement; } = appState.selectedLinearElement;
const elementsMap = app.scene.getNonDeletedElementsMap(); const elementsMap = app.scene.getNonDeletedElementsMap();
const element = LinearElementEditor.getElement(elementId, elementsMap); const linearElement = LinearElementEditor.getElement(
if (!element) { elementId,
elementsMap,
);
if (!linearElement) {
return false; return false;
} }
// case: no point selected → do nothing, as deleting the whole element // case: no point selected → do nothing, as deleting the whole element
@ -225,10 +228,10 @@ export const actionDeleteSelected = register({
return false; return false;
} }
// case: deleting last remaining point // case: deleting all points
if (element.points.length < 2) { if (selectedPointsIndices.length >= linearElement.points.length) {
const nextElements = elements.map((el) => { const nextElements = elements.map((el) => {
if (el.id === element.id) { if (el.id === linearElement.id) {
return newElementWith(el, { isDeleted: true }); return newElementWith(el, { isDeleted: true });
} }
return el; return el;
@ -239,7 +242,7 @@ export const actionDeleteSelected = register({
elements: nextElements, elements: nextElements,
appState: { appState: {
...nextAppState, ...nextAppState,
editingLinearElement: null, selectedLinearElement: null,
}, },
captureUpdate: CaptureUpdateAction.IMMEDIATELY, captureUpdate: CaptureUpdateAction.IMMEDIATELY,
}; };
@ -252,20 +255,24 @@ export const actionDeleteSelected = register({
? null ? null
: startBindingElement, : startBindingElement,
endBindingElement: selectedPointsIndices?.includes( endBindingElement: selectedPointsIndices?.includes(
element.points.length - 1, linearElement.points.length - 1,
) )
? null ? null
: endBindingElement, : endBindingElement,
}; };
LinearElementEditor.deletePoints(element, app, selectedPointsIndices); LinearElementEditor.deletePoints(
linearElement,
app,
selectedPointsIndices,
);
return { return {
elements, elements,
appState: { appState: {
...appState, ...appState,
editingLinearElement: { selectedLinearElement: {
...appState.editingLinearElement, ...appState.selectedLinearElement,
...binding, ...binding,
selectedPointsIndices: selectedPointsIndices:
selectedPointsIndices?.[0] > 0 selectedPointsIndices?.[0] > 0

View File

@ -10,6 +10,8 @@ import { distributeElements } from "@excalidraw/element";
import { CaptureUpdateAction } from "@excalidraw/element"; import { CaptureUpdateAction } from "@excalidraw/element";
import { getSelectedElementsByGroup } from "@excalidraw/element";
import type { ExcalidrawElement } from "@excalidraw/element/types"; import type { ExcalidrawElement } from "@excalidraw/element/types";
import type { Distribution } from "@excalidraw/element"; import type { Distribution } from "@excalidraw/element";
@ -31,7 +33,11 @@ import type { AppClassProperties, AppState } from "../types";
const enableActionGroup = (appState: AppState, app: AppClassProperties) => { const enableActionGroup = (appState: AppState, app: AppClassProperties) => {
const selectedElements = app.scene.getSelectedElements(appState); const selectedElements = app.scene.getSelectedElements(appState);
return ( return (
selectedElements.length > 1 && getSelectedElementsByGroup(
selectedElements,
app.scene.getNonDeletedElementsMap(),
appState as Readonly<AppState>,
).length > 2 &&
// TODO enable distributing frames when implemented properly // TODO enable distributing frames when implemented properly
!selectedElements.some((el) => isFrameLikeElement(el)) !selectedElements.some((el) => isFrameLikeElement(el))
); );
@ -49,6 +55,7 @@ const distributeSelectedElements = (
selectedElements, selectedElements,
app.scene.getNonDeletedElementsMap(), app.scene.getNonDeletedElementsMap(),
distribution, distribution,
appState,
); );
const updatedElementsMap = arrayToMap(updatedElements); const updatedElementsMap = arrayToMap(updatedElements);

View File

@ -39,7 +39,7 @@ export const actionDuplicateSelection = register({
} }
// duplicate selected point(s) if editing a line // duplicate selected point(s) if editing a line
if (appState.editingLinearElement) { if (appState.selectedLinearElement?.isEditing) {
// TODO: Invariants should be checked here instead of duplicateSelectedPoints() // TODO: Invariants should be checked here instead of duplicateSelectedPoints()
try { try {
const newAppState = LinearElementEditor.duplicateSelectedPoints( const newAppState = LinearElementEditor.duplicateSelectedPoints(

View File

@ -94,9 +94,9 @@ export const actionFinalize = register({
} }
} }
if (appState.editingLinearElement) { if (appState.selectedLinearElement?.isEditing) {
const { elementId, startBindingElement, endBindingElement } = const { elementId, startBindingElement, endBindingElement } =
appState.editingLinearElement; appState.selectedLinearElement;
const element = LinearElementEditor.getElement(elementId, elementsMap); const element = LinearElementEditor.getElement(elementId, elementsMap);
if (element) { if (element) {
@ -122,7 +122,11 @@ export const actionFinalize = register({
appState: { appState: {
...appState, ...appState,
cursorButton: "up", cursorButton: "up",
editingLinearElement: null, selectedLinearElement: new LinearElementEditor(
element,
arrayToMap(elementsMap),
false, // exit editing mode
),
}, },
captureUpdate: CaptureUpdateAction.IMMEDIATELY, captureUpdate: CaptureUpdateAction.IMMEDIATELY,
}; };
@ -154,11 +158,7 @@ export const actionFinalize = register({
if (element) { if (element) {
// pen and mouse have hover // pen and mouse have hover
if ( if (appState.multiElement && element.type !== "freedraw") {
appState.multiElement &&
element.type !== "freedraw" &&
appState.lastPointerDownWith !== "touch"
) {
const { points, lastCommittedPoint } = element; const { points, lastCommittedPoint } = element;
if ( if (
!lastCommittedPoint || !lastCommittedPoint ||
@ -289,7 +289,7 @@ export const actionFinalize = register({
}, },
keyTest: (event, appState) => keyTest: (event, appState) =>
(event.key === KEYS.ESCAPE && (event.key === KEYS.ESCAPE &&
(appState.editingLinearElement !== null || (appState.selectedLinearElement?.isEditing ||
(!appState.newElement && appState.multiElement === null))) || (!appState.newElement && appState.multiElement === null))) ||
((event.key === KEYS.ESCAPE || event.key === KEYS.ENTER) && ((event.key === KEYS.ESCAPE || event.key === KEYS.ENTER) &&
appState.multiElement !== null), appState.multiElement !== null),

View File

@ -1,10 +1,9 @@
import { LinearElementEditor } from "@excalidraw/element";
import { import {
isElbowArrow, isElbowArrow,
isLinearElement, isLinearElement,
isLineElement, isLineElement,
} from "@excalidraw/element"; } from "@excalidraw/element";
import { arrayToMap } from "@excalidraw/common"; import { arrayToMap, invariant } from "@excalidraw/common";
import { import {
toggleLinePolygonState, toggleLinePolygonState,
@ -46,7 +45,7 @@ export const actionToggleLinearEditor = register({
predicate: (elements, appState, _, app) => { predicate: (elements, appState, _, app) => {
const selectedElements = app.scene.getSelectedElements(appState); const selectedElements = app.scene.getSelectedElements(appState);
if ( if (
!appState.editingLinearElement && !appState.selectedLinearElement?.isEditing &&
selectedElements.length === 1 && selectedElements.length === 1 &&
isLinearElement(selectedElements[0]) && isLinearElement(selectedElements[0]) &&
!isElbowArrow(selectedElements[0]) !isElbowArrow(selectedElements[0])
@ -61,14 +60,25 @@ export const actionToggleLinearEditor = register({
includeBoundTextElement: true, includeBoundTextElement: true,
})[0] as ExcalidrawLinearElement; })[0] as ExcalidrawLinearElement;
const editingLinearElement = invariant(selectedElement, "No selected element found");
appState.editingLinearElement?.elementId === selectedElement.id invariant(
? null appState.selectedLinearElement,
: new LinearElementEditor(selectedElement, arrayToMap(elements)); "No selected linear element found",
);
invariant(
selectedElement.id === appState.selectedLinearElement.elementId,
"Selected element ID and linear editor elementId does not match",
);
const selectedLinearElement = {
...appState.selectedLinearElement,
isEditing: !appState.selectedLinearElement.isEditing,
};
return { return {
appState: { appState: {
...appState, ...appState,
editingLinearElement, selectedLinearElement,
}, },
captureUpdate: CaptureUpdateAction.IMMEDIATELY, captureUpdate: CaptureUpdateAction.IMMEDIATELY,
}; };

View File

@ -21,7 +21,7 @@ export const actionSelectAll = register({
trackEvent: { category: "canvas" }, trackEvent: { category: "canvas" },
viewMode: false, viewMode: false,
perform: (elements, appState, value, app) => { perform: (elements, appState, value, app) => {
if (appState.editingLinearElement) { if (appState.selectedLinearElement?.isEditing) {
return false; return false;
} }

View File

@ -48,7 +48,6 @@ export const getDefaultAppState = (): Omit<
newElement: null, newElement: null,
editingTextElement: null, editingTextElement: null,
editingGroupId: null, editingGroupId: null,
editingLinearElement: null,
activeTool: { activeTool: {
type: "selection", type: "selection",
customType: null, customType: null,
@ -175,7 +174,6 @@ const APP_STATE_STORAGE_CONF = (<
newElement: { browser: false, export: false, server: false }, newElement: { browser: false, export: false, server: false },
editingTextElement: { browser: false, export: false, server: false }, editingTextElement: { browser: false, export: false, server: false },
editingGroupId: { browser: true, export: false, server: false }, editingGroupId: { browser: true, export: false, server: false },
editingLinearElement: { browser: false, export: false, server: false },
activeTool: { browser: true, export: false, server: false }, activeTool: { browser: true, export: false, server: false },
penMode: { browser: true, export: false, server: false }, penMode: { browser: true, export: false, server: false },
penDetected: { browser: true, export: false, server: false }, penDetected: { browser: true, export: false, server: false },

View File

@ -140,7 +140,7 @@ export const SelectedShapeActions = ({
targetElements.length === 1 || isSingleElementBoundContainer; targetElements.length === 1 || isSingleElementBoundContainer;
const showLineEditorAction = const showLineEditorAction =
!appState.editingLinearElement && !appState.selectedLinearElement?.isEditing &&
targetElements.length === 1 && targetElements.length === 1 &&
isLinearElement(targetElements[0]) && isLinearElement(targetElements[0]) &&
!isElbowArrow(targetElements[0]); !isElbowArrow(targetElements[0]);
@ -505,15 +505,3 @@ export const ExitZenModeAction = ({
{t("buttons.exitZenMode")} {t("buttons.exitZenMode")}
</button> </button>
); );
export const FinalizeAction = ({
renderAction,
className,
}: {
renderAction: ActionManager["renderAction"];
className?: string;
}) => (
<div className={`finalize-button ${className}`}>
{renderAction("finalize", { size: "small" })}
</div>
);

View File

@ -102,6 +102,7 @@ import {
randomInteger, randomInteger,
CLASSES, CLASSES,
Emitter, Emitter,
MINIMUM_ARROW_SIZE,
} from "@excalidraw/common"; } from "@excalidraw/common";
import { import {
@ -594,6 +595,10 @@ class App extends React.Component<AppProps, AppState> {
* insert to DOM before user initially scrolls to them) */ * insert to DOM before user initially scrolls to them) */
private initializedEmbeds = new Set<ExcalidrawIframeLikeElement["id"]>(); private initializedEmbeds = new Set<ExcalidrawIframeLikeElement["id"]>();
private handleToastClose = () => {
this.setToast(null);
};
private elementsPendingErasure: ElementsPendingErasure = new Set(); private elementsPendingErasure: ElementsPendingErasure = new Set();
public flowChartCreator: FlowChartCreator = new FlowChartCreator(); public flowChartCreator: FlowChartCreator = new FlowChartCreator();
@ -1708,14 +1713,16 @@ class App extends React.Component<AppProps, AppState> {
/> />
</ElementCanvasButtons> </ElementCanvasButtons>
)} )}
{this.state.toast !== null && ( {this.state.toast !== null && (
<Toast <Toast
message={this.state.toast.message} message={this.state.toast.message}
onClose={() => this.setToast(null)} onClose={this.handleToastClose}
duration={this.state.toast.duration} duration={this.state.toast.duration}
closable={this.state.toast.closable} closable={this.state.toast.closable}
/> />
)} )}
{this.state.contextMenu && ( {this.state.contextMenu && (
<ContextMenu <ContextMenu
items={this.state.contextMenu.items} items={this.state.contextMenu.items}
@ -2152,9 +2159,14 @@ class App extends React.Component<AppProps, AppState> {
public dismissLinearEditor = () => { public dismissLinearEditor = () => {
setTimeout(() => { setTimeout(() => {
if (this.state.selectedLinearElement?.isEditing) {
this.setState({ this.setState({
editingLinearElement: null, selectedLinearElement: {
...this.state.selectedLinearElement,
isEditing: false,
},
}); });
}
}); });
}; };
@ -2850,15 +2862,15 @@ class App extends React.Component<AppProps, AppState> {
); );
if ( if (
this.state.editingLinearElement && this.state.selectedLinearElement?.isEditing &&
!this.state.selectedElementIds[this.state.editingLinearElement.elementId] !this.state.selectedElementIds[this.state.selectedLinearElement.elementId]
) { ) {
// defer so that the scheduleCapture flag isn't reset via current update // defer so that the scheduleCapture flag isn't reset via current update
setTimeout(() => { setTimeout(() => {
// execute only if the condition still holds when the deferred callback // execute only if the condition still holds when the deferred callback
// executes (it can be scheduled multiple times depending on how // executes (it can be scheduled multiple times depending on how
// many times the component renders) // many times the component renders)
this.state.editingLinearElement && this.state.selectedLinearElement?.isEditing &&
this.actionManager.executeAction(actionFinalize); this.actionManager.executeAction(actionFinalize);
}); });
} }
@ -4413,17 +4425,13 @@ class App extends React.Component<AppProps, AppState> {
if (event[KEYS.CTRL_OR_CMD] || isLineElement(selectedElement)) { if (event[KEYS.CTRL_OR_CMD] || isLineElement(selectedElement)) {
if (isLinearElement(selectedElement)) { if (isLinearElement(selectedElement)) {
if ( if (
!this.state.editingLinearElement || !this.state.selectedLinearElement?.isEditing ||
this.state.editingLinearElement.elementId !== selectedElement.id this.state.selectedLinearElement.elementId !==
selectedElement.id
) { ) {
this.store.scheduleCapture(); this.store.scheduleCapture();
if (!isElbowArrow(selectedElement)) { if (!isElbowArrow(selectedElement)) {
this.setState({ this.actionManager.executeAction(actionToggleLinearEditor);
editingLinearElement: new LinearElementEditor(
selectedElement,
this.scene.getNonDeletedElementsMap(),
),
});
} }
} }
} }
@ -4920,7 +4928,17 @@ class App extends React.Component<AppProps, AppState> {
}), }),
onSubmit: withBatchedUpdates(({ viaKeyboard, nextOriginalText }) => { onSubmit: withBatchedUpdates(({ viaKeyboard, nextOriginalText }) => {
const isDeleted = !nextOriginalText.trim(); const isDeleted = !nextOriginalText.trim();
if (isDeleted && !isExistingElement) {
// let's just remove the element from the scene, as it's an empty just created text element
this.scene.replaceAllElements(
this.scene
.getElementsIncludingDeleted()
.filter((x) => x.id !== element.id),
);
} else {
updateElement(nextOriginalText, isDeleted); updateElement(nextOriginalText, isDeleted);
}
// select the created text element only if submitting via keyboard // select the created text element only if submitting via keyboard
// (when submitting via click it should act as signal to deselect) // (when submitting via click it should act as signal to deselect)
if (!isDeleted && viaKeyboard) { if (!isDeleted && viaKeyboard) {
@ -4949,9 +4967,10 @@ class App extends React.Component<AppProps, AppState> {
element, element,
]); ]);
} }
if (!isDeleted || isExistingElement) {
// we need to record either way, whether the text element was added or removed
// since we need to sync this delta to other clients, otherwise it would end up with inconsistencies
this.store.scheduleCapture(); this.store.scheduleCapture();
}
flushSync(() => { flushSync(() => {
this.setState({ this.setState({
@ -5415,15 +5434,12 @@ class App extends React.Component<AppProps, AppState> {
if ( if (
((event[KEYS.CTRL_OR_CMD] && isSimpleArrow(selectedLinearElement)) || ((event[KEYS.CTRL_OR_CMD] && isSimpleArrow(selectedLinearElement)) ||
isLineElement(selectedLinearElement)) && isLineElement(selectedLinearElement)) &&
this.state.editingLinearElement?.elementId !== selectedLinearElement.id (!this.state.selectedLinearElement?.isEditing ||
this.state.selectedLinearElement.elementId !==
selectedLinearElement.id)
) { ) {
this.store.scheduleCapture(); // Use the proper action to ensure immediate history capture
this.setState({ this.actionManager.executeAction(actionToggleLinearEditor);
editingLinearElement: new LinearElementEditor(
selectedLinearElement,
this.scene.getNonDeletedElementsMap(),
),
});
return; return;
} else if ( } else if (
this.state.selectedLinearElement && this.state.selectedLinearElement &&
@ -5488,8 +5504,8 @@ class App extends React.Component<AppProps, AppState> {
return; return;
} }
} else if ( } else if (
this.state.editingLinearElement && this.state.selectedLinearElement?.isEditing &&
this.state.editingLinearElement.elementId === this.state.selectedLinearElement.elementId ===
selectedLinearElement.id && selectedLinearElement.id &&
isLineElement(selectedLinearElement) isLineElement(selectedLinearElement)
) { ) {
@ -5544,7 +5560,7 @@ class App extends React.Component<AppProps, AppState> {
// shouldn't edit/create text when inside line editor (often false positive) // shouldn't edit/create text when inside line editor (often false positive)
if (!this.state.editingLinearElement) { if (!this.state.selectedLinearElement?.isEditing) {
const container = this.getTextBindableContainerAtPosition( const container = this.getTextBindableContainerAtPosition(
sceneX, sceneX,
sceneY, sceneY,
@ -5846,8 +5862,8 @@ class App extends React.Component<AppProps, AppState> {
} }
if ( if (
this.state.editingLinearElement && this.state.selectedLinearElement?.isEditing &&
!this.state.editingLinearElement.isDragging !this.state.selectedLinearElement.isDragging
) { ) {
const result = LinearElementEditor.handlePointerMove( const result = LinearElementEditor.handlePointerMove(
event, event,
@ -5857,32 +5873,36 @@ class App extends React.Component<AppProps, AppState> {
); );
if (result) { if (result) {
const { linearElementEditor: editingLinearElement, snapLines } = result; const { editingLinearElement, snapLines } = result;
if ( if (
editingLinearElement && editingLinearElement &&
editingLinearElement !== this.state.editingLinearElement editingLinearElement !== this.state.selectedLinearElement
) { ) {
// Since we are reading from previous state which is not possible with // Since we are reading from previous state which is not possible with
// automatic batching in React 18 hence using flush sync to synchronously // automatic batching in React 18 hence using flush sync to synchronously
// update the state. Check https://github.com/excalidraw/excalidraw/pull/5508 for more details. // update the state. Check https://github.com/excalidraw/excalidraw/pull/5508 for more details.
flushSync(() => { flushSync(() => {
this.setState({ this.setState({
editingLinearElement, selectedLinearElement: editingLinearElement,
snapLines, snapLines,
}); });
}); });
} }
if (editingLinearElement?.lastUncommittedPoint != null) { const latestLinearElement = this.scene.getElement(
editingLinearElement.elementId,
);
if (
editingLinearElement.lastUncommittedPoint != null &&
latestLinearElement &&
isBindingElementType(latestLinearElement.type)
) {
this.maybeSuggestBindingAtCursor( this.maybeSuggestBindingAtCursor(
scenePointer, scenePointer,
editingLinearElement.elbowed, editingLinearElement.elbowed,
); );
} else { } else if (this.state.suggestedBindings.length) {
// causes stack overflow if not sync
flushSync(() => {
this.setState({ suggestedBindings: [] }); this.setState({ suggestedBindings: [] });
});
} }
} }
} }
@ -6118,7 +6138,7 @@ class App extends React.Component<AppProps, AppState> {
if ( if (
selectedElements.length === 1 && selectedElements.length === 1 &&
!isOverScrollBar && !isOverScrollBar &&
!this.state.editingLinearElement !this.state.selectedLinearElement?.isEditing
) { ) {
// for linear elements, we'd like to prioritize point dragging over edge resizing // for linear elements, we'd like to prioritize point dragging over edge resizing
// therefore, we update and check hovered point index first // therefore, we update and check hovered point index first
@ -6236,15 +6256,6 @@ class App extends React.Component<AppProps, AppState> {
setCursor(this.interactiveCanvas, CURSOR_TYPE.AUTO); setCursor(this.interactiveCanvas, CURSOR_TYPE.AUTO);
} else if (isOverScrollBar) { } else if (isOverScrollBar) {
setCursor(this.interactiveCanvas, CURSOR_TYPE.AUTO); setCursor(this.interactiveCanvas, CURSOR_TYPE.AUTO);
} else if (
this.state.selectedLinearElement &&
hitElement?.id === this.state.selectedLinearElement.elementId
) {
this.handleHoverSelectedLinearElement(
this.state.selectedLinearElement,
scenePointerX,
scenePointerY,
);
} else if ( } else if (
// if using cmd/ctrl, we're not dragging // if using cmd/ctrl, we're not dragging
!event[KEYS.CTRL_OR_CMD] !event[KEYS.CTRL_OR_CMD]
@ -6286,6 +6297,14 @@ class App extends React.Component<AppProps, AppState> {
} else { } else {
setCursor(this.interactiveCanvas, CURSOR_TYPE.AUTO); setCursor(this.interactiveCanvas, CURSOR_TYPE.AUTO);
} }
if (this.state.selectedLinearElement) {
this.handleHoverSelectedLinearElement(
this.state.selectedLinearElement,
scenePointerX,
scenePointerY,
);
}
} }
if (this.state.openDialog?.name === "elementLinkSelector" && hitElement) { if (this.state.openDialog?.name === "elementLinkSelector" && hitElement) {
@ -7123,7 +7142,7 @@ class App extends React.Component<AppProps, AppState> {
if ( if (
selectedElements.length === 1 && selectedElements.length === 1 &&
!this.state.editingLinearElement && !this.state.selectedLinearElement?.isEditing &&
!isElbowArrow(selectedElements[0]) && !isElbowArrow(selectedElements[0]) &&
!( !(
this.state.selectedLinearElement && this.state.selectedLinearElement &&
@ -7194,8 +7213,7 @@ class App extends React.Component<AppProps, AppState> {
} }
} else { } else {
if (this.state.selectedLinearElement) { if (this.state.selectedLinearElement) {
const linearElementEditor = const linearElementEditor = this.state.selectedLinearElement;
this.state.editingLinearElement || this.state.selectedLinearElement;
const ret = LinearElementEditor.handlePointerDown( const ret = LinearElementEditor.handlePointerDown(
event, event,
this, this,
@ -7209,10 +7227,6 @@ class App extends React.Component<AppProps, AppState> {
} }
if (ret.linearElementEditor) { if (ret.linearElementEditor) {
this.setState({ selectedLinearElement: ret.linearElementEditor }); this.setState({ selectedLinearElement: ret.linearElementEditor });
if (this.state.editingLinearElement) {
this.setState({ editingLinearElement: ret.linearElementEditor });
}
} }
if (ret.didAddPoint) { if (ret.didAddPoint) {
return true; return true;
@ -7313,11 +7327,11 @@ class App extends React.Component<AppProps, AppState> {
this.clearSelection(hitElement); this.clearSelection(hitElement);
} }
if (this.state.editingLinearElement) { if (this.state.selectedLinearElement?.isEditing) {
this.setState({ this.setState({
selectedElementIds: makeNextSelectedElementIds( selectedElementIds: makeNextSelectedElementIds(
{ {
[this.state.editingLinearElement.elementId]: true, [this.state.selectedLinearElement.elementId]: true,
}, },
this.state, this.state,
), ),
@ -8176,8 +8190,6 @@ class App extends React.Component<AppProps, AppState> {
this.scene, this.scene,
); );
flushSync(() => {
if (this.state.selectedLinearElement) {
this.setState({ this.setState({
selectedLinearElement: { selectedLinearElement: {
...this.state.selectedLinearElement, ...this.state.selectedLinearElement,
@ -8185,8 +8197,6 @@ class App extends React.Component<AppProps, AppState> {
pointerDownState: ret.pointerDownState, pointerDownState: ret.pointerDownState,
}, },
}); });
}
});
return; return;
} }
@ -8244,7 +8254,9 @@ class App extends React.Component<AppProps, AppState> {
pointDistance( pointDistance(
pointFrom(pointerCoords.x, pointerCoords.y), pointFrom(pointerCoords.x, pointerCoords.y),
pointFrom(pointerDownState.origin.x, pointerDownState.origin.y), pointFrom(pointerDownState.origin.x, pointerDownState.origin.y),
) < DRAGGING_THRESHOLD ) *
this.state.zoom.value <
MINIMUM_ARROW_SIZE
) { ) {
return; return;
} }
@ -8262,8 +8274,7 @@ class App extends React.Component<AppProps, AppState> {
const elementsMap = this.scene.getNonDeletedElementsMap(); const elementsMap = this.scene.getNonDeletedElementsMap();
if (this.state.selectedLinearElement) { if (this.state.selectedLinearElement) {
const linearElementEditor = const linearElementEditor = this.state.selectedLinearElement;
this.state.editingLinearElement || this.state.selectedLinearElement;
if ( if (
LinearElementEditor.shouldAddMidpoint( LinearElementEditor.shouldAddMidpoint(
@ -8299,16 +8310,6 @@ class App extends React.Component<AppProps, AppState> {
}, },
}); });
} }
if (this.state.editingLinearElement) {
this.setState({
editingLinearElement: {
...this.state.editingLinearElement,
pointerDownState: ret.pointerDownState,
selectedPointsIndices: ret.selectedPointsIndices,
segmentMidPointHoveredCoords: null,
},
});
}
}); });
return; return;
@ -8342,9 +8343,9 @@ class App extends React.Component<AppProps, AppState> {
); );
const isSelectingPointsInLineEditor = const isSelectingPointsInLineEditor =
this.state.editingLinearElement && this.state.selectedLinearElement?.isEditing &&
event.shiftKey && event.shiftKey &&
this.state.editingLinearElement.elementId === this.state.selectedLinearElement.elementId ===
pointerDownState.hit.element?.id; pointerDownState.hit.element?.id;
if ( if (
(hasHitASelectedElement || (hasHitASelectedElement ||
@ -8697,7 +8698,6 @@ class App extends React.Component<AppProps, AppState> {
pointerDownState.lastCoords.x = pointerCoords.x; pointerDownState.lastCoords.x = pointerCoords.x;
pointerDownState.lastCoords.y = pointerCoords.y; pointerDownState.lastCoords.y = pointerCoords.y;
if (event.altKey) { if (event.altKey) {
flushSync(() => {
this.setActiveTool( this.setActiveTool(
{ type: "lasso", fromSelection: true }, { type: "lasso", fromSelection: true },
event.shiftKey, event.shiftKey,
@ -8710,10 +8710,9 @@ class App extends React.Component<AppProps, AppState> {
this.setAppState({ this.setAppState({
selectionElement: null, selectionElement: null,
}); });
}); return;
} else {
this.maybeDragNewGenericElement(pointerDownState, event);
} }
this.maybeDragNewGenericElement(pointerDownState, event);
} else if (this.state.activeTool.type === "lasso") { } else if (this.state.activeTool.type === "lasso") {
if (!event.altKey && this.state.activeTool.fromSelection) { if (!event.altKey && this.state.activeTool.fromSelection) {
this.setActiveTool({ type: "selection" }); this.setActiveTool({ type: "selection" });
@ -8920,7 +8919,7 @@ class App extends React.Component<AppProps, AppState> {
const elements = this.scene.getNonDeletedElements(); const elements = this.scene.getNonDeletedElements();
// box-select line editor points // box-select line editor points
if (this.state.editingLinearElement) { if (this.state.selectedLinearElement?.isEditing) {
LinearElementEditor.handleBoxSelection( LinearElementEditor.handleBoxSelection(
event, event,
this.state, this.state,
@ -9163,23 +9162,23 @@ class App extends React.Component<AppProps, AppState> {
// Handle end of dragging a point of a linear element, might close a loop // Handle end of dragging a point of a linear element, might close a loop
// and sets binding element // and sets binding element
if (this.state.editingLinearElement) { if (this.state.selectedLinearElement?.isEditing) {
if ( if (
!pointerDownState.boxSelection.hasOccurred && !pointerDownState.boxSelection.hasOccurred &&
pointerDownState.hit?.element?.id !== pointerDownState.hit?.element?.id !==
this.state.editingLinearElement.elementId this.state.selectedLinearElement.elementId
) { ) {
this.actionManager.executeAction(actionFinalize); this.actionManager.executeAction(actionFinalize);
} else { } else {
const editingLinearElement = LinearElementEditor.handlePointerUp( const editingLinearElement = LinearElementEditor.handlePointerUp(
childEvent, childEvent,
this.state.editingLinearElement, this.state.selectedLinearElement,
this.state, this.state,
this.scene, this.scene,
); );
if (editingLinearElement !== this.state.editingLinearElement) { if (editingLinearElement !== this.state.selectedLinearElement) {
this.setState({ this.setState({
editingLinearElement, selectedLinearElement: editingLinearElement,
suggestedBindings: [], suggestedBindings: [],
}); });
} }
@ -9282,25 +9281,54 @@ class App extends React.Component<AppProps, AppState> {
this.state, this.state,
); );
if (!pointerDownState.drag.hasOccurred && newElement && !multiElement) { const dragDistance =
pointDistance(
pointFrom(pointerCoords.x, pointerCoords.y),
pointFrom(pointerDownState.origin.x, pointerDownState.origin.y),
) * this.state.zoom.value;
if (
(!pointerDownState.drag.hasOccurred ||
dragDistance < MINIMUM_ARROW_SIZE) &&
newElement &&
!multiElement
) {
if (this.device.isTouchScreen) {
const FIXED_DELTA_X = Math.min(
(this.state.width * 0.7) / this.state.zoom.value,
100,
);
this.scene.mutateElement( this.scene.mutateElement(
newElement, newElement,
{ {
x: newElement.x - FIXED_DELTA_X / 2,
points: [ points: [
...newElement.points, pointFrom<LocalPoint>(0, 0),
pointFrom<LocalPoint>( pointFrom<LocalPoint>(FIXED_DELTA_X, 0),
pointerCoords.x - newElement.x,
pointerCoords.y - newElement.y,
),
], ],
}, },
{ informMutation: false, isDragging: false }, { informMutation: false, isDragging: false },
); );
this.actionManager.executeAction(actionFinalize);
} else {
const dx = pointerCoords.x - newElement.x;
const dy = pointerCoords.y - newElement.y;
this.scene.mutateElement(
newElement,
{
points: [...newElement.points, pointFrom<LocalPoint>(dx, dy)],
},
{ informMutation: false, isDragging: false },
);
this.setState({ this.setState({
multiElement: newElement, multiElement: newElement,
newElement, newElement,
}); });
}
} else if (pointerDownState.drag.hasOccurred && !multiElement) { } else if (pointerDownState.drag.hasOccurred && !multiElement) {
if ( if (
isBindingEnabled(this.state) && isBindingEnabled(this.state) &&
@ -9663,14 +9691,17 @@ class App extends React.Component<AppProps, AppState> {
!pointerDownState.hit.wasAddedToSelection && !pointerDownState.hit.wasAddedToSelection &&
// if we're editing a line, pointerup shouldn't switch selection if // if we're editing a line, pointerup shouldn't switch selection if
// box selected // box selected
(!this.state.editingLinearElement || (!this.state.selectedLinearElement?.isEditing ||
!pointerDownState.boxSelection.hasOccurred) && !pointerDownState.boxSelection.hasOccurred) &&
// hitElement can be set when alt + ctrl to toggle lasso and we will // hitElement can be set when alt + ctrl to toggle lasso and we will
// just respect the selected elements from lasso instead // just respect the selected elements from lasso instead
this.state.activeTool.type !== "lasso" this.state.activeTool.type !== "lasso"
) { ) {
// when inside line editor, shift selects points instead // when inside line editor, shift selects points instead
if (childEvent.shiftKey && !this.state.editingLinearElement) { if (
childEvent.shiftKey &&
!this.state.selectedLinearElement?.isEditing
) {
if (this.state.selectedElementIds[hitElement.id]) { if (this.state.selectedElementIds[hitElement.id]) {
if (isSelectedViaGroup(this.state, hitElement)) { if (isSelectedViaGroup(this.state, hitElement)) {
this.setState((_prevState) => { this.setState((_prevState) => {
@ -9848,8 +9879,9 @@ class App extends React.Component<AppProps, AppState> {
(!hitElement && (!hitElement &&
pointerDownState.hit.hasHitCommonBoundingBoxOfSelectedElements)) pointerDownState.hit.hasHitCommonBoundingBoxOfSelectedElements))
) { ) {
if (this.state.editingLinearElement) { if (this.state.selectedLinearElement?.isEditing) {
this.setState({ editingLinearElement: null }); // Exit editing mode but keep the element selected
this.actionManager.executeAction(actionToggleLinearEditor);
} else { } else {
// Deselect selected elements // Deselect selected elements
this.setState({ this.setState({

View File

@ -108,6 +108,7 @@ $verticalBreakpoint: 861px;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.25rem; gap: 0.25rem;
overflow: hidden;
} }
} }

View File

@ -59,6 +59,8 @@ import { useStableCallback } from "../../hooks/useStableCallback";
import { activeConfirmDialogAtom } from "../ActiveConfirmDialog"; import { activeConfirmDialogAtom } from "../ActiveConfirmDialog";
import { useStable } from "../../hooks/useStable"; import { useStable } from "../../hooks/useStable";
import { Ellipsify } from "../Ellipsify";
import * as defaultItems from "./defaultCommandPaletteItems"; import * as defaultItems from "./defaultCommandPaletteItems";
import "./CommandPalette.scss"; import "./CommandPalette.scss";
@ -964,7 +966,7 @@ const CommandItem = ({
} }
/> />
)} )}
{command.label} <Ellipsify>{command.label}</Ellipsify>
</div> </div>
{showShortcut && command.shortcut && ( {showShortcut && command.shortcut && (
<CommandShortcutHint shortcut={command.shortcut} /> <CommandShortcutHint shortcut={command.shortcut} />

View File

@ -0,0 +1,18 @@
export const Ellipsify = ({
children,
...rest
}: { children: React.ReactNode } & React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
{...rest}
style={{
textOverflow: "ellipsis",
overflow: "hidden",
whiteSpace: "nowrap",
...rest.style,
}}
>
{children}
</span>
);
};

View File

@ -114,7 +114,7 @@ const getHints = ({
appState.selectionElement && appState.selectionElement &&
!selectedElements.length && !selectedElements.length &&
!appState.editingTextElement && !appState.editingTextElement &&
!appState.editingLinearElement !appState.selectedLinearElement?.isEditing
) { ) {
return [t("hints.deepBoxSelect")]; return [t("hints.deepBoxSelect")];
} }
@ -129,8 +129,8 @@ const getHints = ({
if (selectedElements.length === 1) { if (selectedElements.length === 1) {
if (isLinearElement(selectedElements[0])) { if (isLinearElement(selectedElements[0])) {
if (appState.editingLinearElement) { if (appState.selectedLinearElement?.isEditing) {
return appState.editingLinearElement.selectedPointsIndices return appState.selectedLinearElement.selectedPointsIndices
? t("hints.lineEditor_pointSelected") ? t("hints.lineEditor_pointSelected")
: t("hints.lineEditor_nothingSelected"); : t("hints.lineEditor_nothingSelected");
} }

View File

@ -7,6 +7,7 @@ export const InlineIcon = ({ icon }: { icon: React.ReactNode }) => {
display: "inline-block", display: "inline-block",
lineHeight: 0, lineHeight: 0,
verticalAlign: "middle", verticalAlign: "middle",
flex: "0 0 auto",
}} }}
> >
{icon} {icon}

View File

@ -192,7 +192,6 @@ const getRelevantAppStateProps = (
viewModeEnabled: appState.viewModeEnabled, viewModeEnabled: appState.viewModeEnabled,
openDialog: appState.openDialog, openDialog: appState.openDialog,
editingGroupId: appState.editingGroupId, editingGroupId: appState.editingGroupId,
editingLinearElement: appState.editingLinearElement,
selectedElementIds: appState.selectedElementIds, selectedElementIds: appState.selectedElementIds,
frameToHighlight: appState.frameToHighlight, frameToHighlight: appState.frameToHighlight,
offsetLeft: appState.offsetLeft, offsetLeft: appState.offsetLeft,

View File

@ -34,6 +34,13 @@ const StaticCanvas = (props: StaticCanvasProps) => {
const wrapperRef = useRef<HTMLDivElement>(null); const wrapperRef = useRef<HTMLDivElement>(null);
const isComponentMounted = useRef(false); const isComponentMounted = useRef(false);
useEffect(() => {
props.canvas.style.width = `${props.appState.width}px`;
props.canvas.style.height = `${props.appState.height}px`;
props.canvas.width = props.appState.width * props.scale;
props.canvas.height = props.appState.height * props.scale;
}, [props.appState.height, props.appState.width, props.canvas, props.scale]);
useEffect(() => { useEffect(() => {
const wrapper = wrapperRef.current; const wrapper = wrapperRef.current;
if (!wrapper) { if (!wrapper) {
@ -49,26 +56,6 @@ const StaticCanvas = (props: StaticCanvasProps) => {
canvas.classList.add("excalidraw__canvas", "static"); canvas.classList.add("excalidraw__canvas", "static");
} }
const widthString = `${props.appState.width}px`;
const heightString = `${props.appState.height}px`;
if (canvas.style.width !== widthString) {
canvas.style.width = widthString;
}
if (canvas.style.height !== heightString) {
canvas.style.height = heightString;
}
const scaledWidth = props.appState.width * props.scale;
const scaledHeight = props.appState.height * props.scale;
// setting width/height resets the canvas even if dimensions not changed,
// which would cause flicker when we skip frame (due to throttling)
if (canvas.width !== scaledWidth) {
canvas.width = scaledWidth;
}
if (canvas.height !== scaledHeight) {
canvas.height = scaledHeight;
}
renderStaticScene( renderStaticScene(
{ {
canvas, canvas,

View File

@ -19,6 +19,8 @@
border-radius: var(--border-radius-lg); border-radius: var(--border-radius-lg);
position: relative; position: relative;
transition: box-shadow 0.5s ease-in-out; transition: box-shadow 0.5s ease-in-out;
display: flex;
flex-direction: column;
&.zen-mode { &.zen-mode {
box-shadow: none; box-shadow: none;
@ -100,6 +102,7 @@
align-items: center; align-items: center;
cursor: pointer; cursor: pointer;
border-radius: var(--border-radius-md); border-radius: var(--border-radius-md);
flex: 1 0 auto;
@media screen and (min-width: 1921px) { @media screen and (min-width: 1921px) {
height: 2.25rem; height: 2.25rem;

View File

@ -1,5 +1,7 @@
import { useDevice } from "../App"; import { useDevice } from "../App";
import { Ellipsify } from "../Ellipsify";
import type { JSX } from "react"; import type { JSX } from "react";
const MenuItemContent = ({ const MenuItemContent = ({
@ -18,7 +20,7 @@ const MenuItemContent = ({
<> <>
{icon && <div className="dropdown-menu-item__icon">{icon}</div>} {icon && <div className="dropdown-menu-item__icon">{icon}</div>}
<div style={textStyle} className="dropdown-menu-item__text"> <div style={textStyle} className="dropdown-menu-item__text">
{children} <Ellipsify>{children}</Ellipsify>
</div> </div>
{shortcut && !device.editor.isMobile && ( {shortcut && !device.editor.isMobile && (
<div className="dropdown-menu-item__shortcut">{shortcut}</div> <div className="dropdown-menu-item__shortcut">{shortcut}</div>

View File

@ -2,13 +2,7 @@ import clsx from "clsx";
import { actionShortcuts } from "../../actions"; import { actionShortcuts } from "../../actions";
import { useTunnels } from "../../context/tunnels"; import { useTunnels } from "../../context/tunnels";
import { import { ExitZenModeAction, UndoRedoActions, ZoomActions } from "../Actions";
ExitZenModeAction,
FinalizeAction,
UndoRedoActions,
ZoomActions,
} from "../Actions";
import { useDevice } from "../App";
import { HelpButton } from "../HelpButton"; import { HelpButton } from "../HelpButton";
import { Section } from "../Section"; import { Section } from "../Section";
import Stack from "../Stack"; import Stack from "../Stack";
@ -29,10 +23,6 @@ const Footer = ({
}) => { }) => {
const { FooterCenterTunnel, WelcomeScreenHelpHintTunnel } = useTunnels(); const { FooterCenterTunnel, WelcomeScreenHelpHintTunnel } = useTunnels();
const device = useDevice();
const showFinalize =
!appState.viewModeEnabled && appState.multiElement && device.isTouchScreen;
return ( return (
<footer <footer
role="contentinfo" role="contentinfo"
@ -60,15 +50,6 @@ const Footer = ({
})} })}
/> />
)} )}
{showFinalize && (
<FinalizeAction
renderAction={actionManager.renderAction}
className={clsx("zen-mode-transition", {
"layer-ui__wrapper__footer-left--transition-left":
appState.zenModeEnabled,
})}
/>
)}
</Section> </Section>
</Stack.Col> </Stack.Col>
</div> </div>

View File

@ -281,6 +281,7 @@ export { Sidebar } from "./components/Sidebar/Sidebar";
export { Button } from "./components/Button"; export { Button } from "./components/Button";
export { Footer }; export { Footer };
export { MainMenu }; export { MainMenu };
export { Ellipsify } from "./components/Ellipsify";
export { useDevice } from "./components/App"; export { useDevice } from "./components/App";
export { WelcomeScreen }; export { WelcomeScreen };
export { LiveCollaborationTrigger }; export { LiveCollaborationTrigger };

View File

@ -66,12 +66,22 @@
"last 1 safari version" "last 1 safari version"
] ]
}, },
"repository": "https://github.com/excalidraw/excalidraw",
"bugs": "https://github.com/excalidraw/excalidraw/issues",
"homepage": "https://github.com/excalidraw/excalidraw/tree/master/packages/excalidraw",
"scripts": {
"gen:types": "rimraf types && tsc",
"build:esm": "rimraf dist && node ../../scripts/buildPackage.js && yarn gen:types"
},
"peerDependencies": { "peerDependencies": {
"react": "^17.0.2 || ^18.2.0 || ^19.0.0", "react": "^17.0.2 || ^18.2.0 || ^19.0.0",
"react-dom": "^17.0.2 || ^18.2.0 || ^19.0.0" "react-dom": "^17.0.2 || ^18.2.0 || ^19.0.0"
}, },
"dependencies": { "dependencies": {
"@braintree/sanitize-url": "6.0.2", "@braintree/sanitize-url": "6.0.2",
"@excalidraw/common": "0.18.0",
"@excalidraw/element": "0.18.0",
"@excalidraw/math": "0.18.0",
"@excalidraw/laser-pointer": "1.3.1", "@excalidraw/laser-pointer": "1.3.1",
"@excalidraw/mermaid-to-excalidraw": "1.1.2", "@excalidraw/mermaid-to-excalidraw": "1.1.2",
"@excalidraw/random-username": "1.1.0", "@excalidraw/random-username": "1.1.0",
@ -124,12 +134,5 @@
"harfbuzzjs": "0.3.6", "harfbuzzjs": "0.3.6",
"jest-diff": "29.7.0", "jest-diff": "29.7.0",
"typescript": "4.9.4" "typescript": "4.9.4"
},
"repository": "https://github.com/excalidraw/excalidraw",
"bugs": "https://github.com/excalidraw/excalidraw/issues",
"homepage": "https://github.com/excalidraw/excalidraw/tree/master/packages/excalidraw",
"scripts": {
"gen:types": "rimraf types && tsc",
"build:esm": "rimraf dist && node ../../scripts/buildPackage.js && yarn gen:types"
} }
} }

View File

@ -118,7 +118,8 @@ const renderLinearElementPointHighlight = (
) => { ) => {
const { elementId, hoverPointIndex } = appState.selectedLinearElement!; const { elementId, hoverPointIndex } = appState.selectedLinearElement!;
if ( if (
appState.editingLinearElement?.selectedPointsIndices?.includes( appState.selectedLinearElement?.isEditing &&
appState.selectedLinearElement?.selectedPointsIndices?.includes(
hoverPointIndex, hoverPointIndex,
) )
) { ) {
@ -180,7 +181,7 @@ const renderSingleLinearPoint = <Point extends GlobalPoint | LocalPoint>(
point[0], point[0],
point[1], point[1],
(isOverlappingPoint (isOverlappingPoint
? radius * (appState.editingLinearElement ? 1.5 : 2) ? radius * (appState.selectedLinearElement?.isEditing ? 1.5 : 2)
: radius) / appState.zoom.value, : radius) / appState.zoom.value,
!isPhantomPoint, !isPhantomPoint,
!isOverlappingPoint || isSelected, !isOverlappingPoint || isSelected,
@ -448,7 +449,7 @@ const renderLinearPointHandles = (
); );
const { POINT_HANDLE_SIZE } = LinearElementEditor; const { POINT_HANDLE_SIZE } = LinearElementEditor;
const radius = appState.editingLinearElement const radius = appState.selectedLinearElement?.isEditing
? POINT_HANDLE_SIZE ? POINT_HANDLE_SIZE
: POINT_HANDLE_SIZE / 2; : POINT_HANDLE_SIZE / 2;
@ -470,7 +471,8 @@ const renderLinearPointHandles = (
); );
let isSelected = let isSelected =
!!appState.editingLinearElement?.selectedPointsIndices?.includes(idx); !!appState.selectedLinearElement?.isEditing &&
!!appState.selectedLinearElement?.selectedPointsIndices?.includes(idx);
// when element is a polygon, highlight the last point as well if first // when element is a polygon, highlight the last point as well if first
// point is selected since they overlap and the last point tends to be // point is selected since they overlap and the last point tends to be
// rendered on top // rendered on top
@ -479,7 +481,8 @@ const renderLinearPointHandles = (
element.polygon && element.polygon &&
!isSelected && !isSelected &&
idx === element.points.length - 1 && idx === element.points.length - 1 &&
!!appState.editingLinearElement?.selectedPointsIndices?.includes(0) !!appState.selectedLinearElement?.isEditing &&
!!appState.selectedLinearElement?.selectedPointsIndices?.includes(0)
) { ) {
isSelected = true; isSelected = true;
} }
@ -535,7 +538,7 @@ const renderLinearPointHandles = (
); );
midPoints.forEach((segmentMidPoint) => { midPoints.forEach((segmentMidPoint) => {
if (appState.editingLinearElement || points.length === 2) { if (appState.selectedLinearElement?.isEditing || points.length === 2) {
renderSingleLinearPoint( renderSingleLinearPoint(
context, context,
appState, appState,
@ -760,7 +763,10 @@ const _renderInteractiveScene = ({
// Getting the element using LinearElementEditor during collab mismatches version - being one head of visible elements due to // Getting the element using LinearElementEditor during collab mismatches version - being one head of visible elements due to
// ShapeCache returns empty hence making sure that we get the // ShapeCache returns empty hence making sure that we get the
// correct element from visible elements // correct element from visible elements
if (appState.editingLinearElement?.elementId === element.id) { if (
appState.selectedLinearElement?.isEditing &&
appState.selectedLinearElement.elementId === element.id
) {
if (element) { if (element) {
editingLinearElement = element as NonDeleted<ExcalidrawLinearElement>; editingLinearElement = element as NonDeleted<ExcalidrawLinearElement>;
} }
@ -853,7 +859,8 @@ const _renderInteractiveScene = ({
// correct element from visible elements // correct element from visible elements
if ( if (
selectedElements.length === 1 && selectedElements.length === 1 &&
appState.editingLinearElement?.elementId === selectedElements[0].id appState.selectedLinearElement?.isEditing &&
appState.selectedLinearElement.elementId === selectedElements[0].id
) { ) {
renderLinearPointHandles( renderLinearPointHandles(
context, context,
@ -884,7 +891,7 @@ const _renderInteractiveScene = ({
} }
// Paint selected elements // Paint selected elements
if (!appState.multiElement && !appState.editingLinearElement) { if (!appState.multiElement && !appState.selectedLinearElement?.isEditing) {
const showBoundingBox = shouldShowBoundingBox(selectedElements, appState); const showBoundingBox = shouldShowBoundingBox(selectedElements, appState);
const isSingleLinearElementSelected = const isSingleLinearElementSelected =

View File

@ -1,9 +1,16 @@
import { throttleRAF } from "@excalidraw/common"; import { throttleRAF } from "@excalidraw/common";
import { renderElement } from "@excalidraw/element"; import {
getTargetFrame,
isInvisiblySmallElement,
renderElement,
shouldApplyFrameClip,
} from "@excalidraw/element";
import { bootstrapCanvas, getNormalizedCanvasDimensions } from "./helpers"; import { bootstrapCanvas, getNormalizedCanvasDimensions } from "./helpers";
import { frameClip } from "./staticScene";
import type { NewElementSceneRenderConfig } from "../scene/types"; import type { NewElementSceneRenderConfig } from "../scene/types";
const _renderNewElementScene = ({ const _renderNewElementScene = ({
@ -29,11 +36,37 @@ const _renderNewElementScene = ({
normalizedHeight, normalizedHeight,
}); });
// Apply zoom
context.save(); context.save();
// Apply zoom
context.scale(appState.zoom.value, appState.zoom.value); context.scale(appState.zoom.value, appState.zoom.value);
if (newElement && newElement.type !== "selection") { if (newElement && newElement.type !== "selection") {
// e.g. when creating arrows and we're still below the arrow drag distance
// threshold
// (for now we skip render only with elements while we're creating to be
// safe)
if (isInvisiblySmallElement(newElement)) {
return;
}
const frameId = newElement.frameId || appState.frameToHighlight?.id;
if (
frameId &&
appState.frameRendering.enabled &&
appState.frameRendering.clip
) {
const frame = getTargetFrame(newElement, elementsMap, appState);
if (
frame &&
shouldApplyFrameClip(newElement, frame, appState, elementsMap)
) {
frameClip(frame, context, renderConfig, appState);
}
}
renderElement( renderElement(
newElement, newElement,
elementsMap, elementsMap,
@ -46,6 +79,8 @@ const _renderNewElementScene = ({
} else { } else {
context.clearRect(0, 0, normalizedWidth, normalizedHeight); context.clearRect(0, 0, normalizedWidth, normalizedHeight);
} }
context.restore();
} }
}; };

View File

@ -113,7 +113,7 @@ const strokeGrid = (
context.restore(); context.restore();
}; };
const frameClip = ( export const frameClip = (
frame: ExcalidrawFrameLikeElement, frame: ExcalidrawFrameLikeElement,
context: CanvasRenderingContext2D, context: CanvasRenderingContext2D,
renderConfig: StaticCanvasRenderConfig, renderConfig: StaticCanvasRenderConfig,

View File

@ -908,7 +908,6 @@ exports[`contextMenu element > right-clicking on a group should select whole gro
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -1106,7 +1105,6 @@ exports[`contextMenu element > selecting 'Add to library' in context menu adds e
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -1319,7 +1317,6 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -1649,7 +1646,6 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -1979,7 +1975,6 @@ exports[`contextMenu element > selecting 'Copy styles' in context menu copies st
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -2192,7 +2187,6 @@ exports[`contextMenu element > selecting 'Delete' in context menu deletes elemen
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -2432,7 +2426,6 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -2729,7 +2722,6 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -3100,7 +3092,6 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -3592,7 +3583,6 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -3914,7 +3904,6 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -4236,7 +4225,6 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -5520,7 +5508,6 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -6736,7 +6723,6 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -7670,7 +7656,6 @@ exports[`contextMenu element > shows context menu for canvas > [end of test] app
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -8669,7 +8654,6 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -9659,7 +9643,6 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,

View File

@ -14,8 +14,12 @@ exports[`<Excalidraw/> > <MainMenu/> > should render main menu with host menu it
> >
<div <div
class="dropdown-menu-item__text" class="dropdown-menu-item__text"
>
<span
style="text-overflow: ellipsis; overflow: hidden; white-space: nowrap;"
> >
Click me Click me
</span>
</div> </div>
</button> </button>
<a <a
@ -26,8 +30,12 @@ exports[`<Excalidraw/> > <MainMenu/> > should render main menu with host menu it
> >
<div <div
class="dropdown-menu-item__text" class="dropdown-menu-item__text"
>
<span
style="text-overflow: ellipsis; overflow: hidden; white-space: nowrap;"
> >
Excalidraw blog Excalidraw blog
</span>
</div> </div>
</a> </a>
<div <div
@ -87,8 +95,12 @@ exports[`<Excalidraw/> > <MainMenu/> > should render main menu with host menu it
</div> </div>
<div <div
class="dropdown-menu-item__text" class="dropdown-menu-item__text"
>
<span
style="text-overflow: ellipsis; overflow: hidden; white-space: nowrap;"
> >
Help Help
</span>
</div> </div>
<div <div
class="dropdown-menu-item__shortcut" class="dropdown-menu-item__shortcut"
@ -137,8 +149,12 @@ exports[`<Excalidraw/> > Test UIOptions prop > Test canvasActions > should rende
</div> </div>
<div <div
class="dropdown-menu-item__text" class="dropdown-menu-item__text"
>
<span
style="text-overflow: ellipsis; overflow: hidden; white-space: nowrap;"
> >
Open Open
</span>
</div> </div>
<div <div
class="dropdown-menu-item__shortcut" class="dropdown-menu-item__shortcut"
@ -174,8 +190,12 @@ exports[`<Excalidraw/> > Test UIOptions prop > Test canvasActions > should rende
</div> </div>
<div <div
class="dropdown-menu-item__text" class="dropdown-menu-item__text"
>
<span
style="text-overflow: ellipsis; overflow: hidden; white-space: nowrap;"
> >
Save to... Save to...
</span>
</div> </div>
</button> </button>
<button <button
@ -230,8 +250,12 @@ exports[`<Excalidraw/> > Test UIOptions prop > Test canvasActions > should rende
</div> </div>
<div <div
class="dropdown-menu-item__text" class="dropdown-menu-item__text"
>
<span
style="text-overflow: ellipsis; overflow: hidden; white-space: nowrap;"
> >
Export image... Export image...
</span>
</div> </div>
<div <div
class="dropdown-menu-item__shortcut" class="dropdown-menu-item__shortcut"
@ -279,8 +303,12 @@ exports[`<Excalidraw/> > Test UIOptions prop > Test canvasActions > should rende
</div> </div>
<div <div
class="dropdown-menu-item__text" class="dropdown-menu-item__text"
>
<span
style="text-overflow: ellipsis; overflow: hidden; white-space: nowrap;"
> >
Find on canvas Find on canvas
</span>
</div> </div>
<div <div
class="dropdown-menu-item__shortcut" class="dropdown-menu-item__shortcut"
@ -336,8 +364,12 @@ exports[`<Excalidraw/> > Test UIOptions prop > Test canvasActions > should rende
</div> </div>
<div <div
class="dropdown-menu-item__text" class="dropdown-menu-item__text"
>
<span
style="text-overflow: ellipsis; overflow: hidden; white-space: nowrap;"
> >
Help Help
</span>
</div> </div>
<div <div
class="dropdown-menu-item__shortcut" class="dropdown-menu-item__shortcut"
@ -373,8 +405,12 @@ exports[`<Excalidraw/> > Test UIOptions prop > Test canvasActions > should rende
</div> </div>
<div <div
class="dropdown-menu-item__text" class="dropdown-menu-item__text"
>
<span
style="text-overflow: ellipsis; overflow: hidden; white-space: nowrap;"
> >
Reset the canvas Reset the canvas
</span>
</div> </div>
</button> </button>
<div <div
@ -418,8 +454,12 @@ exports[`<Excalidraw/> > Test UIOptions prop > Test canvasActions > should rende
</div> </div>
<div <div
class="dropdown-menu-item__text" class="dropdown-menu-item__text"
>
<span
style="text-overflow: ellipsis; overflow: hidden; white-space: nowrap;"
> >
GitHub GitHub
</span>
</div> </div>
</a> </a>
<a <a
@ -464,8 +504,12 @@ exports[`<Excalidraw/> > Test UIOptions prop > Test canvasActions > should rende
</div> </div>
<div <div
class="dropdown-menu-item__text" class="dropdown-menu-item__text"
>
<span
style="text-overflow: ellipsis; overflow: hidden; white-space: nowrap;"
> >
Follow us Follow us
</span>
</div> </div>
</a> </a>
<a <a
@ -504,8 +548,12 @@ exports[`<Excalidraw/> > Test UIOptions prop > Test canvasActions > should rende
</div> </div>
<div <div
class="dropdown-menu-item__text" class="dropdown-menu-item__text"
>
<span
style="text-overflow: ellipsis; overflow: hidden; white-space: nowrap;"
> >
Discord chat Discord chat
</span>
</div> </div>
</a> </a>
</div> </div>
@ -541,8 +589,12 @@ exports[`<Excalidraw/> > Test UIOptions prop > Test canvasActions > should rende
</div> </div>
<div <div
class="dropdown-menu-item__text" class="dropdown-menu-item__text"
>
<span
style="text-overflow: ellipsis; overflow: hidden; white-space: nowrap;"
> >
Dark mode Dark mode
</span>
</div> </div>
<div <div
class="dropdown-menu-item__shortcut" class="dropdown-menu-item__shortcut"

View File

@ -34,7 +34,6 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -545,10 +544,12 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"id4": true, "id4": true,
}, },
"selectedLinearElementId": "id4", "selectedLinearElementId": "id4",
"selectedLinearElementIsEditing": false,
}, },
"inserted": { "inserted": {
"selectedElementIds": {}, "selectedElementIds": {},
"selectedLinearElementId": null, "selectedLinearElementId": null,
"selectedLinearElementIsEditing": null,
}, },
}, },
}, },
@ -646,7 +647,6 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -1027,10 +1027,12 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"id4": true, "id4": true,
}, },
"selectedLinearElementId": "id4", "selectedLinearElementId": "id4",
"selectedLinearElementIsEditing": false,
}, },
"inserted": { "inserted": {
"selectedElementIds": {}, "selectedElementIds": {},
"selectedLinearElementId": null, "selectedLinearElementId": null,
"selectedLinearElementIsEditing": null,
}, },
}, },
}, },
@ -1128,7 +1130,6 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -1491,7 +1492,6 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -1857,7 +1857,6 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -2119,7 +2118,6 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -2417,10 +2415,12 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"id4": true, "id4": true,
}, },
"selectedLinearElementId": "id4", "selectedLinearElementId": "id4",
"selectedLinearElementIsEditing": false,
}, },
"inserted": { "inserted": {
"selectedElementIds": {}, "selectedElementIds": {},
"selectedLinearElementId": null, "selectedLinearElementId": null,
"selectedLinearElementIsEditing": null,
}, },
}, },
}, },
@ -2557,7 +2557,6 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -2818,7 +2817,6 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -3083,7 +3081,6 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -3376,7 +3373,6 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -3661,7 +3657,6 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -3895,7 +3890,6 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -4151,7 +4145,6 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -4421,7 +4414,6 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -4649,7 +4641,6 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -4877,7 +4868,6 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -5103,7 +5093,6 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -5329,7 +5318,6 @@ exports[`history > multiplayer undo/redo > conflicts in frames and their childre
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -5584,7 +5572,6 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -5845,7 +5832,6 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -6207,7 +6193,6 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -6580,7 +6565,6 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -6891,7 +6875,6 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -7107,9 +7090,11 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
"delta": Delta { "delta": Delta {
"deleted": { "deleted": {
"selectedLinearElementId": "id0", "selectedLinearElementId": "id0",
"selectedLinearElementIsEditing": false,
}, },
"inserted": { "inserted": {
"selectedLinearElementId": null, "selectedLinearElementId": null,
"selectedLinearElementIsEditing": null,
}, },
}, },
}, },
@ -7118,16 +7103,16 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
"removed": {}, "removed": {},
"updated": {}, "updated": {},
}, },
"id": "id12", "id": "id4",
}, },
{ {
"appState": AppStateDelta { "appState": AppStateDelta {
"delta": Delta { "delta": Delta {
"deleted": { "deleted": {
"editingLinearElementId": "id0", "selectedLinearElementIsEditing": true,
}, },
"inserted": { "inserted": {
"editingLinearElementId": null, "selectedLinearElementIsEditing": false,
}, },
}, },
}, },
@ -7136,16 +7121,16 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
"removed": {}, "removed": {},
"updated": {}, "updated": {},
}, },
"id": "id13", "id": "id6",
}, },
{ {
"appState": AppStateDelta { "appState": AppStateDelta {
"delta": Delta { "delta": Delta {
"deleted": { "deleted": {
"editingLinearElementId": null, "selectedLinearElementIsEditing": false,
}, },
"inserted": { "inserted": {
"editingLinearElementId": "id0", "selectedLinearElementIsEditing": true,
}, },
}, },
}, },
@ -7154,7 +7139,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
"removed": {}, "removed": {},
"updated": {}, "updated": {},
}, },
"id": "id14", "id": "id10",
}, },
] ]
`; `;
@ -7193,7 +7178,6 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -7390,7 +7374,6 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -7741,7 +7724,6 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -8092,7 +8074,6 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -8497,7 +8478,6 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -8783,7 +8763,6 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -9046,7 +9025,6 @@ exports[`history > multiplayer undo/redo > should not override remote changes on
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -9310,7 +9288,6 @@ exports[`history > multiplayer undo/redo > should not override remote changes on
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -9541,7 +9518,6 @@ exports[`history > multiplayer undo/redo > should override remotely added groups
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -9837,7 +9813,6 @@ exports[`history > multiplayer undo/redo > should override remotely added points
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -10132,9 +10107,11 @@ exports[`history > multiplayer undo/redo > should override remotely added points
"delta": Delta { "delta": Delta {
"deleted": { "deleted": {
"selectedLinearElementId": "id0", "selectedLinearElementId": "id0",
"selectedLinearElementIsEditing": false,
}, },
"inserted": { "inserted": {
"selectedLinearElementId": null, "selectedLinearElementId": null,
"selectedLinearElementIsEditing": null,
}, },
}, },
}, },
@ -10182,7 +10159,6 @@ exports[`history > multiplayer undo/redo > should redistribute deltas when eleme
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -10406,7 +10382,6 @@ exports[`history > multiplayer undo/redo > should redraw arrows on undo > [end o
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -10853,7 +10828,6 @@ exports[`history > multiplayer undo/redo > should update history entries after r
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -11112,7 +11086,6 @@ exports[`history > singleplayer undo/redo > remounting undo/redo buttons should
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -11346,7 +11319,6 @@ exports[`history > singleplayer undo/redo > should clear the redo stack on eleme
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -11582,7 +11554,6 @@ exports[`history > singleplayer undo/redo > should create entry when selecting f
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -11984,7 +11955,6 @@ exports[`history > singleplayer undo/redo > should create new history entry on e
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": "Couldn't load invalid file", "errorMessage": "Couldn't load invalid file",
@ -12193,7 +12163,6 @@ exports[`history > singleplayer undo/redo > should create new history entry on e
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -12402,7 +12371,6 @@ exports[`history > singleplayer undo/redo > should create new history entry on i
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -12625,7 +12593,6 @@ exports[`history > singleplayer undo/redo > should create new history entry on i
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -12848,7 +12815,6 @@ exports[`history > singleplayer undo/redo > should create new history entry on s
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -13092,7 +13058,6 @@ exports[`history > singleplayer undo/redo > should disable undo/redo buttons whe
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -13328,7 +13293,6 @@ exports[`history > singleplayer undo/redo > should end up with no history entry
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -13564,7 +13528,6 @@ exports[`history > singleplayer undo/redo > should iterate through the history w
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -13810,7 +13773,6 @@ exports[`history > singleplayer undo/redo > should not clear the redo stack on s
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -14140,7 +14102,6 @@ exports[`history > singleplayer undo/redo > should not collapse when applying co
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -14309,7 +14270,6 @@ exports[`history > singleplayer undo/redo > should not end up with history entry
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -14592,7 +14552,6 @@ exports[`history > singleplayer undo/redo > should not end up with history entry
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -14854,7 +14813,6 @@ exports[`history > singleplayer undo/redo > should not modify anything on unrela
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -15006,7 +14964,6 @@ exports[`history > singleplayer undo/redo > should not override appstate changes
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -15287,7 +15244,6 @@ exports[`history > singleplayer undo/redo > should support appstate name or view
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -15448,7 +15404,6 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -15712,10 +15667,12 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"id13": true, "id13": true,
}, },
"selectedLinearElementId": "id13", "selectedLinearElementId": "id13",
"selectedLinearElementIsEditing": false,
}, },
"inserted": { "inserted": {
"selectedElementIds": {}, "selectedElementIds": {},
"selectedLinearElementId": null, "selectedLinearElementId": null,
"selectedLinearElementIsEditing": null,
}, },
}, },
}, },
@ -16004,12 +15961,14 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"id13": true, "id13": true,
}, },
"selectedLinearElementId": "id13", "selectedLinearElementId": "id13",
"selectedLinearElementIsEditing": false,
}, },
"inserted": { "inserted": {
"selectedElementIds": { "selectedElementIds": {
"id0": true, "id0": true,
}, },
"selectedLinearElementId": null, "selectedLinearElementId": null,
"selectedLinearElementIsEditing": null,
}, },
}, },
}, },
@ -16146,7 +16105,6 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -16627,12 +16585,14 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"id13": true, "id13": true,
}, },
"selectedLinearElementId": "id13", "selectedLinearElementId": "id13",
"selectedLinearElementIsEditing": false,
}, },
"inserted": { "inserted": {
"selectedElementIds": { "selectedElementIds": {
"id0": true, "id0": true,
}, },
"selectedLinearElementId": null, "selectedLinearElementId": null,
"selectedLinearElementIsEditing": null,
}, },
}, },
}, },
@ -16777,7 +16737,6 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -17258,12 +17217,14 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"id13": true, "id13": true,
}, },
"selectedLinearElementId": "id13", "selectedLinearElementId": "id13",
"selectedLinearElementIsEditing": false,
}, },
"inserted": { "inserted": {
"selectedElementIds": { "selectedElementIds": {
"id0": true, "id0": true,
}, },
"selectedLinearElementId": null, "selectedLinearElementId": null,
"selectedLinearElementIsEditing": null,
}, },
}, },
}, },
@ -17408,7 +17369,6 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -17954,12 +17914,14 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"id13": true, "id13": true,
}, },
"selectedLinearElementId": "id13", "selectedLinearElementId": "id13",
"selectedLinearElementIsEditing": false,
}, },
"inserted": { "inserted": {
"selectedElementIds": { "selectedElementIds": {
"id0": true, "id0": true,
}, },
"selectedLinearElementId": null, "selectedLinearElementId": null,
"selectedLinearElementIsEditing": null,
}, },
}, },
}, },
@ -18067,12 +18029,14 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"id0": true, "id0": true,
}, },
"selectedLinearElementId": null, "selectedLinearElementId": null,
"selectedLinearElementIsEditing": null,
}, },
"inserted": { "inserted": {
"selectedElementIds": { "selectedElementIds": {
"id13": true, "id13": true,
}, },
"selectedLinearElementId": "id13", "selectedLinearElementId": "id13",
"selectedLinearElementIsEditing": false,
}, },
}, },
}, },
@ -18120,7 +18084,6 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -18678,12 +18641,14 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"id13": true, "id13": true,
}, },
"selectedLinearElementId": "id13", "selectedLinearElementId": "id13",
"selectedLinearElementIsEditing": false,
}, },
"inserted": { "inserted": {
"selectedElementIds": { "selectedElementIds": {
"id0": true, "id0": true,
}, },
"selectedLinearElementId": null, "selectedLinearElementId": null,
"selectedLinearElementIsEditing": null,
}, },
}, },
}, },
@ -18791,12 +18756,14 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"id0": true, "id0": true,
}, },
"selectedLinearElementId": null, "selectedLinearElementId": null,
"selectedLinearElementIsEditing": null,
}, },
"inserted": { "inserted": {
"selectedElementIds": { "selectedElementIds": {
"id13": true, "id13": true,
}, },
"selectedLinearElementId": "id13", "selectedLinearElementId": "id13",
"selectedLinearElementIsEditing": false,
}, },
}, },
}, },
@ -18864,7 +18831,6 @@ exports[`history > singleplayer undo/redo > should support changes in elements'
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -19343,7 +19309,6 @@ exports[`history > singleplayer undo/redo > should support duplication of groups
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -19853,7 +19818,6 @@ exports[`history > singleplayer undo/redo > should support element creation, del
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -20311,7 +20275,6 @@ exports[`history > singleplayer undo/redo > should support linear element creati
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -20590,9 +20553,11 @@ exports[`history > singleplayer undo/redo > should support linear element creati
"delta": Delta { "delta": Delta {
"deleted": { "deleted": {
"selectedLinearElementId": "id0", "selectedLinearElementId": "id0",
"selectedLinearElementIsEditing": false,
}, },
"inserted": { "inserted": {
"selectedLinearElementId": null, "selectedLinearElementId": null,
"selectedLinearElementIsEditing": null,
}, },
}, },
}, },
@ -20607,10 +20572,10 @@ exports[`history > singleplayer undo/redo > should support linear element creati
"appState": AppStateDelta { "appState": AppStateDelta {
"delta": Delta { "delta": Delta {
"deleted": { "deleted": {
"editingLinearElementId": "id0", "selectedLinearElementIsEditing": true,
}, },
"inserted": { "inserted": {
"editingLinearElementId": null, "selectedLinearElementIsEditing": false,
}, },
}, },
}, },
@ -20678,10 +20643,10 @@ exports[`history > singleplayer undo/redo > should support linear element creati
"appState": AppStateDelta { "appState": AppStateDelta {
"delta": Delta { "delta": Delta {
"deleted": { "deleted": {
"editingLinearElementId": null, "selectedLinearElementIsEditing": false,
}, },
"inserted": { "inserted": {
"editingLinearElementId": "id0", "selectedLinearElementIsEditing": true,
}, },
}, },
}, },

View File

@ -34,7 +34,6 @@ exports[`given element A and group of elements B and given both are selected whe
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -459,7 +458,6 @@ exports[`given element A and group of elements B and given both are selected whe
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -874,7 +872,6 @@ exports[`regression tests > Cmd/Ctrl-click exclusively select element under poin
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": "id28", "editingGroupId": "id28",
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -1439,7 +1436,6 @@ exports[`regression tests > Drags selected element when hitting only bounding bo
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -1645,7 +1641,6 @@ exports[`regression tests > adjusts z order when grouping > [end of test] appSta
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -2028,7 +2023,6 @@ exports[`regression tests > alt-drag duplicates an element > [end of test] appSt
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -2272,7 +2266,6 @@ exports[`regression tests > arrow keys > [end of test] appState 1`] = `
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -2451,7 +2444,6 @@ exports[`regression tests > can drag element that covers another element, while
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -2775,7 +2767,6 @@ exports[`regression tests > change the properties of a shape > [end of test] app
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -3029,7 +3020,6 @@ exports[`regression tests > click on an element and drag it > [dragged] appState
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -3269,7 +3259,6 @@ exports[`regression tests > click on an element and drag it > [end of test] appS
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -3504,7 +3493,6 @@ exports[`regression tests > click to select a shape > [end of test] appState 1`]
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -3761,7 +3749,6 @@ exports[`regression tests > click-drag to select a group > [end of test] appStat
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -4074,7 +4061,6 @@ exports[`regression tests > deleting last but one element in editing group shoul
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -4509,7 +4495,6 @@ exports[`regression tests > deselects group of selected elements on pointer down
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -4791,7 +4776,6 @@ exports[`regression tests > deselects group of selected elements on pointer up w
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -5066,7 +5050,6 @@ exports[`regression tests > deselects selected element on pointer down when poin
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -5273,7 +5256,6 @@ exports[`regression tests > deselects selected element, on pointer up, when clic
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -5472,7 +5454,6 @@ exports[`regression tests > double click to edit a group > [end of test] appStat
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": "id11", "editingGroupId": "id11",
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -5864,7 +5845,6 @@ exports[`regression tests > drags selected elements from point inside common bou
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -6160,7 +6140,6 @@ exports[`regression tests > draw every type of shape > [end of test] appState 1`
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -6416,12 +6395,14 @@ exports[`regression tests > draw every type of shape > [end of test] undo stack
"id9": true, "id9": true,
}, },
"selectedLinearElementId": "id9", "selectedLinearElementId": "id9",
"selectedLinearElementIsEditing": false,
}, },
"inserted": { "inserted": {
"selectedElementIds": { "selectedElementIds": {
"id6": true, "id6": true,
}, },
"selectedLinearElementId": null, "selectedLinearElementId": null,
"selectedLinearElementIsEditing": null,
}, },
}, },
}, },
@ -6562,12 +6543,14 @@ exports[`regression tests > draw every type of shape > [end of test] undo stack
"id15": true, "id15": true,
}, },
"selectedLinearElementId": null, "selectedLinearElementId": null,
"selectedLinearElementIsEditing": null,
}, },
"inserted": { "inserted": {
"selectedElementIds": { "selectedElementIds": {
"id12": true, "id12": true,
}, },
"selectedLinearElementId": "id12", "selectedLinearElementId": "id12",
"selectedLinearElementIsEditing": false,
}, },
}, },
}, },
@ -6695,9 +6678,11 @@ exports[`regression tests > draw every type of shape > [end of test] undo stack
"delta": Delta { "delta": Delta {
"deleted": { "deleted": {
"selectedLinearElementId": "id15", "selectedLinearElementId": "id15",
"selectedLinearElementIsEditing": false,
}, },
"inserted": { "inserted": {
"selectedLinearElementId": null, "selectedLinearElementId": null,
"selectedLinearElementIsEditing": null,
}, },
}, },
}, },
@ -6716,12 +6701,14 @@ exports[`regression tests > draw every type of shape > [end of test] undo stack
"id22": true, "id22": true,
}, },
"selectedLinearElementId": null, "selectedLinearElementId": null,
"selectedLinearElementIsEditing": null,
}, },
"inserted": { "inserted": {
"selectedElementIds": { "selectedElementIds": {
"id15": true, "id15": true,
}, },
"selectedLinearElementId": "id15", "selectedLinearElementId": "id15",
"selectedLinearElementIsEditing": false,
}, },
}, },
}, },
@ -6847,9 +6834,11 @@ exports[`regression tests > draw every type of shape > [end of test] undo stack
"delta": Delta { "delta": Delta {
"deleted": { "deleted": {
"selectedLinearElementId": "id22", "selectedLinearElementId": "id22",
"selectedLinearElementIsEditing": false,
}, },
"inserted": { "inserted": {
"selectedLinearElementId": null, "selectedLinearElementId": null,
"selectedLinearElementIsEditing": null,
}, },
}, },
}, },
@ -6885,9 +6874,11 @@ exports[`regression tests > draw every type of shape > [end of test] undo stack
"delta": Delta { "delta": Delta {
"deleted": { "deleted": {
"selectedLinearElementId": null, "selectedLinearElementId": null,
"selectedLinearElementIsEditing": null,
}, },
"inserted": { "inserted": {
"selectedLinearElementId": "id22", "selectedLinearElementId": "id22",
"selectedLinearElementIsEditing": false,
}, },
}, },
}, },
@ -6991,7 +6982,6 @@ exports[`regression tests > given a group of selected elements with an element t
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -7324,7 +7314,6 @@ exports[`regression tests > given a selected element A and a not selected elemen
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -7602,7 +7591,6 @@ exports[`regression tests > given selected element A with lower z-index than uns
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -7836,7 +7824,6 @@ exports[`regression tests > given selected element A with lower z-index than uns
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -8075,7 +8062,6 @@ exports[`regression tests > key 2 selects rectangle tool > [end of test] appStat
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -8190,7 +8176,7 @@ exports[`regression tests > key 2 selects rectangle tool > [end of test] undo st
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
"height": 10, "height": 30,
"index": "a0", "index": "a0",
"isDeleted": false, "isDeleted": false,
"link": null, "link": null,
@ -8203,7 +8189,7 @@ exports[`regression tests > key 2 selects rectangle tool > [end of test] undo st
"strokeWidth": 2, "strokeWidth": 2,
"type": "rectangle", "type": "rectangle",
"version": 3, "version": 3,
"width": 10, "width": 30,
"x": 10, "x": 10,
"y": 10, "y": 10,
}, },
@ -8254,7 +8240,6 @@ exports[`regression tests > key 3 selects diamond tool > [end of test] appState
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -8369,7 +8354,7 @@ exports[`regression tests > key 3 selects diamond tool > [end of test] undo stac
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
"height": 10, "height": 30,
"index": "a0", "index": "a0",
"isDeleted": false, "isDeleted": false,
"link": null, "link": null,
@ -8382,7 +8367,7 @@ exports[`regression tests > key 3 selects diamond tool > [end of test] undo stac
"strokeWidth": 2, "strokeWidth": 2,
"type": "diamond", "type": "diamond",
"version": 3, "version": 3,
"width": 10, "width": 30,
"x": 10, "x": 10,
"y": 10, "y": 10,
}, },
@ -8433,7 +8418,6 @@ exports[`regression tests > key 4 selects ellipse tool > [end of test] appState
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -8548,7 +8532,7 @@ exports[`regression tests > key 4 selects ellipse tool > [end of test] undo stac
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
"height": 10, "height": 30,
"index": "a0", "index": "a0",
"isDeleted": false, "isDeleted": false,
"link": null, "link": null,
@ -8561,7 +8545,7 @@ exports[`regression tests > key 4 selects ellipse tool > [end of test] undo stac
"strokeWidth": 2, "strokeWidth": 2,
"type": "ellipse", "type": "ellipse",
"version": 3, "version": 3,
"width": 10, "width": 30,
"x": 10, "x": 10,
"y": 10, "y": 10,
}, },
@ -8612,7 +8596,6 @@ exports[`regression tests > key 5 selects arrow tool > [end of test] appState 1`
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -8679,6 +8662,7 @@ exports[`regression tests > key 5 selects arrow tool > [end of test] appState 1`
"endBindingElement": "keep", "endBindingElement": "keep",
"hoverPointIndex": -1, "hoverPointIndex": -1,
"isDragging": false, "isDragging": false,
"isEditing": false,
"lastUncommittedPoint": null, "lastUncommittedPoint": null,
"pointerDownState": { "pointerDownState": {
"lastClickedIsEndPoint": false, "lastClickedIsEndPoint": false,
@ -8739,10 +8723,12 @@ exports[`regression tests > key 5 selects arrow tool > [end of test] undo stack
"id0": true, "id0": true,
}, },
"selectedLinearElementId": "id0", "selectedLinearElementId": "id0",
"selectedLinearElementIsEditing": false,
}, },
"inserted": { "inserted": {
"selectedElementIds": {}, "selectedElementIds": {},
"selectedLinearElementId": null, "selectedLinearElementId": null,
"selectedLinearElementIsEditing": null,
}, },
}, },
}, },
@ -8761,7 +8747,7 @@ exports[`regression tests > key 5 selects arrow tool > [end of test] undo stack
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
"height": 10, "height": 30,
"index": "a0", "index": "a0",
"isDeleted": false, "isDeleted": false,
"lastCommittedPoint": null, "lastCommittedPoint": null,
@ -8774,8 +8760,8 @@ exports[`regression tests > key 5 selects arrow tool > [end of test] undo stack
0, 0,
], ],
[ [
10, 30,
10, 30,
], ],
], ],
"roughness": 1, "roughness": 1,
@ -8789,7 +8775,7 @@ exports[`regression tests > key 5 selects arrow tool > [end of test] undo stack
"strokeWidth": 2, "strokeWidth": 2,
"type": "arrow", "type": "arrow",
"version": 4, "version": 4,
"width": 10, "width": 30,
"x": 10, "x": 10,
"y": 10, "y": 10,
}, },
@ -8840,7 +8826,6 @@ exports[`regression tests > key 6 selects line tool > [end of test] appState 1`]
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -8904,6 +8889,7 @@ exports[`regression tests > key 6 selects line tool > [end of test] appState 1`]
"endBindingElement": "keep", "endBindingElement": "keep",
"hoverPointIndex": -1, "hoverPointIndex": -1,
"isDragging": false, "isDragging": false,
"isEditing": false,
"lastUncommittedPoint": null, "lastUncommittedPoint": null,
"pointerDownState": { "pointerDownState": {
"lastClickedIsEndPoint": false, "lastClickedIsEndPoint": false,
@ -8964,10 +8950,12 @@ exports[`regression tests > key 6 selects line tool > [end of test] undo stack 1
"id0": true, "id0": true,
}, },
"selectedLinearElementId": "id0", "selectedLinearElementId": "id0",
"selectedLinearElementIsEditing": false,
}, },
"inserted": { "inserted": {
"selectedElementIds": {}, "selectedElementIds": {},
"selectedLinearElementId": null, "selectedLinearElementId": null,
"selectedLinearElementIsEditing": null,
}, },
}, },
}, },
@ -8985,7 +8973,7 @@ exports[`regression tests > key 6 selects line tool > [end of test] undo stack 1
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
"height": 10, "height": 30,
"index": "a0", "index": "a0",
"isDeleted": false, "isDeleted": false,
"lastCommittedPoint": null, "lastCommittedPoint": null,
@ -8998,8 +8986,8 @@ exports[`regression tests > key 6 selects line tool > [end of test] undo stack 1
0, 0,
], ],
[ [
10, 30,
10, 30,
], ],
], ],
"polygon": false, "polygon": false,
@ -9012,7 +9000,7 @@ exports[`regression tests > key 6 selects line tool > [end of test] undo stack 1
"strokeWidth": 2, "strokeWidth": 2,
"type": "line", "type": "line",
"version": 4, "version": 4,
"width": 10, "width": 30,
"x": 10, "x": 10,
"y": 10, "y": 10,
}, },
@ -9063,7 +9051,6 @@ exports[`regression tests > key 7 selects freedraw tool > [end of test] appState
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -9170,12 +9157,12 @@ exports[`regression tests > key 7 selects freedraw tool > [end of test] undo sta
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
"height": 10, "height": 30,
"index": "a0", "index": "a0",
"isDeleted": false, "isDeleted": false,
"lastCommittedPoint": [ "lastCommittedPoint": [
10, 30,
10, 30,
], ],
"link": null, "link": null,
"locked": false, "locked": false,
@ -9186,12 +9173,12 @@ exports[`regression tests > key 7 selects freedraw tool > [end of test] undo sta
0, 0,
], ],
[ [
10, 30,
10, 30,
], ],
[ [
10, 30,
10, 30,
], ],
], ],
"pressures": [ "pressures": [
@ -9207,7 +9194,7 @@ exports[`regression tests > key 7 selects freedraw tool > [end of test] undo sta
"strokeWidth": 2, "strokeWidth": 2,
"type": "freedraw", "type": "freedraw",
"version": 4, "version": 4,
"width": 10, "width": 30,
"x": 10, "x": 10,
"y": 10, "y": 10,
}, },
@ -9258,7 +9245,6 @@ exports[`regression tests > key a selects arrow tool > [end of test] appState 1`
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -9325,6 +9311,7 @@ exports[`regression tests > key a selects arrow tool > [end of test] appState 1`
"endBindingElement": "keep", "endBindingElement": "keep",
"hoverPointIndex": -1, "hoverPointIndex": -1,
"isDragging": false, "isDragging": false,
"isEditing": false,
"lastUncommittedPoint": null, "lastUncommittedPoint": null,
"pointerDownState": { "pointerDownState": {
"lastClickedIsEndPoint": false, "lastClickedIsEndPoint": false,
@ -9385,10 +9372,12 @@ exports[`regression tests > key a selects arrow tool > [end of test] undo stack
"id0": true, "id0": true,
}, },
"selectedLinearElementId": "id0", "selectedLinearElementId": "id0",
"selectedLinearElementIsEditing": false,
}, },
"inserted": { "inserted": {
"selectedElementIds": {}, "selectedElementIds": {},
"selectedLinearElementId": null, "selectedLinearElementId": null,
"selectedLinearElementIsEditing": null,
}, },
}, },
}, },
@ -9407,7 +9396,7 @@ exports[`regression tests > key a selects arrow tool > [end of test] undo stack
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
"height": 10, "height": 30,
"index": "a0", "index": "a0",
"isDeleted": false, "isDeleted": false,
"lastCommittedPoint": null, "lastCommittedPoint": null,
@ -9420,8 +9409,8 @@ exports[`regression tests > key a selects arrow tool > [end of test] undo stack
0, 0,
], ],
[ [
10, 30,
10, 30,
], ],
], ],
"roughness": 1, "roughness": 1,
@ -9435,7 +9424,7 @@ exports[`regression tests > key a selects arrow tool > [end of test] undo stack
"strokeWidth": 2, "strokeWidth": 2,
"type": "arrow", "type": "arrow",
"version": 4, "version": 4,
"width": 10, "width": 30,
"x": 10, "x": 10,
"y": 10, "y": 10,
}, },
@ -9486,7 +9475,6 @@ exports[`regression tests > key d selects diamond tool > [end of test] appState
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -9601,7 +9589,7 @@ exports[`regression tests > key d selects diamond tool > [end of test] undo stac
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
"height": 10, "height": 30,
"index": "a0", "index": "a0",
"isDeleted": false, "isDeleted": false,
"link": null, "link": null,
@ -9614,7 +9602,7 @@ exports[`regression tests > key d selects diamond tool > [end of test] undo stac
"strokeWidth": 2, "strokeWidth": 2,
"type": "diamond", "type": "diamond",
"version": 3, "version": 3,
"width": 10, "width": 30,
"x": 10, "x": 10,
"y": 10, "y": 10,
}, },
@ -9665,7 +9653,6 @@ exports[`regression tests > key l selects line tool > [end of test] appState 1`]
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -9729,6 +9716,7 @@ exports[`regression tests > key l selects line tool > [end of test] appState 1`]
"endBindingElement": "keep", "endBindingElement": "keep",
"hoverPointIndex": -1, "hoverPointIndex": -1,
"isDragging": false, "isDragging": false,
"isEditing": false,
"lastUncommittedPoint": null, "lastUncommittedPoint": null,
"pointerDownState": { "pointerDownState": {
"lastClickedIsEndPoint": false, "lastClickedIsEndPoint": false,
@ -9789,10 +9777,12 @@ exports[`regression tests > key l selects line tool > [end of test] undo stack 1
"id0": true, "id0": true,
}, },
"selectedLinearElementId": "id0", "selectedLinearElementId": "id0",
"selectedLinearElementIsEditing": false,
}, },
"inserted": { "inserted": {
"selectedElementIds": {}, "selectedElementIds": {},
"selectedLinearElementId": null, "selectedLinearElementId": null,
"selectedLinearElementIsEditing": null,
}, },
}, },
}, },
@ -9810,7 +9800,7 @@ exports[`regression tests > key l selects line tool > [end of test] undo stack 1
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
"height": 10, "height": 30,
"index": "a0", "index": "a0",
"isDeleted": false, "isDeleted": false,
"lastCommittedPoint": null, "lastCommittedPoint": null,
@ -9823,8 +9813,8 @@ exports[`regression tests > key l selects line tool > [end of test] undo stack 1
0, 0,
], ],
[ [
10, 30,
10, 30,
], ],
], ],
"polygon": false, "polygon": false,
@ -9837,7 +9827,7 @@ exports[`regression tests > key l selects line tool > [end of test] undo stack 1
"strokeWidth": 2, "strokeWidth": 2,
"type": "line", "type": "line",
"version": 4, "version": 4,
"width": 10, "width": 30,
"x": 10, "x": 10,
"y": 10, "y": 10,
}, },
@ -9888,7 +9878,6 @@ exports[`regression tests > key o selects ellipse tool > [end of test] appState
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -10003,7 +9992,7 @@ exports[`regression tests > key o selects ellipse tool > [end of test] undo stac
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
"height": 10, "height": 30,
"index": "a0", "index": "a0",
"isDeleted": false, "isDeleted": false,
"link": null, "link": null,
@ -10016,7 +10005,7 @@ exports[`regression tests > key o selects ellipse tool > [end of test] undo stac
"strokeWidth": 2, "strokeWidth": 2,
"type": "ellipse", "type": "ellipse",
"version": 3, "version": 3,
"width": 10, "width": 30,
"x": 10, "x": 10,
"y": 10, "y": 10,
}, },
@ -10067,7 +10056,6 @@ exports[`regression tests > key p selects freedraw tool > [end of test] appState
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -10174,12 +10162,12 @@ exports[`regression tests > key p selects freedraw tool > [end of test] undo sta
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
"height": 10, "height": 30,
"index": "a0", "index": "a0",
"isDeleted": false, "isDeleted": false,
"lastCommittedPoint": [ "lastCommittedPoint": [
10, 30,
10, 30,
], ],
"link": null, "link": null,
"locked": false, "locked": false,
@ -10190,12 +10178,12 @@ exports[`regression tests > key p selects freedraw tool > [end of test] undo sta
0, 0,
], ],
[ [
10, 30,
10, 30,
], ],
[ [
10, 30,
10, 30,
], ],
], ],
"pressures": [ "pressures": [
@ -10211,7 +10199,7 @@ exports[`regression tests > key p selects freedraw tool > [end of test] undo sta
"strokeWidth": 2, "strokeWidth": 2,
"type": "freedraw", "type": "freedraw",
"version": 4, "version": 4,
"width": 10, "width": 30,
"x": 10, "x": 10,
"y": 10, "y": 10,
}, },
@ -10262,7 +10250,6 @@ exports[`regression tests > key r selects rectangle tool > [end of test] appStat
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -10377,7 +10364,7 @@ exports[`regression tests > key r selects rectangle tool > [end of test] undo st
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
"height": 10, "height": 30,
"index": "a0", "index": "a0",
"isDeleted": false, "isDeleted": false,
"link": null, "link": null,
@ -10390,7 +10377,7 @@ exports[`regression tests > key r selects rectangle tool > [end of test] undo st
"strokeWidth": 2, "strokeWidth": 2,
"type": "rectangle", "type": "rectangle",
"version": 3, "version": 3,
"width": 10, "width": 30,
"x": 10, "x": 10,
"y": 10, "y": 10,
}, },
@ -10441,7 +10428,6 @@ exports[`regression tests > make a group and duplicate it > [end of test] appSta
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -10971,7 +10957,6 @@ exports[`regression tests > noop interaction after undo shouldn't create history
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -11250,7 +11235,6 @@ exports[`regression tests > pinch-to-zoom works > [end of test] appState 1`] = `
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -11372,7 +11356,6 @@ exports[`regression tests > shift click on selected element should deselect it o
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -11571,7 +11554,6 @@ exports[`regression tests > shift-click to multiselect, then drag > [end of test
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -11889,7 +11871,6 @@ exports[`regression tests > should group elements and ungroup them > [end of tes
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -12317,7 +12298,6 @@ exports[`regression tests > single-clicking on a subgroup of a selected group sh
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -12956,7 +12936,6 @@ exports[`regression tests > spacebar + drag scrolls the canvas > [end of test] a
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -13081,7 +13060,6 @@ exports[`regression tests > supports nested groups > [end of test] appState 1`]
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": "id11", "editingGroupId": "id11",
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -13711,7 +13689,6 @@ exports[`regression tests > switches from group of selected elements to another
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -14049,7 +14026,6 @@ exports[`regression tests > switches selected element on pointer down > [end of
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -14312,7 +14288,6 @@ exports[`regression tests > two-finger scroll works > [end of test] appState 1`]
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -14434,7 +14409,6 @@ exports[`regression tests > undo/redo drawing an element > [end of test] appStat
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -14527,9 +14501,11 @@ exports[`regression tests > undo/redo drawing an element > [end of test] redo st
"delta": Delta { "delta": Delta {
"deleted": { "deleted": {
"selectedLinearElementId": null, "selectedLinearElementId": null,
"selectedLinearElementIsEditing": null,
}, },
"inserted": { "inserted": {
"selectedLinearElementId": "id6", "selectedLinearElementId": "id6",
"selectedLinearElementIsEditing": false,
}, },
}, },
}, },
@ -14822,7 +14798,6 @@ exports[`regression tests > updates fontSize & fontFamily appState > [end of tes
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,
@ -14944,7 +14919,6 @@ exports[`regression tests > zoom hotkeys > [end of test] appState 1`] = `
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,

View File

@ -1073,7 +1073,7 @@ describe("history", () => {
expect(API.getUndoStack().length).toBe(6); expect(API.getUndoStack().length).toBe(6);
expect(API.getRedoStack().length).toBe(0); expect(API.getRedoStack().length).toBe(0);
expect(assertSelectedElements(h.elements[0])); expect(assertSelectedElements(h.elements[0]));
expect(h.state.editingLinearElement).toBeNull(); expect(h.state.selectedLinearElement?.isEditing ?? false).toBe(false);
expect(h.state.selectedLinearElement).not.toBeNull(); expect(h.state.selectedLinearElement).not.toBeNull();
expect(h.elements).toEqual([ expect(h.elements).toEqual([
expect.objectContaining({ expect.objectContaining({
@ -1090,7 +1090,7 @@ describe("history", () => {
expect(API.getUndoStack().length).toBe(5); expect(API.getUndoStack().length).toBe(5);
expect(API.getRedoStack().length).toBe(1); expect(API.getRedoStack().length).toBe(1);
expect(assertSelectedElements(h.elements[0])); expect(assertSelectedElements(h.elements[0]));
expect(h.state.editingLinearElement?.elementId).toBe(h.elements[0].id); expect(h.state.selectedLinearElement?.isEditing).toBe(true);
expect(h.state.selectedLinearElement?.elementId).toBe(h.elements[0].id); expect(h.state.selectedLinearElement?.elementId).toBe(h.elements[0].id);
expect(h.elements).toEqual([ expect(h.elements).toEqual([
expect.objectContaining({ expect.objectContaining({
@ -1114,7 +1114,7 @@ describe("history", () => {
expect(API.getUndoStack().length).toBe(4); expect(API.getUndoStack().length).toBe(4);
expect(API.getRedoStack().length).toBe(2); expect(API.getRedoStack().length).toBe(2);
expect(assertSelectedElements(h.elements[0])); expect(assertSelectedElements(h.elements[0]));
expect(h.state.editingLinearElement?.elementId).toBe(h.elements[0].id); expect(h.state.selectedLinearElement?.isEditing).toBe(true);
expect(h.state.selectedLinearElement?.elementId).toBe(h.elements[0].id); expect(h.state.selectedLinearElement?.elementId).toBe(h.elements[0].id);
expect(h.elements).toEqual([ expect(h.elements).toEqual([
expect.objectContaining({ expect.objectContaining({
@ -1131,7 +1131,7 @@ describe("history", () => {
expect(API.getUndoStack().length).toBe(3); expect(API.getUndoStack().length).toBe(3);
expect(API.getRedoStack().length).toBe(3); expect(API.getRedoStack().length).toBe(3);
expect(assertSelectedElements(h.elements[0])); expect(assertSelectedElements(h.elements[0]));
expect(h.state.editingLinearElement).toBeNull(); // undo `open editor` expect(h.state.selectedLinearElement?.isEditing ?? false).toBe(false); // undo `open editor`
expect(h.state.selectedLinearElement?.elementId).toBe(h.elements[0].id); expect(h.state.selectedLinearElement?.elementId).toBe(h.elements[0].id);
expect(h.elements).toEqual([ expect(h.elements).toEqual([
expect.objectContaining({ expect.objectContaining({
@ -1148,7 +1148,7 @@ describe("history", () => {
expect(API.getUndoStack().length).toBe(2); expect(API.getUndoStack().length).toBe(2);
expect(API.getRedoStack().length).toBe(4); expect(API.getRedoStack().length).toBe(4);
expect(assertSelectedElements(h.elements[0])); expect(assertSelectedElements(h.elements[0]));
expect(h.state.editingLinearElement).toBeNull(); expect(h.state.selectedLinearElement?.isEditing ?? false).toBe(false);
expect(h.state.selectedLinearElement).toBeNull(); // undo `actionFinalize` expect(h.state.selectedLinearElement).toBeNull(); // undo `actionFinalize`
expect(h.elements).toEqual([ expect(h.elements).toEqual([
expect.objectContaining({ expect.objectContaining({
@ -1165,7 +1165,7 @@ describe("history", () => {
expect(API.getUndoStack().length).toBe(1); expect(API.getUndoStack().length).toBe(1);
expect(API.getRedoStack().length).toBe(5); expect(API.getRedoStack().length).toBe(5);
expect(assertSelectedElements(h.elements[0])); expect(assertSelectedElements(h.elements[0]));
expect(h.state.editingLinearElement).toBeNull(); expect(h.state.selectedLinearElement?.isEditing ?? false).toBe(false);
expect(h.state.selectedLinearElement).toBeNull(); expect(h.state.selectedLinearElement).toBeNull();
expect(h.elements).toEqual([ expect(h.elements).toEqual([
expect.objectContaining({ expect.objectContaining({
@ -1181,7 +1181,7 @@ describe("history", () => {
expect(API.getUndoStack().length).toBe(0); expect(API.getUndoStack().length).toBe(0);
expect(API.getRedoStack().length).toBe(6); expect(API.getRedoStack().length).toBe(6);
expect(API.getSelectedElements().length).toBe(0); expect(API.getSelectedElements().length).toBe(0);
expect(h.state.editingLinearElement).toBeNull(); expect(h.state.selectedLinearElement?.isEditing ?? false).toBe(false);
expect(h.state.selectedLinearElement).toBeNull(); expect(h.state.selectedLinearElement).toBeNull();
expect(h.elements).toEqual([ expect(h.elements).toEqual([
expect.objectContaining({ expect.objectContaining({
@ -1197,7 +1197,7 @@ describe("history", () => {
expect(API.getUndoStack().length).toBe(1); expect(API.getUndoStack().length).toBe(1);
expect(API.getRedoStack().length).toBe(5); expect(API.getRedoStack().length).toBe(5);
expect(assertSelectedElements(h.elements[0])); expect(assertSelectedElements(h.elements[0]));
expect(h.state.editingLinearElement).toBeNull(); expect(h.state.selectedLinearElement?.isEditing ?? false).toBe(false);
expect(h.state.selectedLinearElement).toBeNull(); expect(h.state.selectedLinearElement).toBeNull();
expect(h.elements).toEqual([ expect(h.elements).toEqual([
expect.objectContaining({ expect.objectContaining({
@ -1213,7 +1213,7 @@ describe("history", () => {
expect(API.getUndoStack().length).toBe(2); expect(API.getUndoStack().length).toBe(2);
expect(API.getRedoStack().length).toBe(4); expect(API.getRedoStack().length).toBe(4);
expect(assertSelectedElements(h.elements[0])); expect(assertSelectedElements(h.elements[0]));
expect(h.state.editingLinearElement).toBeNull(); expect(h.state.selectedLinearElement?.isEditing ?? false).toBe(false);
expect(h.state.selectedLinearElement).toBeNull(); // undo `actionFinalize` expect(h.state.selectedLinearElement).toBeNull(); // undo `actionFinalize`
expect(h.elements).toEqual([ expect(h.elements).toEqual([
expect.objectContaining({ expect.objectContaining({
@ -1230,7 +1230,7 @@ describe("history", () => {
expect(API.getUndoStack().length).toBe(3); expect(API.getUndoStack().length).toBe(3);
expect(API.getRedoStack().length).toBe(3); expect(API.getRedoStack().length).toBe(3);
expect(assertSelectedElements(h.elements[0])); expect(assertSelectedElements(h.elements[0]));
expect(h.state.editingLinearElement).toBeNull(); // undo `open editor` expect(h.state.selectedLinearElement?.isEditing ?? false).toBe(false); // undo `open editor`
expect(h.state.selectedLinearElement?.elementId).toBe(h.elements[0].id); expect(h.state.selectedLinearElement?.elementId).toBe(h.elements[0].id);
expect(h.elements).toEqual([ expect(h.elements).toEqual([
expect.objectContaining({ expect.objectContaining({
@ -1247,7 +1247,7 @@ describe("history", () => {
expect(API.getUndoStack().length).toBe(4); expect(API.getUndoStack().length).toBe(4);
expect(API.getRedoStack().length).toBe(2); expect(API.getRedoStack().length).toBe(2);
expect(assertSelectedElements(h.elements[0])); expect(assertSelectedElements(h.elements[0]));
expect(h.state.editingLinearElement?.elementId).toBe(h.elements[0].id); expect(h.state.selectedLinearElement?.isEditing).toBe(true);
expect(h.state.selectedLinearElement?.elementId).toBe(h.elements[0].id); expect(h.state.selectedLinearElement?.elementId).toBe(h.elements[0].id);
expect(h.elements).toEqual([ expect(h.elements).toEqual([
expect.objectContaining({ expect.objectContaining({
@ -1264,7 +1264,7 @@ describe("history", () => {
expect(API.getUndoStack().length).toBe(5); expect(API.getUndoStack().length).toBe(5);
expect(API.getRedoStack().length).toBe(1); expect(API.getRedoStack().length).toBe(1);
expect(assertSelectedElements(h.elements[0])); expect(assertSelectedElements(h.elements[0]));
expect(h.state.editingLinearElement?.elementId).toBe(h.elements[0].id); expect(h.state.selectedLinearElement?.isEditing).toBe(true);
expect(h.state.selectedLinearElement?.elementId).toBe(h.elements[0].id); expect(h.state.selectedLinearElement?.elementId).toBe(h.elements[0].id);
expect(h.elements).toEqual([ expect(h.elements).toEqual([
expect.objectContaining({ expect.objectContaining({
@ -1281,7 +1281,7 @@ describe("history", () => {
expect(API.getUndoStack().length).toBe(6); expect(API.getUndoStack().length).toBe(6);
expect(API.getRedoStack().length).toBe(0); expect(API.getRedoStack().length).toBe(0);
expect(assertSelectedElements(h.elements[0])); expect(assertSelectedElements(h.elements[0]));
expect(h.state.editingLinearElement).toBeNull(); expect(h.state.selectedLinearElement?.isEditing ?? false).toBe(false);
expect(h.state.selectedLinearElement).not.toBeNull(); expect(h.state.selectedLinearElement).not.toBeNull();
expect(h.elements).toEqual([ expect(h.elements).toEqual([
expect.objectContaining({ expect.objectContaining({
@ -3029,8 +3029,8 @@ describe("history", () => {
expect(API.getUndoStack().length).toBe(4); expect(API.getUndoStack().length).toBe(4);
expect(API.getRedoStack().length).toBe(0); expect(API.getRedoStack().length).toBe(0);
expect(h.state.editingLinearElement).toBeNull();
expect(h.state.selectedLinearElement).not.toBeNull(); expect(h.state.selectedLinearElement).not.toBeNull();
expect(h.state.selectedLinearElement?.isEditing).toBe(false);
// Simulate remote update // Simulate remote update
API.updateScene({ API.updateScene({
@ -3043,16 +3043,16 @@ describe("history", () => {
}); });
Keyboard.undo(); Keyboard.undo();
expect(API.getUndoStack().length).toBe(1); expect(API.getUndoStack().length).toBe(3);
expect(API.getRedoStack().length).toBe(3); expect(API.getRedoStack().length).toBe(1);
expect(h.state.editingLinearElement).toBeNull(); expect(h.state.selectedLinearElement).not.toBeNull();
expect(h.state.selectedLinearElement).toBeNull(); expect(h.state.selectedLinearElement?.isEditing).toBe(true);
Keyboard.redo(); Keyboard.redo();
expect(API.getUndoStack().length).toBe(4); expect(API.getUndoStack().length).toBe(4);
expect(API.getRedoStack().length).toBe(0); expect(API.getRedoStack().length).toBe(0);
expect(h.state.editingLinearElement).toBeNull(); expect(h.state.selectedLinearElement).not.toBeNull();
expect(h.state.selectedLinearElement).toBeNull(); expect(h.state.selectedLinearElement?.isEditing ?? false).toBe(false);
}); });
it("should iterate through the history when z-index changes do not produce visible change and we synced changed indices", async () => { it("should iterate through the history when z-index changes do not produce visible change and we synced changed indices", async () => {

View File

@ -150,7 +150,7 @@ describe("regression tests", () => {
expect(h.state.activeTool.type).toBe(shape); expect(h.state.activeTool.type).toBe(shape);
mouse.down(10, 10); mouse.down(10, 10);
mouse.up(10, 10); mouse.up(30, 30);
if (shouldSelect) { if (shouldSelect) {
expect(API.getSelectedElement().type).toBe(shape); expect(API.getSelectedElement().type).toBe(shape);

View File

@ -214,7 +214,6 @@ export type InteractiveCanvasAppState = Readonly<
_CommonCanvasAppState & { _CommonCanvasAppState & {
// renderInteractiveScene // renderInteractiveScene
activeEmbeddable: AppState["activeEmbeddable"]; activeEmbeddable: AppState["activeEmbeddable"];
editingLinearElement: AppState["editingLinearElement"];
selectionElement: AppState["selectionElement"]; selectionElement: AppState["selectionElement"];
selectedGroupIds: AppState["selectedGroupIds"]; selectedGroupIds: AppState["selectedGroupIds"];
selectedLinearElement: AppState["selectedLinearElement"]; selectedLinearElement: AppState["selectedLinearElement"];
@ -250,10 +249,8 @@ export type ObservedElementsAppState = {
editingGroupId: AppState["editingGroupId"]; editingGroupId: AppState["editingGroupId"];
selectedElementIds: AppState["selectedElementIds"]; selectedElementIds: AppState["selectedElementIds"];
selectedGroupIds: AppState["selectedGroupIds"]; selectedGroupIds: AppState["selectedGroupIds"];
// Avoiding storing whole instance, as it could lead into state incosistencies, empty undos/redos and etc.
editingLinearElementId: LinearElementEditor["elementId"] | null;
// Right now it's coupled to `editingLinearElement`, ideally it should not be really needed as we already have selectedElementIds & editingLinearElementId
selectedLinearElementId: LinearElementEditor["elementId"] | null; selectedLinearElementId: LinearElementEditor["elementId"] | null;
selectedLinearElementIsEditing: boolean | null;
croppingElementId: AppState["croppingElementId"]; croppingElementId: AppState["croppingElementId"];
lockedMultiSelections: AppState["lockedMultiSelections"]; lockedMultiSelections: AppState["lockedMultiSelections"];
activeLockedId: AppState["activeLockedId"]; activeLockedId: AppState["activeLockedId"];
@ -308,7 +305,6 @@ export interface AppState {
* set when a new text is created or when an existing text is being edited * set when a new text is created or when an existing text is being edited
*/ */
editingTextElement: NonDeletedExcalidrawElement | null; editingTextElement: NonDeletedExcalidrawElement | null;
editingLinearElement: LinearElementEditor | null;
activeTool: { activeTool: {
/** /**
* indicates a previous tool we should revert back to if we deselect the * indicates a previous tool we should revert back to if we deselect the

View File

@ -704,7 +704,7 @@ describe("textWysiwyg", () => {
rectangle.x + rectangle.width / 2, rectangle.x + rectangle.width / 2,
rectangle.y + rectangle.height / 2, rectangle.y + rectangle.height / 2,
); );
expect(h.elements.length).toBe(3); expect(h.elements.length).toBe(2);
text = h.elements[1] as ExcalidrawTextElementWithContainer; text = h.elements[1] as ExcalidrawTextElementWithContainer;
expect(text.type).toBe("text"); expect(text.type).toBe("text");
@ -1198,7 +1198,7 @@ describe("textWysiwyg", () => {
updateTextEditor(editor, " "); updateTextEditor(editor, " ");
Keyboard.exitTextEditor(editor); Keyboard.exitTextEditor(editor);
expect(rectangle.boundElements).toStrictEqual([]); expect(rectangle.boundElements).toStrictEqual([]);
expect(h.elements[1].isDeleted).toBe(true); expect(h.elements[1]).toBeUndefined();
}); });
it("should restore original container height and clear cache once text is unbind", async () => { it("should restore original container height and clear cache once text is unbind", async () => {

View File

@ -1,6 +1,6 @@
{ {
"name": "@excalidraw/math", "name": "@excalidraw/math",
"version": "0.1.0", "version": "0.18.0",
"type": "module", "type": "module",
"types": "./dist/types/math/src/index.d.ts", "types": "./dist/types/math/src/index.d.ts",
"main": "./dist/prod/index.js", "main": "./dist/prod/index.js",
@ -13,7 +13,10 @@
"default": "./dist/prod/index.js" "default": "./dist/prod/index.js"
}, },
"./*": { "./*": {
"types": "./dist/types/math/src/*.d.ts" "types": "./dist/types/math/src/*.d.ts",
"development": "./dist/dev/index.js",
"production": "./dist/prod/index.js",
"default": "./dist/prod/index.js"
} }
}, },
"files": [ "files": [
@ -56,5 +59,8 @@
"scripts": { "scripts": {
"gen:types": "rimraf types && tsc", "gen:types": "rimraf types && tsc",
"build:esm": "rimraf dist && node ../../scripts/buildBase.js && yarn gen:types" "build:esm": "rimraf dist && node ../../scripts/buildBase.js && yarn gen:types"
},
"dependencies": {
"@excalidraw/common": "0.18.0"
} }
} }

View File

@ -34,7 +34,6 @@ exports[`exportToSvg > with default arguments 1`] = `
"defaultSidebarDockedPreference": false, "defaultSidebarDockedPreference": false,
"editingFrame": null, "editingFrame": null,
"editingGroupId": null, "editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null, "editingTextElement": null,
"elementsToHighlight": null, "elementsToHighlight": null,
"errorMessage": null, "errorMessage": null,

View File

@ -1,71 +0,0 @@
const { exec, execSync } = require("child_process");
const fs = require("fs");
const core = require("@actions/core");
const excalidrawDir = `${__dirname}/../packages/excalidraw`;
const excalidrawPackage = `${excalidrawDir}/package.json`;
const pkg = require(excalidrawPackage);
const isPreview = process.argv.slice(2)[0] === "preview";
const getShortCommitHash = () => {
return execSync("git rev-parse --short HEAD").toString().trim();
};
const publish = () => {
const tag = isPreview ? "preview" : "next";
try {
execSync(`yarn --frozen-lockfile`);
execSync(`yarn run build:esm`, { cwd: excalidrawDir });
execSync(`yarn --cwd ${excalidrawDir} publish --tag ${tag}`);
console.info(`Published ${pkg.name}@${tag}🎉`);
core.setOutput(
"result",
`**Preview version has been shipped** :rocket:
You can use [@excalidraw/excalidraw@${pkg.version}](https://www.npmjs.com/package/@excalidraw/excalidraw/v/${pkg.version}) for testing!`,
);
} catch (error) {
core.setOutput("result", "package couldn't be published :warning:!");
console.error(error);
process.exit(1);
}
};
// get files changed between prev and head commit
exec(`git diff --name-only HEAD^ HEAD`, async (error, stdout, stderr) => {
if (error || stderr) {
console.error(error);
core.setOutput("result", ":warning: Package couldn't be published!");
process.exit(1);
}
const changedFiles = stdout.trim().split("\n");
const excalidrawPackageFiles = changedFiles.filter((file) => {
return (
file.indexOf("packages/excalidraw") >= 0 ||
file.indexOf("buildPackage.js") > 0
);
});
if (!excalidrawPackageFiles.length) {
console.info("Skipping release as no valid diff found");
core.setOutput("result", "Skipping release as no valid diff found");
process.exit(0);
}
// update package.json
let version = `${pkg.version}-${getShortCommitHash()}`;
// update readme
if (isPreview) {
// use pullNumber-commithash as the version for preview
const pullRequestNumber = process.argv.slice(3)[0];
version = `${pkg.version}-${pullRequestNumber}-${getShortCommitHash()}`;
}
pkg.version = version;
fs.writeFileSync(excalidrawPackage, JSON.stringify(pkg, null, 2), "utf8");
console.info("Publish in progress...");
publish();
});

View File

@ -11,12 +11,9 @@ const getConfig = (outdir) => ({
entryNames: "[name]", entryNames: "[name]",
assetNames: "[dir]/[name]", assetNames: "[dir]/[name]",
alias: { alias: {
"@excalidraw/common": path.resolve(__dirname, "../packages/common/src"),
"@excalidraw/element": path.resolve(__dirname, "../packages/element/src"),
"@excalidraw/excalidraw": path.resolve(__dirname, "../packages/excalidraw"),
"@excalidraw/math": path.resolve(__dirname, "../packages/math/src"),
"@excalidraw/utils": path.resolve(__dirname, "../packages/utils/src"), "@excalidraw/utils": path.resolve(__dirname, "../packages/utils/src"),
}, },
external: ["@excalidraw/common", "@excalidraw/element", "@excalidraw/math"],
}); });
function buildDev(config) { function buildDev(config) {

View File

@ -28,12 +28,9 @@ const getConfig = (outdir) => ({
assetNames: "[dir]/[name]", assetNames: "[dir]/[name]",
chunkNames: "[dir]/[name]-[hash]", chunkNames: "[dir]/[name]-[hash]",
alias: { alias: {
"@excalidraw/common": path.resolve(__dirname, "../packages/common/src"),
"@excalidraw/element": path.resolve(__dirname, "../packages/element/src"),
"@excalidraw/excalidraw": path.resolve(__dirname, "../packages/excalidraw"),
"@excalidraw/math": path.resolve(__dirname, "../packages/math/src"),
"@excalidraw/utils": path.resolve(__dirname, "../packages/utils/src"), "@excalidraw/utils": path.resolve(__dirname, "../packages/utils/src"),
}, },
external: ["@excalidraw/common", "@excalidraw/element", "@excalidraw/math"],
loader: { loader: {
".woff2": "file", ".woff2": "file",
}, },

View File

@ -1,38 +0,0 @@
const fs = require("fs");
const util = require("util");
const exec = util.promisify(require("child_process").exec);
const updateChangelog = require("./updateChangelog");
const excalidrawDir = `${__dirname}/../packages/excalidraw/`;
const excalidrawPackage = `${excalidrawDir}/package.json`;
const updatePackageVersion = (nextVersion) => {
const pkg = require(excalidrawPackage);
pkg.version = nextVersion;
const content = `${JSON.stringify(pkg, null, 2)}\n`;
fs.writeFileSync(excalidrawPackage, content, "utf-8");
};
const prerelease = async (nextVersion) => {
try {
await updateChangelog(nextVersion);
updatePackageVersion(nextVersion);
await exec(`git add -u`);
await exec(
`git commit -m "docs: release @excalidraw/excalidraw@${nextVersion} 🎉"`,
);
console.info("Done!");
} catch (error) {
console.error(error);
process.exit(1);
}
};
const nextVersion = process.argv.slice(2)[0];
if (!nextVersion) {
console.error("Pass the next version to release!");
process.exit(1);
}
prerelease(nextVersion);

View File

@ -1,28 +1,239 @@
const fs = require("fs");
const path = require("path");
const { execSync } = require("child_process"); const { execSync } = require("child_process");
const excalidrawDir = `${__dirname}/../packages/excalidraw`; const updateChangelog = require("./updateChangelog");
const excalidrawPackage = `${excalidrawDir}/package.json`;
const pkg = require(excalidrawPackage);
const publish = () => { // skipping utils for now, as it has independent release process
try { const PACKAGES = ["common", "math", "element", "excalidraw"];
console.info("Installing the dependencies in root folder..."); const PACKAGES_DIR = path.resolve(__dirname, "../packages");
execSync(`yarn --frozen-lockfile`);
console.info("Installing the dependencies in excalidraw directory..."); /**
execSync(`yarn --frozen-lockfile`, { cwd: excalidrawDir }); * Returns the arguments for the release script.
console.info("Building ESM Package..."); *
execSync(`yarn run build:esm`, { cwd: excalidrawDir }); * Usage examples:
console.info("Publishing the package..."); * - yarn release --help -> prints this help message
execSync(`yarn --cwd ${excalidrawDir} publish`); * - yarn release -> publishes `@excalidraw` packages with "test" tag and "-[hash]" version suffix
} catch (error) { * - yarn release --tag=test -> same as above
console.error(error); * - yarn release --tag=next -> publishes `@excalidraw` packages with "next" tag and version "-[hash]" suffix
* - yarn release --tag=next --non-interactive -> skips interactive prompts (runs on CI/CD), otherwise same as above
* - yarn release --tag=latest --version=0.19.0 -> publishes `@excalidraw` packages with "latest" tag and version "0.19.0" & prepares changelog for the release
*
* @returns [tag, version, nonInteractive]
*/
const getArguments = () => {
let tag = "test";
let version = "";
let nonInteractive = false;
for (const argument of process.argv.slice(2)) {
if (/--help/.test(argument)) {
console.info(`Available arguments:
--tag=<tag> -> (optional) "test" (default), "next" for auto release, "latest" for stable release
--version=<version> -> (optional) for "next" and "test", (required) for "latest" i.e. "0.19.0"
--non-interactive -> (optional) disables interactive prompts`);
console.info(`\nUsage examples:
- yarn release -> publishes \`@excalidraw\` packages with "test" tag and "-[hash]" version suffix
- yarn release --tag=test -> same as above
- yarn release --tag=next -> publishes \`@excalidraw\` packages with "next" tag and version "-[hash]" suffix
- yarn release --tag=next --non-interactive -> skips interactive prompts (runs on CI/CD), otherwise same as above
- yarn release --tag=latest --version=0.19.0 -> publishes \`@excalidraw\` packages with "latest" tag and version "0.19.0" & prepares changelog for the release`);
process.exit(0);
}
if (/--tag=/.test(argument)) {
tag = argument.split("=")[1];
}
if (/--version=/.test(argument)) {
version = argument.split("=")[1];
}
if (/--non-interactive/.test(argument)) {
nonInteractive = true;
}
}
if (tag !== "latest" && tag !== "next" && tag !== "test") {
console.error(`Unsupported tag "${tag}", use "latest", "next" or "test".`);
process.exit(1);
}
if (tag === "latest" && !version) {
console.error("Pass the version to make the latest stable release!");
process.exit(1);
}
if (!version) {
// set the next version based on the excalidraw package version + commit hash
const excalidrawPackageVersion = require(getPackageJsonPath(
"excalidraw",
)).version;
const hash = getShortCommitHash();
if (!excalidrawPackageVersion.includes(hash)) {
version = `${excalidrawPackageVersion}-${hash}`;
} else {
// ensuring idempotency
version = excalidrawPackageVersion;
}
}
console.info(`Running with tag "${tag}" and version "${version}"...`);
return [tag, version, nonInteractive];
};
const validatePackageName = (packageName) => {
if (!PACKAGES.includes(packageName)) {
console.error(`Package "${packageName}" not found!`);
process.exit(1); process.exit(1);
} }
}; };
const release = () => { const getPackageJsonPath = (packageName) => {
publish(); validatePackageName(packageName);
console.info(`Published ${pkg.version}!`); return path.resolve(PACKAGES_DIR, packageName, "package.json");
}; };
release(); const updatePackageJsons = (nextVersion) => {
const packageJsons = new Map();
for (const packageName of PACKAGES) {
const pkg = require(getPackageJsonPath(packageName));
pkg.version = nextVersion;
if (pkg.dependencies) {
for (const dependencyName of PACKAGES) {
if (!pkg.dependencies[`@excalidraw/${dependencyName}`]) {
continue;
}
pkg.dependencies[`@excalidraw/${dependencyName}`] = nextVersion;
}
}
packageJsons.set(packageName, `${JSON.stringify(pkg, null, 2)}\n`);
}
// modify once, to avoid inconsistent state
for (const packageName of PACKAGES) {
const content = packageJsons.get(packageName);
fs.writeFileSync(getPackageJsonPath(packageName), content, "utf-8");
}
};
const getShortCommitHash = () => {
return execSync("git rev-parse --short HEAD").toString().trim();
};
const askToCommit = (tag, nextVersion) => {
if (tag !== "latest") {
return Promise.resolve();
}
return new Promise((resolve) => {
const rl = require("readline").createInterface({
input: process.stdin,
output: process.stdout,
});
rl.question(
"Do you want to commit these changes to git? (Y/n): ",
(answer) => {
rl.close();
if (answer.toLowerCase() === "y") {
execSync(`git add -u`);
execSync(
`git commit -m "chore: release @excalidraw/excalidraw@${nextVersion} 🎉"`,
);
} else {
console.warn(
"Skipping commit. Don't forget to commit manually later!",
);
}
resolve();
},
);
});
};
const buildPackages = () => {
console.info("Running yarn install...");
execSync(`yarn --frozen-lockfile`, { stdio: "inherit" });
console.info("Removing existing build artifacts...");
execSync(`yarn rm:build`, { stdio: "inherit" });
for (const packageName of PACKAGES) {
console.info(`Building "@excalidraw/${packageName}"...`);
execSync(`yarn run build:esm`, {
cwd: path.resolve(PACKAGES_DIR, packageName),
stdio: "inherit",
});
}
};
const askToPublish = (tag, version) => {
return new Promise((resolve) => {
const rl = require("readline").createInterface({
input: process.stdin,
output: process.stdout,
});
rl.question(
"Do you want to publish these changes to npm? (Y/n): ",
(answer) => {
rl.close();
if (answer.toLowerCase() === "y") {
publishPackages(tag, version);
} else {
console.info("Skipping publish.");
}
resolve();
},
);
});
};
const publishPackages = (tag, version) => {
for (const packageName of PACKAGES) {
execSync(`yarn publish --tag ${tag}`, {
cwd: path.resolve(PACKAGES_DIR, packageName),
stdio: "inherit",
});
console.info(
`Published "@excalidraw/${packageName}@${tag}" with version "${version}"! 🎉`,
);
}
};
/** main */
(async () => {
const [tag, version, nonInteractive] = getArguments();
buildPackages();
if (tag === "latest") {
await updateChangelog(version);
}
updatePackageJsons(version);
if (nonInteractive) {
publishPackages(tag, version);
} else {
await askToCommit(tag, version);
await askToPublish(tag, version);
}
})();

View File

@ -20,14 +20,16 @@ const headerForType = {
perf: "Performance", perf: "Performance",
build: "Build", build: "Build",
}; };
const badCommits = []; const badCommits = [];
const getCommitHashForLastVersion = async () => { const getCommitHashForLastVersion = async () => {
try { try {
const commitMessage = `"release @excalidraw/excalidraw@${lastVersion}"`; const commitMessage = `"release @excalidraw/excalidraw"`;
const { stdout } = await exec( const { stdout } = await exec(
`git log --format=format:"%H" --grep=${commitMessage}`, `git log --format=format:"%H" --grep=${commitMessage}`,
); );
return stdout; // take commit hash from latest release
return stdout.split(/\r?\n/)[0];
} catch (error) { } catch (error) {
console.error(error); console.error(error);
} }