Compare commits

..

3 Commits

Author SHA1 Message Date
Ryan Di
c79893608a feat: experiment with a diff shift locking behavior 2025-07-07 18:09:18 +10:00
Ryan Di
a2cf15db9c fix: setting states correctly for cropping 2025-07-02 15:25:32 +10:00
Ryan Di
8d3195e350 feat: move by uncropped area too 2025-06-18 00:16:44 +10:00
71 changed files with 1160 additions and 1948 deletions

View File

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

View File

@@ -0,0 +1,55 @@
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,12 +28,32 @@ To start the example app using the `@excalidraw/excalidraw` package, follow the
## 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
To release the next stable version follow the below steps:
```bash
yarn release --tag=latest --version=0.19.0
yarn prerelease:excalidraw
```
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.
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.
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,7 +33,6 @@ const ExcalidrawScope = {
initialData,
useI18n: ExcalidrawComp.useI18n,
convertToExcalidrawElements: ExcalidrawComp.convertToExcalidrawElements,
CaptureUpdateAction: ExcalidrawComp.CaptureUpdateAction,
};
export default ExcalidrawScope;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@excalidraw/element",
"version": "0.18.0",
"version": "0.1.0",
"type": "module",
"types": "./dist/types/element/src/index.d.ts",
"main": "./dist/prod/index.js",
@@ -13,10 +13,7 @@
"default": "./dist/prod/index.js"
},
"./*": {
"types": "./dist/types/element/src/*.d.ts",
"development": "./dist/dev/index.js",
"production": "./dist/prod/index.js",
"default": "./dist/prod/index.js"
"types": "./dist/types/element/src/*.d.ts"
}
},
"files": [
@@ -55,9 +52,5 @@
"scripts": {
"gen:types": "rimraf types && tsc",
"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,8 +1,6 @@
import type { AppState } from "@excalidraw/excalidraw/types";
import { updateBoundElements } from "./binding";
import { getCommonBoundingBox } from "./bounds";
import { getSelectedElementsByGroup } from "./groups";
import { getMaximumGroups } from "./groups";
import type { Scene } from "./Scene";
@@ -18,12 +16,11 @@ export const alignElements = (
selectedElements: ExcalidrawElement[],
alignment: Alignment,
scene: Scene,
appState: Readonly<AppState>,
): ExcalidrawElement[] => {
const groups: ExcalidrawElement[][] = getSelectedElementsByGroup(
const elementsMap = scene.getNonDeletedElementsMap();
const groups: ExcalidrawElement[][] = getMaximumGroups(
selectedElements,
scene.getNonDeletedElementsMap(),
appState,
elementsMap,
);
const selectionBoundingBox = getCommonBoundingBox(selectedElements);

View File

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

View File

@@ -1,9 +1,7 @@
import type { AppState } from "@excalidraw/excalidraw/types";
import { getCommonBoundingBox } from "./bounds";
import { newElementWith } from "./mutateElement";
import { getSelectedElementsByGroup } from "./groups";
import { getMaximumGroups } from "./groups";
import type { ElementsMap, ExcalidrawElement } from "./types";
@@ -16,7 +14,6 @@ export const distributeElements = (
selectedElements: ExcalidrawElement[],
elementsMap: ElementsMap,
distribution: Distribution,
appState: Readonly<AppState>,
): ExcalidrawElement[] => {
const [start, mid, end, extent] =
distribution.axis === "x"
@@ -24,11 +21,7 @@ export const distributeElements = (
: (["minY", "midY", "maxY", "height"] as const);
const bounds = getCommonBoundingBox(selectedElements);
const groups = getSelectedElementsByGroup(
selectedElements,
elementsMap,
appState,
)
const groups = getMaximumGroups(selectedElements, elementsMap)
.map((group) => [group, getCommonBoundingBox(group)] as const)
.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 RE_YOUTUBE =
/^(?:http(?:s)?:\/\/)?(?:www\.)?youtu(?:be\.com|\.be)\/(embed\/|watch\?v=|shorts\/|playlist\?list=|embed\/videoseries\?list=)?([a-zA-Z0-9_-]+)/;
/^(?:http(?:s)?:\/\/)?(?:www\.)?youtu(?:be\.com|\.be)\/(embed\/|watch\?v=|shorts\/|playlist\?list=|embed\/videoseries\?list=)?([a-zA-Z0-9_-]+)(?:\?t=|&t=|\?start=|&start=)?([a-zA-Z0-9_-]+)?[^\s]*$/;
const RE_VIMEO =
/^(?:http(?:s)?:\/\/)?(?:(?:w){3}\.)?(?:player\.)?vimeo\.com\/(?:video\/)?([^?\s]+)(?:\?.*)?$/;
@@ -56,35 +56,6 @@ const RE_REDDIT =
const RE_REDDIT_EMBED =
/^<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([
"youtube.com",
"youtu.be",
@@ -142,8 +113,7 @@ export const getEmbedLink = (
let aspectRatio = { w: 560, h: 840 };
const ytLink = link.match(RE_YOUTUBE);
if (ytLink?.[2]) {
const startTime = parseYouTubeTimestamp(originalLink);
const time = startTime > 0 ? `&start=${startTime}` : ``;
const time = ytLink[3] ? `&start=${ytLink[3]}` : ``;
const isPortrait = link.includes("shorts");
type = "video";
switch (ytLink[1]) {

View File

@@ -7,8 +7,6 @@ import type { Mutable } from "@excalidraw/common/utility-types";
import { getBoundTextElement } from "./textElement";
import { isBoundToContainer } from "./typeChecks";
import { makeNextSelectedElementIds, getSelectedElements } from "./selection";
import type {
@@ -404,78 +402,3 @@ export const getNewGroupIdsForDuplication = (
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

@@ -149,12 +149,10 @@ export class LinearElementEditor {
public readonly segmentMidPointHoveredCoords: GlobalPoint | null;
public readonly elbowed: boolean;
public readonly customLineAngle: number | null;
public readonly isEditing: boolean;
constructor(
element: NonDeleted<ExcalidrawLinearElement>,
elementsMap: ElementsMap,
isEditing: boolean = false,
) {
this.elementId = element.id as string & {
_brand: "excalidrawLinearElementId";
@@ -189,7 +187,6 @@ export class LinearElementEditor {
this.segmentMidPointHoveredCoords = null;
this.elbowed = isElbowArrow(element) && element.elbowed;
this.customLineAngle = null;
this.isEditing = isEditing;
}
// ---------------------------------------------------------------------------
@@ -197,7 +194,6 @@ export class LinearElementEditor {
// ---------------------------------------------------------------------------
static POINT_HANDLE_SIZE = 10;
/**
* @param id the `elementId` from the instance of this class (so that we can
* statically guarantee this method returns an ExcalidrawLinearElement)
@@ -219,14 +215,11 @@ export class LinearElementEditor {
setState: React.Component<any, AppState>["setState"],
elementsMap: NonDeletedSceneElementsMap,
) {
if (
!appState.selectedLinearElement?.isEditing ||
!appState.selectionElement
) {
if (!appState.editingLinearElement || !appState.selectionElement) {
return false;
}
const { selectedLinearElement } = appState;
const { selectedPointsIndices, elementId } = selectedLinearElement;
const { editingLinearElement } = appState;
const { selectedPointsIndices, elementId } = editingLinearElement;
const element = LinearElementEditor.getElement(elementId, elementsMap);
if (!element) {
@@ -267,8 +260,8 @@ export class LinearElementEditor {
});
setState({
selectedLinearElement: {
...selectedLinearElement,
editingLinearElement: {
...editingLinearElement,
selectedPointsIndices: nextSelectedPoints.length
? nextSelectedPoints
: null,
@@ -486,6 +479,9 @@ export class LinearElementEditor {
return {
...app.state,
editingLinearElement: app.state.editingLinearElement
? newLinearElementEditor
: null,
selectedLinearElement: newLinearElementEditor,
suggestedBindings,
};
@@ -622,7 +618,7 @@ export class LinearElementEditor {
// Since its not needed outside editor unless 2 pointer lines or bound text
if (
!isElbowArrow(element) &&
!appState.selectedLinearElement?.isEditing &&
!appState.editingLinearElement &&
element.points.length > 2 &&
!boundText
) {
@@ -688,7 +684,7 @@ export class LinearElementEditor {
);
if (
points.length >= 3 &&
!appState.selectedLinearElement?.isEditing &&
!appState.editingLinearElement &&
!isElbowArrow(element)
) {
return null;
@@ -885,7 +881,7 @@ export class LinearElementEditor {
segmentMidpoint,
elementsMap,
);
} else if (event.altKey && appState.selectedLinearElement?.isEditing) {
} else if (event.altKey && appState.editingLinearElement) {
if (linearElementEditor.lastUncommittedPoint == null) {
scene.mutateElement(element, {
points: [
@@ -1027,14 +1023,14 @@ export class LinearElementEditor {
app: AppClassProperties,
): LinearElementEditor | null {
const appState = app.state;
if (!appState.selectedLinearElement?.isEditing) {
if (!appState.editingLinearElement) {
return null;
}
const { elementId, lastUncommittedPoint } = appState.selectedLinearElement;
const { elementId, lastUncommittedPoint } = appState.editingLinearElement;
const elementsMap = app.scene.getNonDeletedElementsMap();
const element = LinearElementEditor.getElement(elementId, elementsMap);
if (!element) {
return appState.selectedLinearElement;
return appState.editingLinearElement;
}
const { points } = element;
@@ -1044,12 +1040,10 @@ export class LinearElementEditor {
if (lastPoint === lastUncommittedPoint) {
LinearElementEditor.deletePoints(element, app, [points.length - 1]);
}
return appState.selectedLinearElement?.lastUncommittedPoint
? {
...appState.selectedLinearElement,
lastUncommittedPoint: null,
}
: appState.selectedLinearElement;
return {
...appState.editingLinearElement,
lastUncommittedPoint: null,
};
}
let newPoint: LocalPoint;
@@ -1073,8 +1067,8 @@ export class LinearElementEditor {
newPoint = LinearElementEditor.createPointAt(
element,
elementsMap,
scenePointerX - appState.selectedLinearElement.pointerOffset.x,
scenePointerY - appState.selectedLinearElement.pointerOffset.y,
scenePointerX - appState.editingLinearElement.pointerOffset.x,
scenePointerY - appState.editingLinearElement.pointerOffset.y,
event[KEYS.CTRL_OR_CMD] || isElbowArrow(element)
? null
: app.getEffectiveGridSize(),
@@ -1098,7 +1092,7 @@ export class LinearElementEditor {
LinearElementEditor.addPoints(element, app.scene, [newPoint]);
}
return {
...appState.selectedLinearElement,
...appState.editingLinearElement,
lastUncommittedPoint: element.points[element.points.length - 1],
};
}
@@ -1257,12 +1251,12 @@ export class LinearElementEditor {
// ---------------------------------------------------------------------------
static duplicateSelectedPoints(appState: AppState, scene: Scene): AppState {
invariant(
appState.selectedLinearElement?.isEditing,
appState.editingLinearElement,
"Not currently editing a linear element",
);
const elementsMap = scene.getNonDeletedElementsMap();
const { selectedPointsIndices, elementId } = appState.selectedLinearElement;
const { selectedPointsIndices, elementId } = appState.editingLinearElement;
const element = LinearElementEditor.getElement(elementId, elementsMap);
invariant(
@@ -1324,8 +1318,8 @@ export class LinearElementEditor {
return {
...appState,
selectedLinearElement: {
...appState.selectedLinearElement,
editingLinearElement: {
...appState.editingLinearElement,
selectedPointsIndices: nextSelectedIndices,
},
};
@@ -1337,9 +1331,8 @@ export class LinearElementEditor {
pointIndices: readonly number[],
) {
const isUncommittedPoint =
app.state.selectedLinearElement?.isEditing &&
app.state.selectedLinearElement?.lastUncommittedPoint ===
element.points[element.points.length - 1];
app.state.editingLinearElement?.lastUncommittedPoint ===
element.points[element.points.length - 1];
const nextPoints = element.points.filter((_, idx) => {
return !pointIndices.includes(idx);
@@ -1512,7 +1505,7 @@ export class LinearElementEditor {
pointFrom(pointerCoords.x, pointerCoords.y),
);
if (
!appState.selectedLinearElement?.isEditing &&
!appState.editingLinearElement &&
dist < DRAGGING_THRESHOLD / appState.zoom.value
) {
return false;

View File

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

View File

@@ -27,8 +27,6 @@ import {
isImageElement,
} from "./index";
import type { ApplyToOptions } from "./delta";
import type {
ExcalidrawElement,
OrderedExcalidrawElement,
@@ -572,15 +570,9 @@ export class StoreDelta {
delta: StoreDelta,
elements: SceneElementsMap,
appState: AppState,
options: ApplyToOptions = {
excludedProperties: new Set(),
},
): [SceneElementsMap, AppState, boolean] {
const [nextElements, elementsContainVisibleChange] = delta.elements.applyTo(
elements,
StoreSnapshot.empty().elements,
options,
);
const [nextElements, elementsContainVisibleChange] =
delta.elements.applyTo(elements);
const [nextAppState, appStateContainsVisibleChange] =
delta.appState.applyTo(appState, nextElements);
@@ -978,8 +970,8 @@ const getDefaultObservedAppState = (): ObservedAppState => {
viewBackgroundColor: COLOR_PALETTE.white,
selectedElementIds: {},
selectedGroupIds: {},
editingLinearElementId: null,
selectedLinearElementId: null,
selectedLinearElementIsEditing: null,
croppingElementId: null,
activeLockedId: null,
lockedMultiSelections: {},
@@ -998,14 +990,14 @@ export const getObservedAppState = (
croppingElementId: appState.croppingElementId,
activeLockedId: appState.activeLockedId,
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:
(appState as AppState).selectedLinearElement?.elementId ??
(appState as ObservedAppState).selectedLinearElementId ??
null,
selectedLinearElementIsEditing:
(appState as AppState).selectedLinearElement?.isEditing ??
(appState as ObservedAppState).selectedLinearElementIsEditing ??
null,
};
Reflect.defineProperty(observedAppState, hiddenObservedAppStateProp, {

View File

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

View File

@@ -589,424 +589,4 @@ describe("aligning", () => {
expect(API.getSelectedElements()[2].x).toEqual(250);
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
// the issue, due to https://github.com/excalidraw/excalidraw/blob/46bff3daceb602accf60c40a84610797260fca94/src/components/App.tsx#L740
mouse.reset();
expect(h.state.selectedLinearElement?.isEditing).toBe(true);
expect(h.state.editingLinearElement).not.toBe(null);
mouse.down(0, 0);
await new Promise((r) => setTimeout(r, 100));
expect(h.state.selectedLinearElement?.isEditing).toBe(false);
expect(h.state.editingLinearElement).toBe(null);
expect(API.getSelectedElement().type).toBe("rectangle");
mouse.up();
expect(API.getSelectedElement().type).toBe("rectangle");

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -48,6 +48,7 @@ export const getDefaultAppState = (): Omit<
newElement: null,
editingTextElement: null,
editingGroupId: null,
editingLinearElement: null,
activeTool: {
type: "selection",
customType: null,
@@ -123,6 +124,11 @@ export const getDefaultAppState = (): Omit<
searchMatches: null,
lockedMultiSelections: {},
activeLockedId: null,
cropPositionMovement: {
enabled: false,
croppingElementId: undefined,
directionLock: null,
},
};
};
@@ -174,6 +180,7 @@ const APP_STATE_STORAGE_CONF = (<
newElement: { browser: false, export: false, server: false },
editingTextElement: { browser: false, 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 },
penMode: { browser: true, export: false, server: false },
penDetected: { browser: true, export: false, server: false },
@@ -247,6 +254,7 @@ const APP_STATE_STORAGE_CONF = (<
searchMatches: { browser: false, export: false, server: false },
lockedMultiSelections: { browser: true, export: true, server: true },
activeLockedId: { browser: false, export: false, server: false },
cropPositionMovement: { browser: false, export: false, server: false },
});
const _clearAppStateForStorage = <

View File

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

View File

@@ -100,7 +100,6 @@ import {
randomInteger,
CLASSES,
Emitter,
MINIMUM_ARROW_SIZE,
} from "@excalidraw/common";
import {
@@ -192,6 +191,7 @@ import {
FlowChartNavigator,
getLinkDirectionFromKey,
cropElement,
getUncroppedImageElement,
wrapText,
isElementLink,
parseElementLinkFromURL,
@@ -461,6 +461,7 @@ import type {
} from "../types";
import type { RoughCanvas } from "roughjs/bin/canvas";
import type { Action, ActionResult } from "../actions/types";
import type { GlobalPoint } from "@excalidraw/math";
const AppContext = React.createContext<AppClassProperties>(null!);
const AppPropsContext = React.createContext<AppProps>(null!);
@@ -594,10 +595,6 @@ class App extends React.Component<AppProps, AppState> {
* insert to DOM before user initially scrolls to them) */
private initializedEmbeds = new Set<ExcalidrawIframeLikeElement["id"]>();
private handleToastClose = () => {
this.setToast(null);
};
private elementsPendingErasure: ElementsPendingErasure = new Set();
public flowChartCreator: FlowChartCreator = new FlowChartCreator();
@@ -1712,16 +1709,14 @@ class App extends React.Component<AppProps, AppState> {
/>
</ElementCanvasButtons>
)}
{this.state.toast !== null && (
<Toast
message={this.state.toast.message}
onClose={this.handleToastClose}
onClose={() => this.setToast(null)}
duration={this.state.toast.duration}
closable={this.state.toast.closable}
/>
)}
{this.state.contextMenu && (
<ContextMenu
items={this.state.contextMenu.items}
@@ -2158,14 +2153,9 @@ class App extends React.Component<AppProps, AppState> {
public dismissLinearEditor = () => {
setTimeout(() => {
if (this.state.selectedLinearElement?.isEditing) {
this.setState({
selectedLinearElement: {
...this.state.selectedLinearElement,
isEditing: false,
},
});
}
this.setState({
editingLinearElement: null,
});
});
};
@@ -2861,15 +2851,15 @@ class App extends React.Component<AppProps, AppState> {
);
if (
this.state.selectedLinearElement?.isEditing &&
!this.state.selectedElementIds[this.state.selectedLinearElement.elementId]
this.state.editingLinearElement &&
!this.state.selectedElementIds[this.state.editingLinearElement.elementId]
) {
// defer so that the scheduleCapture flag isn't reset via current update
setTimeout(() => {
// execute only if the condition still holds when the deferred callback
// executes (it can be scheduled multiple times depending on how
// many times the component renders)
this.state.selectedLinearElement?.isEditing &&
this.state.editingLinearElement &&
this.actionManager.executeAction(actionFinalize);
});
}
@@ -4424,13 +4414,17 @@ class App extends React.Component<AppProps, AppState> {
if (event[KEYS.CTRL_OR_CMD] || isLineElement(selectedElement)) {
if (isLinearElement(selectedElement)) {
if (
!this.state.selectedLinearElement?.isEditing ||
this.state.selectedLinearElement.elementId !==
selectedElement.id
!this.state.editingLinearElement ||
this.state.editingLinearElement.elementId !== selectedElement.id
) {
this.store.scheduleCapture();
if (!isElbowArrow(selectedElement)) {
this.actionManager.executeAction(actionToggleLinearEditor);
this.setState({
editingLinearElement: new LinearElementEditor(
selectedElement,
this.scene.getNonDeletedElementsMap(),
),
});
}
}
}
@@ -4927,17 +4921,7 @@ class App extends React.Component<AppProps, AppState> {
}),
onSubmit: withBatchedUpdates(({ viaKeyboard, nextOriginalText }) => {
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
// (when submitting via click it should act as signal to deselect)
if (!isDeleted && viaKeyboard) {
@@ -4966,10 +4950,9 @@ class App extends React.Component<AppProps, AppState> {
element,
]);
}
// 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();
if (!isDeleted || isExistingElement) {
this.store.scheduleCapture();
}
flushSync(() => {
this.setState({
@@ -5433,12 +5416,15 @@ class App extends React.Component<AppProps, AppState> {
if (
((event[KEYS.CTRL_OR_CMD] && isSimpleArrow(selectedLinearElement)) ||
isLineElement(selectedLinearElement)) &&
(!this.state.selectedLinearElement?.isEditing ||
this.state.selectedLinearElement.elementId !==
selectedLinearElement.id)
this.state.editingLinearElement?.elementId !== selectedLinearElement.id
) {
// Use the proper action to ensure immediate history capture
this.actionManager.executeAction(actionToggleLinearEditor);
this.store.scheduleCapture();
this.setState({
editingLinearElement: new LinearElementEditor(
selectedLinearElement,
this.scene.getNonDeletedElementsMap(),
),
});
return;
} else if (
this.state.selectedLinearElement &&
@@ -5503,8 +5489,8 @@ class App extends React.Component<AppProps, AppState> {
return;
}
} else if (
this.state.selectedLinearElement?.isEditing &&
this.state.selectedLinearElement.elementId ===
this.state.editingLinearElement &&
this.state.editingLinearElement.elementId ===
selectedLinearElement.id &&
isLineElement(selectedLinearElement)
) {
@@ -5559,7 +5545,7 @@ class App extends React.Component<AppProps, AppState> {
// shouldn't edit/create text when inside line editor (often false positive)
if (!this.state.selectedLinearElement?.isEditing) {
if (!this.state.editingLinearElement) {
const container = this.getTextBindableContainerAtPosition(
sceneX,
sceneY,
@@ -5857,8 +5843,8 @@ class App extends React.Component<AppProps, AppState> {
}
if (
this.state.selectedLinearElement?.isEditing &&
!this.state.selectedLinearElement.isDragging
this.state.editingLinearElement &&
!this.state.editingLinearElement.isDragging
) {
const editingLinearElement = LinearElementEditor.handlePointerMove(
event,
@@ -5866,34 +5852,30 @@ class App extends React.Component<AppProps, AppState> {
scenePointerY,
this,
);
const linearElement = editingLinearElement
? this.scene.getElement(editingLinearElement.elementId)
: null;
if (
editingLinearElement &&
editingLinearElement !== this.state.selectedLinearElement
editingLinearElement !== this.state.editingLinearElement
) {
// Since we are reading from previous state which is not possible with
// 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.
flushSync(() => {
this.setState({
selectedLinearElement: editingLinearElement,
editingLinearElement,
});
});
}
if (
editingLinearElement?.lastUncommittedPoint != null &&
linearElement &&
isBindingElementType(linearElement.type)
) {
if (editingLinearElement?.lastUncommittedPoint != null) {
this.maybeSuggestBindingAtCursor(
scenePointer,
editingLinearElement.elbowed,
);
} else if (this.state.suggestedBindings.length) {
this.setState({ suggestedBindings: [] });
} else {
// causes stack overflow if not sync
flushSync(() => {
this.setState({ suggestedBindings: [] });
});
}
}
@@ -6039,7 +6021,7 @@ class App extends React.Component<AppProps, AppState> {
if (
selectedElements.length === 1 &&
!isOverScrollBar &&
!this.state.selectedLinearElement?.isEditing
!this.state.editingLinearElement
) {
// for linear elements, we'd like to prioritize point dragging over edge resizing
// therefore, we update and check hovered point index first
@@ -6157,6 +6139,15 @@ class App extends React.Component<AppProps, AppState> {
setCursor(this.interactiveCanvas, CURSOR_TYPE.AUTO);
} else if (isOverScrollBar) {
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 (
// if using cmd/ctrl, we're not dragging
!event[KEYS.CTRL_OR_CMD]
@@ -6198,14 +6189,6 @@ class App extends React.Component<AppProps, AppState> {
} else {
setCursor(this.interactiveCanvas, CURSOR_TYPE.AUTO);
}
if (this.state.selectedLinearElement) {
this.handleHoverSelectedLinearElement(
this.state.selectedLinearElement,
scenePointerX,
scenePointerY,
);
}
}
if (this.state.openDialog?.name === "elementLinkSelector" && hitElement) {
@@ -6560,6 +6543,44 @@ class App extends React.Component<AppProps, AppState> {
this.clearSelectionIfNotUsingSelection();
this.updateBindingEnabledOnPointerMove(event);
// Check if we're in crop mode and hitting uncropped area - if so, skip selection handling
if (this.state.croppingElementId) {
const croppingElement = this.scene
.getNonDeletedElementsMap()
.get(this.state.croppingElementId);
if (
croppingElement &&
isImageElement(croppingElement) &&
croppingElement.crop
) {
const uncroppedElement = getUncroppedImageElement(
croppingElement,
this.scene.getNonDeletedElementsMap(),
);
const hitUncroppedArea = hitElementItself({
point: pointFrom<GlobalPoint>(
pointerDownState.origin.x,
pointerDownState.origin.y,
),
element: uncroppedElement,
threshold: this.getElementHitThreshold(uncroppedElement),
elementsMap: this.scene.getNonDeletedElementsMap(),
});
if (hitUncroppedArea) {
// Set a dedicated flag for crop position movement
pointerDownState.cropPositionMovement.enabled = true;
pointerDownState.cropPositionMovement.croppingElementId =
croppingElement.id;
// Set isCropping state to true so crop mode UI stays active
this.setState({
isCropping: true,
});
}
}
}
if (this.handleSelectionOnPointerDown(event, pointerDownState)) {
return;
}
@@ -6971,6 +6992,11 @@ class App extends React.Component<AppProps, AppState> {
boxSelection: {
hasOccurred: false,
},
cropPositionMovement: {
enabled: false,
croppingElementId: undefined,
directionLock: null,
},
};
}
@@ -7043,7 +7069,7 @@ class App extends React.Component<AppProps, AppState> {
if (
selectedElements.length === 1 &&
!this.state.selectedLinearElement?.isEditing &&
!this.state.editingLinearElement &&
!isElbowArrow(selectedElements[0]) &&
!(
this.state.selectedLinearElement &&
@@ -7114,7 +7140,8 @@ class App extends React.Component<AppProps, AppState> {
}
} else {
if (this.state.selectedLinearElement) {
const linearElementEditor = this.state.selectedLinearElement;
const linearElementEditor =
this.state.editingLinearElement || this.state.selectedLinearElement;
const ret = LinearElementEditor.handlePointerDown(
event,
this,
@@ -7128,6 +7155,10 @@ class App extends React.Component<AppProps, AppState> {
}
if (ret.linearElementEditor) {
this.setState({ selectedLinearElement: ret.linearElementEditor });
if (this.state.editingLinearElement) {
this.setState({ editingLinearElement: ret.linearElementEditor });
}
}
if (ret.didAddPoint) {
return true;
@@ -7190,11 +7221,39 @@ class App extends React.Component<AppProps, AppState> {
return true;
}
if (
this.state.croppingElementId &&
pointerDownState.hit.element?.id !== this.state.croppingElementId
) {
this.finishImageCropping();
if (this.state.croppingElementId) {
const croppingElement = this.scene
.getNonDeletedElementsMap()
.get(this.state.croppingElementId);
if (croppingElement) {
const uncroppedElement = getUncroppedImageElement(
croppingElement as any,
this.scene.getNonDeletedElementsMap(),
);
const hitUncroppedArea = hitElementItself({
point: pointFrom<GlobalPoint>(
pointerDownState.origin.x,
pointerDownState.origin.y,
),
element: uncroppedElement,
threshold: this.getElementHitThreshold(uncroppedElement),
elementsMap: this.scene.getNonDeletedElementsMap(),
});
if (!hitUncroppedArea) {
this.finishImageCropping();
} else {
// ensure the image remains selected so crop handles are rendered
if (
(!this.state.selectedElementIds ||
Object.keys(this.state.selectedElementIds).length === 0) &&
this.state.croppingElementId
) {
this.setState({
selectedElementIds: { [this.state.croppingElementId]: true },
});
}
}
}
}
if (pointerDownState.hit.element) {
@@ -7220,6 +7279,7 @@ class App extends React.Component<AppProps, AppState> {
pointerDownState.hit.allHitElements.some((element) =>
this.isASelectedElement(element),
);
if (
(hitElement === null || !someHitElementIsSelected) &&
!event.shiftKey &&
@@ -7228,11 +7288,11 @@ class App extends React.Component<AppProps, AppState> {
this.clearSelection(hitElement);
}
if (this.state.selectedLinearElement?.isEditing) {
if (this.state.editingLinearElement) {
this.setState({
selectedElementIds: makeNextSelectedElementIds(
{
[this.state.selectedLinearElement.elementId]: true,
[this.state.editingLinearElement.elementId]: true,
},
this.state,
),
@@ -8043,6 +8103,165 @@ class App extends React.Component<AppProps, AppState> {
}
const pointerCoords = viewportCoordsToSceneCoords(event, this.state);
// #region dedicated crop position movement
if (
pointerDownState.cropPositionMovement.enabled &&
pointerDownState.cropPositionMovement.croppingElementId
) {
const croppingElement = pointerDownState.cropPositionMovement
.croppingElementId
? this.scene
.getNonDeletedElementsMap()
.get(pointerDownState.cropPositionMovement.croppingElementId)
: null;
if (
croppingElement &&
isImageElement(croppingElement) &&
croppingElement.crop
) {
const transformHandleType = pointerDownState.resize.handleType;
if (!transformHandleType) {
const crop = croppingElement.crop;
const image =
isInitializedImageElement(croppingElement) &&
this.imageCache.get(croppingElement.fileId)?.image;
if (image && !(image instanceof Promise)) {
// calculate total drag offset from the original pointer down position
const totalDragOffset = {
x: pointerCoords.x - pointerDownState.origin.x,
y: pointerCoords.y - pointerDownState.origin.y,
};
// apply shift key constraint for directional movement
const threshold = 20;
let snappingToOrigin = false;
if (event.shiftKey) {
if (!pointerDownState.cropPositionMovement.directionLock) {
if (
Math.abs(totalDragOffset.x) > threshold ||
Math.abs(totalDragOffset.y) > threshold
) {
pointerDownState.cropPositionMovement.directionLock =
Math.abs(totalDragOffset.x) > Math.abs(totalDragOffset.y)
? "x"
: "y";
} else {
// if within threshold and not locked, always snap to origin
snappingToOrigin = true;
}
} else {
// if user moves back within threshold, unlock and snap back
if (
Math.abs(totalDragOffset.x) < threshold &&
Math.abs(totalDragOffset.y) < threshold
) {
pointerDownState.cropPositionMovement.directionLock = null;
snappingToOrigin = true;
}
}
} else {
pointerDownState.cropPositionMovement.directionLock = null;
}
if (snappingToOrigin) {
totalDragOffset.x = 0;
totalDragOffset.y = 0;
}
if (pointerDownState.cropPositionMovement.directionLock === "x") {
totalDragOffset.y = 0;
} else if (
pointerDownState.cropPositionMovement.directionLock === "y"
) {
totalDragOffset.x = 0;
}
// scale the drag offset
const scaledDragOffset = vectorScale(
vector(totalDragOffset.x, totalDragOffset.y),
Math.max(this.state.zoom.value, 2),
);
const elementsMap = this.scene.getNonDeletedElementsMap();
const [x1, y1, x2, y2, cx, cy] = getElementAbsoluteCoords(
croppingElement,
elementsMap,
);
const topLeft = vectorFromPoint(
pointRotateRads(
pointFrom(x1, y1),
pointFrom(cx, cy),
croppingElement.angle,
),
);
const topRight = vectorFromPoint(
pointRotateRads(
pointFrom(x2, y1),
pointFrom(cx, cy),
croppingElement.angle,
),
);
const bottomLeft = vectorFromPoint(
pointRotateRads(
pointFrom(x1, y2),
pointFrom(cx, cy),
croppingElement.angle,
),
);
const topEdge = vectorNormalize(
vectorSubtract(topRight, topLeft),
);
const leftEdge = vectorNormalize(
vectorSubtract(bottomLeft, topLeft),
);
// project scaledDragOffset onto leftEdge and topEdge to decompose
const offsetVector = vector(
vectorDot(scaledDragOffset, topEdge),
vectorDot(scaledDragOffset, leftEdge),
);
// get the original crop from when the drag started
const originalCroppingElement =
pointerDownState.originalElements.get(croppingElement.id) as
| ExcalidrawImageElement
| undefined;
const originalCrop = originalCroppingElement?.crop || crop;
const nextCrop = {
...crop,
x: clamp(
originalCrop.x -
offsetVector[0] * Math.sign(croppingElement.scale[0]),
0,
image.naturalWidth - crop.width,
),
y: clamp(
originalCrop.y -
offsetVector[1] * Math.sign(croppingElement.scale[1]),
0,
image.naturalHeight - crop.height,
),
};
this.scene.mutateElement(croppingElement, {
crop: nextCrop,
});
// set drag occurred flag for consistency
pointerDownState.drag.hasOccurred = true;
return;
}
}
}
}
if (this.state.activeLockedId) {
this.setState({
activeLockedId: null,
@@ -8091,12 +8310,16 @@ class App extends React.Component<AppProps, AppState> {
this.scene,
);
this.setState({
selectedLinearElement: {
...this.state.selectedLinearElement,
segmentMidPointHoveredCoords: ret.segmentMidPointHoveredCoords,
pointerDownState: ret.pointerDownState,
},
flushSync(() => {
if (this.state.selectedLinearElement) {
this.setState({
selectedLinearElement: {
...this.state.selectedLinearElement,
segmentMidPointHoveredCoords: ret.segmentMidPointHoveredCoords,
pointerDownState: ret.pointerDownState,
},
});
}
});
return;
}
@@ -8155,9 +8378,7 @@ class App extends React.Component<AppProps, AppState> {
pointDistance(
pointFrom(pointerCoords.x, pointerCoords.y),
pointFrom(pointerDownState.origin.x, pointerDownState.origin.y),
) *
this.state.zoom.value <
MINIMUM_ARROW_SIZE
) < DRAGGING_THRESHOLD
) {
return;
}
@@ -8175,7 +8396,8 @@ class App extends React.Component<AppProps, AppState> {
const elementsMap = this.scene.getNonDeletedElementsMap();
if (this.state.selectedLinearElement) {
const linearElementEditor = this.state.selectedLinearElement;
const linearElementEditor =
this.state.editingLinearElement || this.state.selectedLinearElement;
if (
LinearElementEditor.shouldAddMidpoint(
@@ -8211,6 +8433,16 @@ 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;
@@ -8244,9 +8476,9 @@ class App extends React.Component<AppProps, AppState> {
);
const isSelectingPointsInLineEditor =
this.state.selectedLinearElement?.isEditing &&
this.state.editingLinearElement &&
event.shiftKey &&
this.state.selectedLinearElement.elementId ===
this.state.editingLinearElement.elementId ===
pointerDownState.hit.element?.id;
if (
(hasHitASelectedElement ||
@@ -8314,96 +8546,6 @@ class App extends React.Component<AppProps, AppState> {
}
}
// #region move crop region
if (this.state.croppingElementId) {
const croppingElement = this.scene
.getNonDeletedElementsMap()
.get(this.state.croppingElementId);
if (
croppingElement &&
isImageElement(croppingElement) &&
croppingElement.crop !== null &&
pointerDownState.hit.element === croppingElement
) {
const crop = croppingElement.crop;
const image =
isInitializedImageElement(croppingElement) &&
this.imageCache.get(croppingElement.fileId)?.image;
if (image && !(image instanceof Promise)) {
const instantDragOffset = vectorScale(
vector(
pointerCoords.x - lastPointerCoords.x,
pointerCoords.y - lastPointerCoords.y,
),
Math.max(this.state.zoom.value, 2),
);
const [x1, y1, x2, y2, cx, cy] = getElementAbsoluteCoords(
croppingElement,
elementsMap,
);
const topLeft = vectorFromPoint(
pointRotateRads(
pointFrom(x1, y1),
pointFrom(cx, cy),
croppingElement.angle,
),
);
const topRight = vectorFromPoint(
pointRotateRads(
pointFrom(x2, y1),
pointFrom(cx, cy),
croppingElement.angle,
),
);
const bottomLeft = vectorFromPoint(
pointRotateRads(
pointFrom(x1, y2),
pointFrom(cx, cy),
croppingElement.angle,
),
);
const topEdge = vectorNormalize(
vectorSubtract(topRight, topLeft),
);
const leftEdge = vectorNormalize(
vectorSubtract(bottomLeft, topLeft),
);
// project instantDrafOffset onto leftEdge and topEdge to decompose
const offsetVector = vector(
vectorDot(instantDragOffset, topEdge),
vectorDot(instantDragOffset, leftEdge),
);
const nextCrop = {
...crop,
x: clamp(
crop.x -
offsetVector[0] * Math.sign(croppingElement.scale[0]),
0,
image.naturalWidth - crop.width,
),
y: clamp(
crop.y -
offsetVector[1] * Math.sign(croppingElement.scale[1]),
0,
image.naturalHeight - crop.height,
),
};
this.scene.mutateElement(croppingElement, {
crop: nextCrop,
});
return;
}
}
}
// Snap cache *must* be synchronously popuplated before initial drag,
// otherwise the first drag even will not snap, causing a jump before
// it snaps to its position if previously snapped already.
@@ -8599,21 +8741,23 @@ class App extends React.Component<AppProps, AppState> {
pointerDownState.lastCoords.x = pointerCoords.x;
pointerDownState.lastCoords.y = pointerCoords.y;
if (event.altKey) {
this.setActiveTool(
{ type: "lasso", fromSelection: true },
event.shiftKey,
);
this.lassoTrail.startPath(
pointerDownState.origin.x,
pointerDownState.origin.y,
event.shiftKey,
);
this.setAppState({
selectionElement: null,
flushSync(() => {
this.setActiveTool(
{ type: "lasso", fromSelection: true },
event.shiftKey,
);
this.lassoTrail.startPath(
pointerDownState.origin.x,
pointerDownState.origin.y,
event.shiftKey,
);
this.setAppState({
selectionElement: null,
});
});
return;
} else {
this.maybeDragNewGenericElement(pointerDownState, event);
}
this.maybeDragNewGenericElement(pointerDownState, event);
} else if (this.state.activeTool.type === "lasso") {
if (!event.altKey && this.state.activeTool.fromSelection) {
this.setActiveTool({ type: "selection" });
@@ -8733,7 +8877,7 @@ class App extends React.Component<AppProps, AppState> {
const elements = this.scene.getNonDeletedElements();
// box-select line editor points
if (this.state.selectedLinearElement?.isEditing) {
if (this.state.editingLinearElement) {
LinearElementEditor.handleBoxSelection(
event,
this.state,
@@ -8885,6 +9029,10 @@ class App extends React.Component<AppProps, AppState> {
isCropping,
} = this.state;
// Clean up crop position movement flag
const wasCropPositionMovement =
pointerDownState.cropPositionMovement.enabled;
this.setState((prevState) => ({
isResizing: false,
isRotating: false,
@@ -8976,23 +9124,23 @@ class App extends React.Component<AppProps, AppState> {
// Handle end of dragging a point of a linear element, might close a loop
// and sets binding element
if (this.state.selectedLinearElement?.isEditing) {
if (this.state.editingLinearElement) {
if (
!pointerDownState.boxSelection.hasOccurred &&
pointerDownState.hit?.element?.id !==
this.state.selectedLinearElement.elementId
this.state.editingLinearElement.elementId
) {
this.actionManager.executeAction(actionFinalize);
} else {
const editingLinearElement = LinearElementEditor.handlePointerUp(
childEvent,
this.state.selectedLinearElement,
this.state.editingLinearElement,
this.state,
this.scene,
);
if (editingLinearElement !== this.state.selectedLinearElement) {
if (editingLinearElement !== this.state.editingLinearElement) {
this.setState({
selectedLinearElement: editingLinearElement,
editingLinearElement,
suggestedBindings: [],
});
}
@@ -9095,54 +9243,25 @@ class App extends React.Component<AppProps, AppState> {
this.state,
);
const dragDistance =
pointDistance(
pointFrom(pointerCoords.x, pointerCoords.y),
pointFrom(pointerDownState.origin.x, pointerDownState.origin.y),
) * this.state.zoom.value;
if (!pointerDownState.drag.hasOccurred && newElement && !multiElement) {
this.scene.mutateElement(
newElement,
{
points: [
...newElement.points,
pointFrom<LocalPoint>(
pointerCoords.x - newElement.x,
pointerCoords.y - newElement.y,
),
],
},
{ informMutation: false, isDragging: false },
);
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(
newElement,
{
x: newElement.x - FIXED_DELTA_X / 2,
points: [
pointFrom<LocalPoint>(0, 0),
pointFrom<LocalPoint>(FIXED_DELTA_X, 0),
],
},
{ 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({
multiElement: newElement,
newElement,
});
}
this.setState({
multiElement: newElement,
newElement,
});
} else if (pointerDownState.drag.hasOccurred && !multiElement) {
if (
isBindingEnabled(this.state) &&
@@ -9453,17 +9572,46 @@ class App extends React.Component<AppProps, AppState> {
}
// click outside the cropping region to exit
if (
// not in the cropping mode at all
!croppingElementId ||
// in the cropping mode
(croppingElementId &&
// not cropping and no hit element
((!hitElement && !isCropping) ||
// hitting something else
(hitElement && hitElement.id !== croppingElementId)))
) {
this.finishImageCropping();
if (croppingElementId) {
const croppingElement = this.scene
.getNonDeletedElementsMap()
.get(croppingElementId);
if (
croppingElement &&
isImageElement(croppingElement) &&
croppingElement.crop
) {
const uncroppedElement = getUncroppedImageElement(
croppingElement,
this.scene.getNonDeletedElementsMap(),
);
const pointer = pointFrom<GlobalPoint>(sceneCoords.x, sceneCoords.y);
const hitUncroppedArea = hitElementItself({
point: pointer,
element: uncroppedElement,
threshold: this.getElementHitThreshold(uncroppedElement),
elementsMap: this.scene.getNonDeletedElementsMap(),
});
if (!hitUncroppedArea) {
this.finishImageCropping();
} else {
// ensure the image remains selected so crop handles are rendered
if (
(!this.state.selectedElementIds ||
Object.keys(this.state.selectedElementIds).length === 0) &&
this.state.croppingElementId
) {
this.setState({
selectedElementIds: { [this.state.croppingElementId]: true },
});
}
}
} else {
// fallback: if not in cropping mode or no cropping element, finish cropping
this.finishImageCropping();
}
}
const pointerStart = this.lastPointerDownEvent;
@@ -9505,17 +9653,14 @@ class App extends React.Component<AppProps, AppState> {
!pointerDownState.hit.wasAddedToSelection &&
// if we're editing a line, pointerup shouldn't switch selection if
// box selected
(!this.state.selectedLinearElement?.isEditing ||
(!this.state.editingLinearElement ||
!pointerDownState.boxSelection.hasOccurred) &&
// hitElement can be set when alt + ctrl to toggle lasso and we will
// just respect the selected elements from lasso instead
this.state.activeTool.type !== "lasso"
) {
// when inside line editor, shift selects points instead
if (
childEvent.shiftKey &&
!this.state.selectedLinearElement?.isEditing
) {
if (childEvent.shiftKey && !this.state.editingLinearElement) {
if (this.state.selectedElementIds[hitElement.id]) {
if (isSelectedViaGroup(this.state, hitElement)) {
this.setState((_prevState) => {
@@ -9677,7 +9822,7 @@ class App extends React.Component<AppProps, AppState> {
((hitElement &&
hitElementBoundingBoxOnly(
{
point: pointFrom(
point: pointFrom<GlobalPoint>(
pointerDownState.origin.x,
pointerDownState.origin.y,
),
@@ -9693,9 +9838,8 @@ class App extends React.Component<AppProps, AppState> {
(!hitElement &&
pointerDownState.hit.hasHitCommonBoundingBoxOfSelectedElements))
) {
if (this.state.selectedLinearElement?.isEditing) {
// Exit editing mode but keep the element selected
this.actionManager.executeAction(actionToggleLinearEditor);
if (this.state.editingLinearElement) {
this.setState({ editingLinearElement: null });
} else {
// Deselect selected elements
this.setState({
@@ -10630,6 +10774,8 @@ class App extends React.Component<AppProps, AppState> {
event[KEYS.CTRL_OR_CMD] ? null : this.getEffectiveGridSize(),
);
console.log("hi");
const croppingElement = this.scene
.getNonDeletedElementsMap()
.get(this.state.croppingElementId);

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

@@ -34,13 +34,6 @@ const StaticCanvas = (props: StaticCanvasProps) => {
const wrapperRef = useRef<HTMLDivElement>(null);
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(() => {
const wrapper = wrapperRef.current;
if (!wrapper) {
@@ -56,6 +49,26 @@ const StaticCanvas = (props: StaticCanvasProps) => {
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(
{
canvas,

View File

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

View File

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

View File

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

View File

@@ -4,13 +4,7 @@ import {
supported as nativeFileSystemSupported,
} from "browser-fs-access";
import {
EVENT,
MIME_TYPES,
debounce,
isIOS,
isAndroid,
} from "@excalidraw/common";
import { EVENT, MIME_TYPES, debounce } from "@excalidraw/common";
import { AbortError } from "../errors";
@@ -19,8 +13,6 @@ import type { FileSystemHandle } from "browser-fs-access";
type FILE_EXTENSION = Exclude<keyof typeof MIME_TYPES, "binary">;
const INPUT_CHANGE_INTERVAL_MS = 500;
// increase timeout for mobile devices to give more time for file selection
const MOBILE_INPUT_CHANGE_INTERVAL_MS = 2000;
export const fileOpen = <M extends boolean | undefined = false>(opts: {
extensions?: FILE_EXTENSION[];
@@ -49,22 +41,13 @@ export const fileOpen = <M extends boolean | undefined = false>(opts: {
mimeTypes,
multiple: opts.multiple ?? false,
legacySetup: (resolve, reject, input) => {
const isMobile = isIOS || isAndroid;
const intervalMs = isMobile
? MOBILE_INPUT_CHANGE_INTERVAL_MS
: INPUT_CHANGE_INTERVAL_MS;
const scheduleRejection = debounce(reject, intervalMs);
const scheduleRejection = debounce(reject, INPUT_CHANGE_INTERVAL_MS);
const focusHandler = () => {
checkForFile();
// on mobile, be less aggressive with rejection
if (!isMobile) {
document.addEventListener(EVENT.KEYUP, scheduleRejection);
document.addEventListener(EVENT.POINTER_UP, scheduleRejection);
scheduleRejection();
}
document.addEventListener(EVENT.KEYUP, scheduleRejection);
document.addEventListener(EVENT.POINTER_UP, scheduleRejection);
scheduleRejection();
};
const checkForFile = () => {
// this hack might not work when expecting multiple files
if (input.files?.length) {
@@ -72,15 +55,12 @@ export const fileOpen = <M extends boolean | undefined = false>(opts: {
resolve(ret as RetType);
}
};
requestAnimationFrame(() => {
window.addEventListener(EVENT.FOCUS, focusHandler);
});
const interval = window.setInterval(() => {
checkForFile();
}, intervalMs);
}, INPUT_CHANGE_INTERVAL_MS);
return (rejectPromise) => {
clearInterval(interval);
scheduleRejection.cancel();
@@ -89,9 +69,7 @@ export const fileOpen = <M extends boolean | undefined = false>(opts: {
document.removeEventListener(EVENT.POINTER_UP, scheduleRejection);
if (rejectPromise) {
// so that something is shown in console if we need to debug this
console.warn(
"Opening the file was canceled (legacy-fs). This may happen on mobile devices.",
);
console.warn("Opening the file was canceled (legacy-fs).");
rejectPromise(new AbortError());
}
};

View File

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

View File

@@ -66,22 +66,12 @@
"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": {
"react": "^17.0.2 || ^18.2.0 || ^19.0.0",
"react-dom": "^17.0.2 || ^18.2.0 || ^19.0.0"
},
"dependencies": {
"@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/mermaid-to-excalidraw": "1.1.2",
"@excalidraw/random-username": "1.1.0",
@@ -134,5 +124,12 @@
"harfbuzzjs": "0.3.6",
"jest-diff": "29.7.0",
"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,8 +118,7 @@ const renderLinearElementPointHighlight = (
) => {
const { elementId, hoverPointIndex } = appState.selectedLinearElement!;
if (
appState.selectedLinearElement?.isEditing &&
appState.selectedLinearElement?.selectedPointsIndices?.includes(
appState.editingLinearElement?.selectedPointsIndices?.includes(
hoverPointIndex,
)
) {
@@ -181,7 +180,7 @@ const renderSingleLinearPoint = <Point extends GlobalPoint | LocalPoint>(
point[0],
point[1],
(isOverlappingPoint
? radius * (appState.selectedLinearElement?.isEditing ? 1.5 : 2)
? radius * (appState.editingLinearElement ? 1.5 : 2)
: radius) / appState.zoom.value,
!isPhantomPoint,
!isOverlappingPoint || isSelected,
@@ -449,7 +448,7 @@ const renderLinearPointHandles = (
);
const { POINT_HANDLE_SIZE } = LinearElementEditor;
const radius = appState.selectedLinearElement?.isEditing
const radius = appState.editingLinearElement
? POINT_HANDLE_SIZE
: POINT_HANDLE_SIZE / 2;
@@ -471,8 +470,7 @@ const renderLinearPointHandles = (
);
let isSelected =
!!appState.selectedLinearElement?.isEditing &&
!!appState.selectedLinearElement?.selectedPointsIndices?.includes(idx);
!!appState.editingLinearElement?.selectedPointsIndices?.includes(idx);
// 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
// rendered on top
@@ -481,8 +479,7 @@ const renderLinearPointHandles = (
element.polygon &&
!isSelected &&
idx === element.points.length - 1 &&
!!appState.selectedLinearElement?.isEditing &&
!!appState.selectedLinearElement?.selectedPointsIndices?.includes(0)
!!appState.editingLinearElement?.selectedPointsIndices?.includes(0)
) {
isSelected = true;
}
@@ -538,7 +535,7 @@ const renderLinearPointHandles = (
);
midPoints.forEach((segmentMidPoint) => {
if (appState.selectedLinearElement?.isEditing || points.length === 2) {
if (appState.editingLinearElement || points.length === 2) {
renderSingleLinearPoint(
context,
appState,
@@ -763,10 +760,7 @@ const _renderInteractiveScene = ({
// 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
// correct element from visible elements
if (
appState.selectedLinearElement?.isEditing &&
appState.selectedLinearElement.elementId === element.id
) {
if (appState.editingLinearElement?.elementId === element.id) {
if (element) {
editingLinearElement = element as NonDeleted<ExcalidrawLinearElement>;
}
@@ -859,8 +853,7 @@ const _renderInteractiveScene = ({
// correct element from visible elements
if (
selectedElements.length === 1 &&
appState.selectedLinearElement?.isEditing &&
appState.selectedLinearElement.elementId === selectedElements[0].id
appState.editingLinearElement?.elementId === selectedElements[0].id
) {
renderLinearPointHandles(
context,
@@ -891,7 +884,7 @@ const _renderInteractiveScene = ({
}
// Paint selected elements
if (!appState.multiElement && !appState.selectedLinearElement?.isEditing) {
if (!appState.multiElement && !appState.editingLinearElement) {
const showBoundingBox = shouldShowBoundingBox(selectedElements, appState);
const isSingleLinearElementSelected =

View File

@@ -1,16 +1,9 @@
import { throttleRAF } from "@excalidraw/common";
import {
getTargetFrame,
isInvisiblySmallElement,
renderElement,
shouldApplyFrameClip,
} from "@excalidraw/element";
import { renderElement } from "@excalidraw/element";
import { bootstrapCanvas, getNormalizedCanvasDimensions } from "./helpers";
import { frameClip } from "./staticScene";
import type { NewElementSceneRenderConfig } from "../scene/types";
const _renderNewElementScene = ({
@@ -36,37 +29,11 @@ const _renderNewElementScene = ({
normalizedHeight,
});
context.save();
// Apply zoom
context.save();
context.scale(appState.zoom.value, appState.zoom.value);
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(
newElement,
elementsMap,
@@ -79,8 +46,6 @@ const _renderNewElementScene = ({
} else {
context.clearRect(0, 0, normalizedWidth, normalizedHeight);
}
context.restore();
}
};

View File

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

View File

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

View File

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

View File

@@ -15,11 +15,7 @@ exports[`<Excalidraw/> > <MainMenu/> > should render main menu with host menu it
<div
class="dropdown-menu-item__text"
>
<span
style="text-overflow: ellipsis; overflow: hidden; white-space: nowrap;"
>
Click me
</span>
Click me
</div>
</button>
<a
@@ -31,11 +27,7 @@ exports[`<Excalidraw/> > <MainMenu/> > should render main menu with host menu it
<div
class="dropdown-menu-item__text"
>
<span
style="text-overflow: ellipsis; overflow: hidden; white-space: nowrap;"
>
Excalidraw blog
</span>
Excalidraw blog
</div>
</a>
<div
@@ -96,11 +88,7 @@ exports[`<Excalidraw/> > <MainMenu/> > should render main menu with host menu it
<div
class="dropdown-menu-item__text"
>
<span
style="text-overflow: ellipsis; overflow: hidden; white-space: nowrap;"
>
Help
</span>
Help
</div>
<div
class="dropdown-menu-item__shortcut"
@@ -150,11 +138,7 @@ exports[`<Excalidraw/> > Test UIOptions prop > Test canvasActions > should rende
<div
class="dropdown-menu-item__text"
>
<span
style="text-overflow: ellipsis; overflow: hidden; white-space: nowrap;"
>
Open
</span>
Open
</div>
<div
class="dropdown-menu-item__shortcut"
@@ -191,11 +175,7 @@ exports[`<Excalidraw/> > Test UIOptions prop > Test canvasActions > should rende
<div
class="dropdown-menu-item__text"
>
<span
style="text-overflow: ellipsis; overflow: hidden; white-space: nowrap;"
>
Save to...
</span>
Save to...
</div>
</button>
<button
@@ -251,11 +231,7 @@ exports[`<Excalidraw/> > Test UIOptions prop > Test canvasActions > should rende
<div
class="dropdown-menu-item__text"
>
<span
style="text-overflow: ellipsis; overflow: hidden; white-space: nowrap;"
>
Export image...
</span>
Export image...
</div>
<div
class="dropdown-menu-item__shortcut"
@@ -304,11 +280,7 @@ exports[`<Excalidraw/> > Test UIOptions prop > Test canvasActions > should rende
<div
class="dropdown-menu-item__text"
>
<span
style="text-overflow: ellipsis; overflow: hidden; white-space: nowrap;"
>
Find on canvas
</span>
Find on canvas
</div>
<div
class="dropdown-menu-item__shortcut"
@@ -365,11 +337,7 @@ exports[`<Excalidraw/> > Test UIOptions prop > Test canvasActions > should rende
<div
class="dropdown-menu-item__text"
>
<span
style="text-overflow: ellipsis; overflow: hidden; white-space: nowrap;"
>
Help
</span>
Help
</div>
<div
class="dropdown-menu-item__shortcut"
@@ -406,11 +374,7 @@ exports[`<Excalidraw/> > Test UIOptions prop > Test canvasActions > should rende
<div
class="dropdown-menu-item__text"
>
<span
style="text-overflow: ellipsis; overflow: hidden; white-space: nowrap;"
>
Reset the canvas
</span>
Reset the canvas
</div>
</button>
<div
@@ -455,11 +419,7 @@ exports[`<Excalidraw/> > Test UIOptions prop > Test canvasActions > should rende
<div
class="dropdown-menu-item__text"
>
<span
style="text-overflow: ellipsis; overflow: hidden; white-space: nowrap;"
>
GitHub
</span>
GitHub
</div>
</a>
<a
@@ -505,11 +465,7 @@ exports[`<Excalidraw/> > Test UIOptions prop > Test canvasActions > should rende
<div
class="dropdown-menu-item__text"
>
<span
style="text-overflow: ellipsis; overflow: hidden; white-space: nowrap;"
>
Follow us
</span>
Follow us
</div>
</a>
<a
@@ -549,11 +505,7 @@ exports[`<Excalidraw/> > Test UIOptions prop > Test canvasActions > should rende
<div
class="dropdown-menu-item__text"
>
<span
style="text-overflow: ellipsis; overflow: hidden; white-space: nowrap;"
>
Discord chat
</span>
Discord chat
</div>
</a>
</div>
@@ -590,11 +542,7 @@ exports[`<Excalidraw/> > Test UIOptions prop > Test canvasActions > should rende
<div
class="dropdown-menu-item__text"
>
<span
style="text-overflow: ellipsis; overflow: hidden; white-space: nowrap;"
>
Dark mode
</span>
Dark mode
</div>
<div
class="dropdown-menu-item__shortcut"

View File

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

View File

@@ -34,6 +34,7 @@ exports[`given element A and group of elements B and given both are selected whe
"defaultSidebarDockedPreference": false,
"editingFrame": null,
"editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null,
"elementsToHighlight": null,
"errorMessage": null,
@@ -458,6 +459,7 @@ exports[`given element A and group of elements B and given both are selected whe
"defaultSidebarDockedPreference": false,
"editingFrame": null,
"editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null,
"elementsToHighlight": null,
"errorMessage": null,
@@ -872,6 +874,7 @@ exports[`regression tests > Cmd/Ctrl-click exclusively select element under poin
"defaultSidebarDockedPreference": false,
"editingFrame": null,
"editingGroupId": "id28",
"editingLinearElement": null,
"editingTextElement": null,
"elementsToHighlight": null,
"errorMessage": null,
@@ -1436,6 +1439,7 @@ exports[`regression tests > Drags selected element when hitting only bounding bo
"defaultSidebarDockedPreference": false,
"editingFrame": null,
"editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null,
"elementsToHighlight": null,
"errorMessage": null,
@@ -1641,6 +1645,7 @@ exports[`regression tests > adjusts z order when grouping > [end of test] appSta
"defaultSidebarDockedPreference": false,
"editingFrame": null,
"editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null,
"elementsToHighlight": null,
"errorMessage": null,
@@ -2023,6 +2028,7 @@ exports[`regression tests > alt-drag duplicates an element > [end of test] appSt
"defaultSidebarDockedPreference": false,
"editingFrame": null,
"editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null,
"elementsToHighlight": null,
"errorMessage": null,
@@ -2266,6 +2272,7 @@ exports[`regression tests > arrow keys > [end of test] appState 1`] = `
"defaultSidebarDockedPreference": false,
"editingFrame": null,
"editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null,
"elementsToHighlight": null,
"errorMessage": null,
@@ -2444,6 +2451,7 @@ exports[`regression tests > can drag element that covers another element, while
"defaultSidebarDockedPreference": false,
"editingFrame": null,
"editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null,
"elementsToHighlight": null,
"errorMessage": null,
@@ -2767,6 +2775,7 @@ exports[`regression tests > change the properties of a shape > [end of test] app
"defaultSidebarDockedPreference": false,
"editingFrame": null,
"editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null,
"elementsToHighlight": null,
"errorMessage": null,
@@ -3020,6 +3029,7 @@ exports[`regression tests > click on an element and drag it > [dragged] appState
"defaultSidebarDockedPreference": false,
"editingFrame": null,
"editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null,
"elementsToHighlight": null,
"errorMessage": null,
@@ -3259,6 +3269,7 @@ exports[`regression tests > click on an element and drag it > [end of test] appS
"defaultSidebarDockedPreference": false,
"editingFrame": null,
"editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null,
"elementsToHighlight": null,
"errorMessage": null,
@@ -3493,6 +3504,7 @@ exports[`regression tests > click to select a shape > [end of test] appState 1`]
"defaultSidebarDockedPreference": false,
"editingFrame": null,
"editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null,
"elementsToHighlight": null,
"errorMessage": null,
@@ -3749,6 +3761,7 @@ exports[`regression tests > click-drag to select a group > [end of test] appStat
"defaultSidebarDockedPreference": false,
"editingFrame": null,
"editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null,
"elementsToHighlight": null,
"errorMessage": null,
@@ -4061,6 +4074,7 @@ exports[`regression tests > deleting last but one element in editing group shoul
"defaultSidebarDockedPreference": false,
"editingFrame": null,
"editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null,
"elementsToHighlight": null,
"errorMessage": null,
@@ -4495,6 +4509,7 @@ exports[`regression tests > deselects group of selected elements on pointer down
"defaultSidebarDockedPreference": false,
"editingFrame": null,
"editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null,
"elementsToHighlight": null,
"errorMessage": null,
@@ -4776,6 +4791,7 @@ exports[`regression tests > deselects group of selected elements on pointer up w
"defaultSidebarDockedPreference": false,
"editingFrame": null,
"editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null,
"elementsToHighlight": null,
"errorMessage": null,
@@ -5050,6 +5066,7 @@ exports[`regression tests > deselects selected element on pointer down when poin
"defaultSidebarDockedPreference": false,
"editingFrame": null,
"editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null,
"elementsToHighlight": null,
"errorMessage": null,
@@ -5256,6 +5273,7 @@ exports[`regression tests > deselects selected element, on pointer up, when clic
"defaultSidebarDockedPreference": false,
"editingFrame": null,
"editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null,
"elementsToHighlight": null,
"errorMessage": null,
@@ -5454,6 +5472,7 @@ exports[`regression tests > double click to edit a group > [end of test] appStat
"defaultSidebarDockedPreference": false,
"editingFrame": null,
"editingGroupId": "id11",
"editingLinearElement": null,
"editingTextElement": null,
"elementsToHighlight": null,
"errorMessage": null,
@@ -5845,6 +5864,7 @@ exports[`regression tests > drags selected elements from point inside common bou
"defaultSidebarDockedPreference": false,
"editingFrame": null,
"editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null,
"elementsToHighlight": null,
"errorMessage": null,
@@ -6140,6 +6160,7 @@ exports[`regression tests > draw every type of shape > [end of test] appState 1`
"defaultSidebarDockedPreference": false,
"editingFrame": null,
"editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null,
"elementsToHighlight": null,
"errorMessage": null,
@@ -6395,14 +6416,12 @@ exports[`regression tests > draw every type of shape > [end of test] undo stack
"id9": true,
},
"selectedLinearElementId": "id9",
"selectedLinearElementIsEditing": false,
},
"inserted": {
"selectedElementIds": {
"id6": true,
},
"selectedLinearElementId": null,
"selectedLinearElementIsEditing": null,
},
},
},
@@ -6543,14 +6562,12 @@ exports[`regression tests > draw every type of shape > [end of test] undo stack
"id15": true,
},
"selectedLinearElementId": null,
"selectedLinearElementIsEditing": null,
},
"inserted": {
"selectedElementIds": {
"id12": true,
},
"selectedLinearElementId": "id12",
"selectedLinearElementIsEditing": false,
},
},
},
@@ -6678,11 +6695,9 @@ exports[`regression tests > draw every type of shape > [end of test] undo stack
"delta": Delta {
"deleted": {
"selectedLinearElementId": "id15",
"selectedLinearElementIsEditing": false,
},
"inserted": {
"selectedLinearElementId": null,
"selectedLinearElementIsEditing": null,
},
},
},
@@ -6701,14 +6716,12 @@ exports[`regression tests > draw every type of shape > [end of test] undo stack
"id22": true,
},
"selectedLinearElementId": null,
"selectedLinearElementIsEditing": null,
},
"inserted": {
"selectedElementIds": {
"id15": true,
},
"selectedLinearElementId": "id15",
"selectedLinearElementIsEditing": false,
},
},
},
@@ -6834,11 +6847,9 @@ exports[`regression tests > draw every type of shape > [end of test] undo stack
"delta": Delta {
"deleted": {
"selectedLinearElementId": "id22",
"selectedLinearElementIsEditing": false,
},
"inserted": {
"selectedLinearElementId": null,
"selectedLinearElementIsEditing": null,
},
},
},
@@ -6874,11 +6885,9 @@ exports[`regression tests > draw every type of shape > [end of test] undo stack
"delta": Delta {
"deleted": {
"selectedLinearElementId": null,
"selectedLinearElementIsEditing": null,
},
"inserted": {
"selectedLinearElementId": "id22",
"selectedLinearElementIsEditing": false,
},
},
},
@@ -6982,6 +6991,7 @@ exports[`regression tests > given a group of selected elements with an element t
"defaultSidebarDockedPreference": false,
"editingFrame": null,
"editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null,
"elementsToHighlight": null,
"errorMessage": null,
@@ -7314,6 +7324,7 @@ exports[`regression tests > given a selected element A and a not selected elemen
"defaultSidebarDockedPreference": false,
"editingFrame": null,
"editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null,
"elementsToHighlight": null,
"errorMessage": null,
@@ -7591,6 +7602,7 @@ exports[`regression tests > given selected element A with lower z-index than uns
"defaultSidebarDockedPreference": false,
"editingFrame": null,
"editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null,
"elementsToHighlight": null,
"errorMessage": null,
@@ -7824,6 +7836,7 @@ exports[`regression tests > given selected element A with lower z-index than uns
"defaultSidebarDockedPreference": false,
"editingFrame": null,
"editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null,
"elementsToHighlight": null,
"errorMessage": null,
@@ -8062,6 +8075,7 @@ exports[`regression tests > key 2 selects rectangle tool > [end of test] appStat
"defaultSidebarDockedPreference": false,
"editingFrame": null,
"editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null,
"elementsToHighlight": null,
"errorMessage": null,
@@ -8176,7 +8190,7 @@ exports[`regression tests > key 2 selects rectangle tool > [end of test] undo st
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
"height": 30,
"height": 10,
"index": "a0",
"isDeleted": false,
"link": null,
@@ -8189,7 +8203,7 @@ exports[`regression tests > key 2 selects rectangle tool > [end of test] undo st
"strokeWidth": 2,
"type": "rectangle",
"version": 3,
"width": 30,
"width": 10,
"x": 10,
"y": 10,
},
@@ -8240,6 +8254,7 @@ exports[`regression tests > key 3 selects diamond tool > [end of test] appState
"defaultSidebarDockedPreference": false,
"editingFrame": null,
"editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null,
"elementsToHighlight": null,
"errorMessage": null,
@@ -8354,7 +8369,7 @@ exports[`regression tests > key 3 selects diamond tool > [end of test] undo stac
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
"height": 30,
"height": 10,
"index": "a0",
"isDeleted": false,
"link": null,
@@ -8367,7 +8382,7 @@ exports[`regression tests > key 3 selects diamond tool > [end of test] undo stac
"strokeWidth": 2,
"type": "diamond",
"version": 3,
"width": 30,
"width": 10,
"x": 10,
"y": 10,
},
@@ -8418,6 +8433,7 @@ exports[`regression tests > key 4 selects ellipse tool > [end of test] appState
"defaultSidebarDockedPreference": false,
"editingFrame": null,
"editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null,
"elementsToHighlight": null,
"errorMessage": null,
@@ -8532,7 +8548,7 @@ exports[`regression tests > key 4 selects ellipse tool > [end of test] undo stac
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
"height": 30,
"height": 10,
"index": "a0",
"isDeleted": false,
"link": null,
@@ -8545,7 +8561,7 @@ exports[`regression tests > key 4 selects ellipse tool > [end of test] undo stac
"strokeWidth": 2,
"type": "ellipse",
"version": 3,
"width": 30,
"width": 10,
"x": 10,
"y": 10,
},
@@ -8596,6 +8612,7 @@ exports[`regression tests > key 5 selects arrow tool > [end of test] appState 1`
"defaultSidebarDockedPreference": false,
"editingFrame": null,
"editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null,
"elementsToHighlight": null,
"errorMessage": null,
@@ -8659,7 +8676,6 @@ exports[`regression tests > key 5 selects arrow tool > [end of test] appState 1`
"endBindingElement": "keep",
"hoverPointIndex": -1,
"isDragging": false,
"isEditing": false,
"lastUncommittedPoint": null,
"pointerDownState": {
"lastClickedIsEndPoint": false,
@@ -8720,12 +8736,10 @@ exports[`regression tests > key 5 selects arrow tool > [end of test] undo stack
"id0": true,
},
"selectedLinearElementId": "id0",
"selectedLinearElementIsEditing": false,
},
"inserted": {
"selectedElementIds": {},
"selectedLinearElementId": null,
"selectedLinearElementIsEditing": null,
},
},
},
@@ -8744,7 +8758,7 @@ exports[`regression tests > key 5 selects arrow tool > [end of test] undo stack
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
"height": 30,
"height": 10,
"index": "a0",
"isDeleted": false,
"lastCommittedPoint": null,
@@ -8757,8 +8771,8 @@ exports[`regression tests > key 5 selects arrow tool > [end of test] undo stack
0,
],
[
30,
30,
10,
10,
],
],
"roughness": 1,
@@ -8772,7 +8786,7 @@ exports[`regression tests > key 5 selects arrow tool > [end of test] undo stack
"strokeWidth": 2,
"type": "arrow",
"version": 4,
"width": 30,
"width": 10,
"x": 10,
"y": 10,
},
@@ -8823,6 +8837,7 @@ exports[`regression tests > key 6 selects line tool > [end of test] appState 1`]
"defaultSidebarDockedPreference": false,
"editingFrame": null,
"editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null,
"elementsToHighlight": null,
"errorMessage": null,
@@ -8886,7 +8901,6 @@ exports[`regression tests > key 6 selects line tool > [end of test] appState 1`]
"endBindingElement": "keep",
"hoverPointIndex": -1,
"isDragging": false,
"isEditing": false,
"lastUncommittedPoint": null,
"pointerDownState": {
"lastClickedIsEndPoint": false,
@@ -8947,12 +8961,10 @@ exports[`regression tests > key 6 selects line tool > [end of test] undo stack 1
"id0": true,
},
"selectedLinearElementId": "id0",
"selectedLinearElementIsEditing": false,
},
"inserted": {
"selectedElementIds": {},
"selectedLinearElementId": null,
"selectedLinearElementIsEditing": null,
},
},
},
@@ -8970,7 +8982,7 @@ exports[`regression tests > key 6 selects line tool > [end of test] undo stack 1
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
"height": 30,
"height": 10,
"index": "a0",
"isDeleted": false,
"lastCommittedPoint": null,
@@ -8983,8 +8995,8 @@ exports[`regression tests > key 6 selects line tool > [end of test] undo stack 1
0,
],
[
30,
30,
10,
10,
],
],
"polygon": false,
@@ -8997,7 +9009,7 @@ exports[`regression tests > key 6 selects line tool > [end of test] undo stack 1
"strokeWidth": 2,
"type": "line",
"version": 4,
"width": 30,
"width": 10,
"x": 10,
"y": 10,
},
@@ -9048,6 +9060,7 @@ exports[`regression tests > key 7 selects freedraw tool > [end of test] appState
"defaultSidebarDockedPreference": false,
"editingFrame": null,
"editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null,
"elementsToHighlight": null,
"errorMessage": null,
@@ -9154,12 +9167,12 @@ exports[`regression tests > key 7 selects freedraw tool > [end of test] undo sta
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
"height": 30,
"height": 10,
"index": "a0",
"isDeleted": false,
"lastCommittedPoint": [
30,
30,
10,
10,
],
"link": null,
"locked": false,
@@ -9170,12 +9183,12 @@ exports[`regression tests > key 7 selects freedraw tool > [end of test] undo sta
0,
],
[
30,
30,
10,
10,
],
[
30,
30,
10,
10,
],
],
"pressures": [
@@ -9191,7 +9204,7 @@ exports[`regression tests > key 7 selects freedraw tool > [end of test] undo sta
"strokeWidth": 2,
"type": "freedraw",
"version": 4,
"width": 30,
"width": 10,
"x": 10,
"y": 10,
},
@@ -9242,6 +9255,7 @@ exports[`regression tests > key a selects arrow tool > [end of test] appState 1`
"defaultSidebarDockedPreference": false,
"editingFrame": null,
"editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null,
"elementsToHighlight": null,
"errorMessage": null,
@@ -9305,7 +9319,6 @@ exports[`regression tests > key a selects arrow tool > [end of test] appState 1`
"endBindingElement": "keep",
"hoverPointIndex": -1,
"isDragging": false,
"isEditing": false,
"lastUncommittedPoint": null,
"pointerDownState": {
"lastClickedIsEndPoint": false,
@@ -9366,12 +9379,10 @@ exports[`regression tests > key a selects arrow tool > [end of test] undo stack
"id0": true,
},
"selectedLinearElementId": "id0",
"selectedLinearElementIsEditing": false,
},
"inserted": {
"selectedElementIds": {},
"selectedLinearElementId": null,
"selectedLinearElementIsEditing": null,
},
},
},
@@ -9390,7 +9401,7 @@ exports[`regression tests > key a selects arrow tool > [end of test] undo stack
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
"height": 30,
"height": 10,
"index": "a0",
"isDeleted": false,
"lastCommittedPoint": null,
@@ -9403,8 +9414,8 @@ exports[`regression tests > key a selects arrow tool > [end of test] undo stack
0,
],
[
30,
30,
10,
10,
],
],
"roughness": 1,
@@ -9418,7 +9429,7 @@ exports[`regression tests > key a selects arrow tool > [end of test] undo stack
"strokeWidth": 2,
"type": "arrow",
"version": 4,
"width": 30,
"width": 10,
"x": 10,
"y": 10,
},
@@ -9469,6 +9480,7 @@ exports[`regression tests > key d selects diamond tool > [end of test] appState
"defaultSidebarDockedPreference": false,
"editingFrame": null,
"editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null,
"elementsToHighlight": null,
"errorMessage": null,
@@ -9583,7 +9595,7 @@ exports[`regression tests > key d selects diamond tool > [end of test] undo stac
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
"height": 30,
"height": 10,
"index": "a0",
"isDeleted": false,
"link": null,
@@ -9596,7 +9608,7 @@ exports[`regression tests > key d selects diamond tool > [end of test] undo stac
"strokeWidth": 2,
"type": "diamond",
"version": 3,
"width": 30,
"width": 10,
"x": 10,
"y": 10,
},
@@ -9647,6 +9659,7 @@ exports[`regression tests > key l selects line tool > [end of test] appState 1`]
"defaultSidebarDockedPreference": false,
"editingFrame": null,
"editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null,
"elementsToHighlight": null,
"errorMessage": null,
@@ -9710,7 +9723,6 @@ exports[`regression tests > key l selects line tool > [end of test] appState 1`]
"endBindingElement": "keep",
"hoverPointIndex": -1,
"isDragging": false,
"isEditing": false,
"lastUncommittedPoint": null,
"pointerDownState": {
"lastClickedIsEndPoint": false,
@@ -9771,12 +9783,10 @@ exports[`regression tests > key l selects line tool > [end of test] undo stack 1
"id0": true,
},
"selectedLinearElementId": "id0",
"selectedLinearElementIsEditing": false,
},
"inserted": {
"selectedElementIds": {},
"selectedLinearElementId": null,
"selectedLinearElementIsEditing": null,
},
},
},
@@ -9794,7 +9804,7 @@ exports[`regression tests > key l selects line tool > [end of test] undo stack 1
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
"height": 30,
"height": 10,
"index": "a0",
"isDeleted": false,
"lastCommittedPoint": null,
@@ -9807,8 +9817,8 @@ exports[`regression tests > key l selects line tool > [end of test] undo stack 1
0,
],
[
30,
30,
10,
10,
],
],
"polygon": false,
@@ -9821,7 +9831,7 @@ exports[`regression tests > key l selects line tool > [end of test] undo stack 1
"strokeWidth": 2,
"type": "line",
"version": 4,
"width": 30,
"width": 10,
"x": 10,
"y": 10,
},
@@ -9872,6 +9882,7 @@ exports[`regression tests > key o selects ellipse tool > [end of test] appState
"defaultSidebarDockedPreference": false,
"editingFrame": null,
"editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null,
"elementsToHighlight": null,
"errorMessage": null,
@@ -9986,7 +9997,7 @@ exports[`regression tests > key o selects ellipse tool > [end of test] undo stac
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
"height": 30,
"height": 10,
"index": "a0",
"isDeleted": false,
"link": null,
@@ -9999,7 +10010,7 @@ exports[`regression tests > key o selects ellipse tool > [end of test] undo stac
"strokeWidth": 2,
"type": "ellipse",
"version": 3,
"width": 30,
"width": 10,
"x": 10,
"y": 10,
},
@@ -10050,6 +10061,7 @@ exports[`regression tests > key p selects freedraw tool > [end of test] appState
"defaultSidebarDockedPreference": false,
"editingFrame": null,
"editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null,
"elementsToHighlight": null,
"errorMessage": null,
@@ -10156,12 +10168,12 @@ exports[`regression tests > key p selects freedraw tool > [end of test] undo sta
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
"height": 30,
"height": 10,
"index": "a0",
"isDeleted": false,
"lastCommittedPoint": [
30,
30,
10,
10,
],
"link": null,
"locked": false,
@@ -10172,12 +10184,12 @@ exports[`regression tests > key p selects freedraw tool > [end of test] undo sta
0,
],
[
30,
30,
10,
10,
],
[
30,
30,
10,
10,
],
],
"pressures": [
@@ -10193,7 +10205,7 @@ exports[`regression tests > key p selects freedraw tool > [end of test] undo sta
"strokeWidth": 2,
"type": "freedraw",
"version": 4,
"width": 30,
"width": 10,
"x": 10,
"y": 10,
},
@@ -10244,6 +10256,7 @@ exports[`regression tests > key r selects rectangle tool > [end of test] appStat
"defaultSidebarDockedPreference": false,
"editingFrame": null,
"editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null,
"elementsToHighlight": null,
"errorMessage": null,
@@ -10358,7 +10371,7 @@ exports[`regression tests > key r selects rectangle tool > [end of test] undo st
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
"height": 30,
"height": 10,
"index": "a0",
"isDeleted": false,
"link": null,
@@ -10371,7 +10384,7 @@ exports[`regression tests > key r selects rectangle tool > [end of test] undo st
"strokeWidth": 2,
"type": "rectangle",
"version": 3,
"width": 30,
"width": 10,
"x": 10,
"y": 10,
},
@@ -10422,6 +10435,7 @@ exports[`regression tests > make a group and duplicate it > [end of test] appSta
"defaultSidebarDockedPreference": false,
"editingFrame": null,
"editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null,
"elementsToHighlight": null,
"errorMessage": null,
@@ -10951,6 +10965,7 @@ exports[`regression tests > noop interaction after undo shouldn't create history
"defaultSidebarDockedPreference": false,
"editingFrame": null,
"editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null,
"elementsToHighlight": null,
"errorMessage": null,
@@ -11229,6 +11244,7 @@ exports[`regression tests > pinch-to-zoom works > [end of test] appState 1`] = `
"defaultSidebarDockedPreference": false,
"editingFrame": null,
"editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null,
"elementsToHighlight": null,
"errorMessage": null,
@@ -11350,6 +11366,7 @@ exports[`regression tests > shift click on selected element should deselect it o
"defaultSidebarDockedPreference": false,
"editingFrame": null,
"editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null,
"elementsToHighlight": null,
"errorMessage": null,
@@ -11548,6 +11565,7 @@ exports[`regression tests > shift-click to multiselect, then drag > [end of test
"defaultSidebarDockedPreference": false,
"editingFrame": null,
"editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null,
"elementsToHighlight": null,
"errorMessage": null,
@@ -11865,6 +11883,7 @@ exports[`regression tests > should group elements and ungroup them > [end of tes
"defaultSidebarDockedPreference": false,
"editingFrame": null,
"editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null,
"elementsToHighlight": null,
"errorMessage": null,
@@ -12292,6 +12311,7 @@ exports[`regression tests > single-clicking on a subgroup of a selected group sh
"defaultSidebarDockedPreference": false,
"editingFrame": null,
"editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null,
"elementsToHighlight": null,
"errorMessage": null,
@@ -12930,6 +12950,7 @@ exports[`regression tests > spacebar + drag scrolls the canvas > [end of test] a
"defaultSidebarDockedPreference": false,
"editingFrame": null,
"editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null,
"elementsToHighlight": null,
"errorMessage": null,
@@ -13054,6 +13075,7 @@ exports[`regression tests > supports nested groups > [end of test] appState 1`]
"defaultSidebarDockedPreference": false,
"editingFrame": null,
"editingGroupId": "id11",
"editingLinearElement": null,
"editingTextElement": null,
"elementsToHighlight": null,
"errorMessage": null,
@@ -13683,6 +13705,7 @@ exports[`regression tests > switches from group of selected elements to another
"defaultSidebarDockedPreference": false,
"editingFrame": null,
"editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null,
"elementsToHighlight": null,
"errorMessage": null,
@@ -14020,6 +14043,7 @@ exports[`regression tests > switches selected element on pointer down > [end of
"defaultSidebarDockedPreference": false,
"editingFrame": null,
"editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null,
"elementsToHighlight": null,
"errorMessage": null,
@@ -14282,6 +14306,7 @@ exports[`regression tests > two-finger scroll works > [end of test] appState 1`]
"defaultSidebarDockedPreference": false,
"editingFrame": null,
"editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null,
"elementsToHighlight": null,
"errorMessage": null,
@@ -14403,6 +14428,7 @@ exports[`regression tests > undo/redo drawing an element > [end of test] appStat
"defaultSidebarDockedPreference": false,
"editingFrame": null,
"editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null,
"elementsToHighlight": null,
"errorMessage": null,
@@ -14495,11 +14521,9 @@ exports[`regression tests > undo/redo drawing an element > [end of test] redo st
"delta": Delta {
"deleted": {
"selectedLinearElementId": null,
"selectedLinearElementIsEditing": null,
},
"inserted": {
"selectedLinearElementId": "id6",
"selectedLinearElementIsEditing": false,
},
},
},
@@ -14792,6 +14816,7 @@ exports[`regression tests > updates fontSize & fontFamily appState > [end of tes
"defaultSidebarDockedPreference": false,
"editingFrame": null,
"editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null,
"elementsToHighlight": null,
"errorMessage": null,
@@ -14913,6 +14938,7 @@ exports[`regression tests > zoom hotkeys > [end of test] appState 1`] = `
"defaultSidebarDockedPreference": false,
"editingFrame": null,
"editingGroupId": null,
"editingLinearElement": null,
"editingTextElement": null,
"elementsToHighlight": null,
"errorMessage": null,

View File

@@ -1073,7 +1073,7 @@ describe("history", () => {
expect(API.getUndoStack().length).toBe(6);
expect(API.getRedoStack().length).toBe(0);
expect(assertSelectedElements(h.elements[0]));
expect(h.state.selectedLinearElement?.isEditing ?? false).toBe(false);
expect(h.state.editingLinearElement).toBeNull();
expect(h.state.selectedLinearElement).not.toBeNull();
expect(h.elements).toEqual([
expect.objectContaining({
@@ -1090,7 +1090,7 @@ describe("history", () => {
expect(API.getUndoStack().length).toBe(5);
expect(API.getRedoStack().length).toBe(1);
expect(assertSelectedElements(h.elements[0]));
expect(h.state.selectedLinearElement?.isEditing).toBe(true);
expect(h.state.editingLinearElement?.elementId).toBe(h.elements[0].id);
expect(h.state.selectedLinearElement?.elementId).toBe(h.elements[0].id);
expect(h.elements).toEqual([
expect.objectContaining({
@@ -1114,7 +1114,7 @@ describe("history", () => {
expect(API.getUndoStack().length).toBe(4);
expect(API.getRedoStack().length).toBe(2);
expect(assertSelectedElements(h.elements[0]));
expect(h.state.selectedLinearElement?.isEditing).toBe(true);
expect(h.state.editingLinearElement?.elementId).toBe(h.elements[0].id);
expect(h.state.selectedLinearElement?.elementId).toBe(h.elements[0].id);
expect(h.elements).toEqual([
expect.objectContaining({
@@ -1131,7 +1131,7 @@ describe("history", () => {
expect(API.getUndoStack().length).toBe(3);
expect(API.getRedoStack().length).toBe(3);
expect(assertSelectedElements(h.elements[0]));
expect(h.state.selectedLinearElement?.isEditing ?? false).toBe(false); // undo `open editor`
expect(h.state.editingLinearElement).toBeNull(); // undo `open editor`
expect(h.state.selectedLinearElement?.elementId).toBe(h.elements[0].id);
expect(h.elements).toEqual([
expect.objectContaining({
@@ -1148,7 +1148,7 @@ describe("history", () => {
expect(API.getUndoStack().length).toBe(2);
expect(API.getRedoStack().length).toBe(4);
expect(assertSelectedElements(h.elements[0]));
expect(h.state.selectedLinearElement?.isEditing ?? false).toBe(false);
expect(h.state.editingLinearElement).toBeNull();
expect(h.state.selectedLinearElement).toBeNull(); // undo `actionFinalize`
expect(h.elements).toEqual([
expect.objectContaining({
@@ -1165,7 +1165,7 @@ describe("history", () => {
expect(API.getUndoStack().length).toBe(1);
expect(API.getRedoStack().length).toBe(5);
expect(assertSelectedElements(h.elements[0]));
expect(h.state.selectedLinearElement?.isEditing ?? false).toBe(false);
expect(h.state.editingLinearElement).toBeNull();
expect(h.state.selectedLinearElement).toBeNull();
expect(h.elements).toEqual([
expect.objectContaining({
@@ -1181,7 +1181,7 @@ describe("history", () => {
expect(API.getUndoStack().length).toBe(0);
expect(API.getRedoStack().length).toBe(6);
expect(API.getSelectedElements().length).toBe(0);
expect(h.state.selectedLinearElement?.isEditing ?? false).toBe(false);
expect(h.state.editingLinearElement).toBeNull();
expect(h.state.selectedLinearElement).toBeNull();
expect(h.elements).toEqual([
expect.objectContaining({
@@ -1197,7 +1197,7 @@ describe("history", () => {
expect(API.getUndoStack().length).toBe(1);
expect(API.getRedoStack().length).toBe(5);
expect(assertSelectedElements(h.elements[0]));
expect(h.state.selectedLinearElement?.isEditing ?? false).toBe(false);
expect(h.state.editingLinearElement).toBeNull();
expect(h.state.selectedLinearElement).toBeNull();
expect(h.elements).toEqual([
expect.objectContaining({
@@ -1213,7 +1213,7 @@ describe("history", () => {
expect(API.getUndoStack().length).toBe(2);
expect(API.getRedoStack().length).toBe(4);
expect(assertSelectedElements(h.elements[0]));
expect(h.state.selectedLinearElement?.isEditing ?? false).toBe(false);
expect(h.state.editingLinearElement).toBeNull();
expect(h.state.selectedLinearElement).toBeNull(); // undo `actionFinalize`
expect(h.elements).toEqual([
expect.objectContaining({
@@ -1230,7 +1230,7 @@ describe("history", () => {
expect(API.getUndoStack().length).toBe(3);
expect(API.getRedoStack().length).toBe(3);
expect(assertSelectedElements(h.elements[0]));
expect(h.state.selectedLinearElement?.isEditing ?? false).toBe(false); // undo `open editor`
expect(h.state.editingLinearElement).toBeNull(); // undo `open editor`
expect(h.state.selectedLinearElement?.elementId).toBe(h.elements[0].id);
expect(h.elements).toEqual([
expect.objectContaining({
@@ -1247,7 +1247,7 @@ describe("history", () => {
expect(API.getUndoStack().length).toBe(4);
expect(API.getRedoStack().length).toBe(2);
expect(assertSelectedElements(h.elements[0]));
expect(h.state.selectedLinearElement?.isEditing).toBe(true);
expect(h.state.editingLinearElement?.elementId).toBe(h.elements[0].id);
expect(h.state.selectedLinearElement?.elementId).toBe(h.elements[0].id);
expect(h.elements).toEqual([
expect.objectContaining({
@@ -1264,7 +1264,7 @@ describe("history", () => {
expect(API.getUndoStack().length).toBe(5);
expect(API.getRedoStack().length).toBe(1);
expect(assertSelectedElements(h.elements[0]));
expect(h.state.selectedLinearElement?.isEditing).toBe(true);
expect(h.state.editingLinearElement?.elementId).toBe(h.elements[0].id);
expect(h.state.selectedLinearElement?.elementId).toBe(h.elements[0].id);
expect(h.elements).toEqual([
expect.objectContaining({
@@ -1281,7 +1281,7 @@ describe("history", () => {
expect(API.getUndoStack().length).toBe(6);
expect(API.getRedoStack().length).toBe(0);
expect(assertSelectedElements(h.elements[0]));
expect(h.state.selectedLinearElement?.isEditing ?? false).toBe(false);
expect(h.state.editingLinearElement).toBeNull();
expect(h.state.selectedLinearElement).not.toBeNull();
expect(h.elements).toEqual([
expect.objectContaining({
@@ -3029,8 +3029,8 @@ describe("history", () => {
expect(API.getUndoStack().length).toBe(4);
expect(API.getRedoStack().length).toBe(0);
expect(h.state.editingLinearElement).toBeNull();
expect(h.state.selectedLinearElement).not.toBeNull();
expect(h.state.selectedLinearElement?.isEditing).toBe(false);
// Simulate remote update
API.updateScene({
@@ -3043,16 +3043,16 @@ describe("history", () => {
});
Keyboard.undo();
expect(API.getUndoStack().length).toBe(3);
expect(API.getRedoStack().length).toBe(1);
expect(h.state.selectedLinearElement).not.toBeNull();
expect(h.state.selectedLinearElement?.isEditing).toBe(true);
expect(API.getUndoStack().length).toBe(1);
expect(API.getRedoStack().length).toBe(3);
expect(h.state.editingLinearElement).toBeNull();
expect(h.state.selectedLinearElement).toBeNull();
Keyboard.redo();
expect(API.getUndoStack().length).toBe(4);
expect(API.getRedoStack().length).toBe(0);
expect(h.state.selectedLinearElement).not.toBeNull();
expect(h.state.selectedLinearElement?.isEditing ?? false).toBe(false);
expect(h.state.editingLinearElement).toBeNull();
expect(h.state.selectedLinearElement).toBeNull();
});
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);
mouse.down(10, 10);
mouse.up(30, 30);
mouse.up(10, 10);
if (shouldSelect) {
expect(API.getSelectedElement().type).toBe(shape);

View File

@@ -213,6 +213,7 @@ export type InteractiveCanvasAppState = Readonly<
_CommonCanvasAppState & {
// renderInteractiveScene
activeEmbeddable: AppState["activeEmbeddable"];
editingLinearElement: AppState["editingLinearElement"];
selectionElement: AppState["selectionElement"];
selectedGroupIds: AppState["selectedGroupIds"];
selectedLinearElement: AppState["selectedLinearElement"];
@@ -248,8 +249,10 @@ export type ObservedElementsAppState = {
editingGroupId: AppState["editingGroupId"];
selectedElementIds: AppState["selectedElementIds"];
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;
selectedLinearElementIsEditing: boolean | null;
croppingElementId: AppState["croppingElementId"];
lockedMultiSelections: AppState["lockedMultiSelections"];
activeLockedId: AppState["activeLockedId"];
@@ -304,6 +307,7 @@ export interface AppState {
* set when a new text is created or when an existing text is being edited
*/
editingTextElement: NonDeletedExcalidrawElement | null;
editingLinearElement: LinearElementEditor | null;
activeTool: {
/**
* indicates a previous tool we should revert back to if we deselect the
@@ -440,6 +444,11 @@ export interface AppState {
// as elements are unlocked, we remove the groupId from the elements
// and also remove groupId from this map
lockedMultiSelections: { [groupId: string]: true };
cropPositionMovement: {
croppingElementId?: string;
enabled: boolean;
directionLock: "x" | "y" | null;
};
}
export type SearchMatch = {
@@ -793,6 +802,11 @@ export type PointerDownState = Readonly<{
boxSelection: {
hasOccurred: boolean;
};
cropPositionMovement: {
croppingElementId?: string;
enabled: boolean;
directionLock: "x" | "y" | null;
};
}>;
export type UnsubscribeCallback = () => void;

View File

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

View File

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

View File

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

71
scripts/autorelease.js Normal file
View File

@@ -0,0 +1,71 @@
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,9 +11,12 @@ const getConfig = (outdir) => ({
entryNames: "[name]",
assetNames: "[dir]/[name]",
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"),
},
external: ["@excalidraw/common", "@excalidraw/element", "@excalidraw/math"],
});
function buildDev(config) {

View File

@@ -28,9 +28,12 @@ const getConfig = (outdir) => ({
assetNames: "[dir]/[name]",
chunkNames: "[dir]/[name]-[hash]",
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"),
},
external: ["@excalidraw/common", "@excalidraw/element", "@excalidraw/math"],
loader: {
".woff2": "file",
},

38
scripts/prerelease.js Normal file
View File

@@ -0,0 +1,38 @@
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,239 +1,28 @@
const fs = require("fs");
const path = require("path");
const { execSync } = require("child_process");
const updateChangelog = require("./updateChangelog");
const excalidrawDir = `${__dirname}/../packages/excalidraw`;
const excalidrawPackage = `${excalidrawDir}/package.json`;
const pkg = require(excalidrawPackage);
// skipping utils for now, as it has independent release process
const PACKAGES = ["common", "math", "element", "excalidraw"];
const PACKAGES_DIR = path.resolve(__dirname, "../packages");
/**
* Returns the arguments for the release script.
*
* Usage examples:
* - yarn release --help -> prints this help message
* - 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
*
* @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!`);
const publish = () => {
try {
console.info("Installing the dependencies in root folder...");
execSync(`yarn --frozen-lockfile`);
console.info("Installing the dependencies in excalidraw directory...");
execSync(`yarn --frozen-lockfile`, { cwd: excalidrawDir });
console.info("Building ESM Package...");
execSync(`yarn run build:esm`, { cwd: excalidrawDir });
console.info("Publishing the package...");
execSync(`yarn --cwd ${excalidrawDir} publish`);
} catch (error) {
console.error(error);
process.exit(1);
}
};
const getPackageJsonPath = (packageName) => {
validatePackageName(packageName);
return path.resolve(PACKAGES_DIR, packageName, "package.json");
const release = () => {
publish();
console.info(`Published ${pkg.version}!`);
};
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);
}
})();
release();

View File

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