Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e625d5aba3 |
+1
-3
@@ -1,6 +1,5 @@
|
|||||||
{
|
{
|
||||||
"extends": ["@excalidraw/eslint-config", "react-app"],
|
"extends": ["@excalidraw/eslint-config", "react-app"],
|
||||||
"plugins": ["excalidraw"],
|
|
||||||
"rules": {
|
"rules": {
|
||||||
"import/order": [
|
"import/order": [
|
||||||
"warn",
|
"warn",
|
||||||
@@ -39,7 +38,6 @@
|
|||||||
{
|
{
|
||||||
"allowReferrer": true
|
"allowReferrer": true
|
||||||
}
|
}
|
||||||
],
|
]
|
||||||
"excalidraw/no-binding-direct-mod": "error"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -259,9 +259,7 @@ export const loadFromFirebase = async (
|
|||||||
}
|
}
|
||||||
const storedScene = docSnap.data() as FirebaseStoredScene;
|
const storedScene = docSnap.data() as FirebaseStoredScene;
|
||||||
const elements = getSyncableElements(
|
const elements = getSyncableElements(
|
||||||
restoreElements(await decryptElements(storedScene, roomKey), null, {
|
restoreElements(await decryptElements(storedScene, roomKey), null),
|
||||||
deleteInvisibleElements: true,
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (socket) {
|
if (socket) {
|
||||||
|
|||||||
@@ -258,16 +258,11 @@ export const loadScene = async (
|
|||||||
await importFromBackend(id, privateKey),
|
await importFromBackend(id, privateKey),
|
||||||
localDataState?.appState,
|
localDataState?.appState,
|
||||||
localDataState?.elements,
|
localDataState?.elements,
|
||||||
{
|
{ repairBindings: true, refreshDimensions: false },
|
||||||
repairBindings: true,
|
|
||||||
refreshDimensions: false,
|
|
||||||
deleteInvisibleElements: true,
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
data = restore(localDataState || null, null, null, {
|
data = restore(localDataState || null, null, null, {
|
||||||
repairBindings: true,
|
repairBindings: true,
|
||||||
deleteInvisibleElements: true,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,7 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<title>
|
<title>Excalidraw | Hand-drawn look & feel • Collaborative • Secure</title>
|
||||||
Free, collaborative whiteboard • Hand-drawn look & feel | Excalidraw
|
|
||||||
</title>
|
|
||||||
<meta
|
<meta
|
||||||
name="viewport"
|
name="viewport"
|
||||||
content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover, shrink-to-fit=no"
|
content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover, shrink-to-fit=no"
|
||||||
@@ -16,7 +14,7 @@
|
|||||||
<!-- Primary Meta Tags -->
|
<!-- Primary Meta Tags -->
|
||||||
<meta
|
<meta
|
||||||
name="title"
|
name="title"
|
||||||
content="Free, collaborative whiteboard • Hand-drawn look & feel | Excalidraw"
|
content="Excalidraw — Collaborative whiteboarding made easy"
|
||||||
/>
|
/>
|
||||||
<meta
|
<meta
|
||||||
name="description"
|
name="description"
|
||||||
|
|||||||
@@ -8,7 +8,6 @@
|
|||||||
"examples/*"
|
"examples/*"
|
||||||
],
|
],
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/plugin-transform-explicit-resource-management": "7.28.0",
|
|
||||||
"@babel/preset-env": "7.26.9",
|
"@babel/preset-env": "7.26.9",
|
||||||
"@excalidraw/eslint-config": "1.0.3",
|
"@excalidraw/eslint-config": "1.0.3",
|
||||||
"@excalidraw/prettier-config": "1.0.2",
|
"@excalidraw/prettier-config": "1.0.2",
|
||||||
@@ -25,7 +24,6 @@
|
|||||||
"dotenv": "16.0.1",
|
"dotenv": "16.0.1",
|
||||||
"eslint-config-prettier": "8.5.0",
|
"eslint-config-prettier": "8.5.0",
|
||||||
"eslint-config-react-app": "7.0.1",
|
"eslint-config-react-app": "7.0.1",
|
||||||
"eslint-plugin-eslint": "file:packages/eslint",
|
|
||||||
"eslint-plugin-import": "2.31.0",
|
"eslint-plugin-import": "2.31.0",
|
||||||
"eslint-plugin-prettier": "3.3.1",
|
"eslint-plugin-prettier": "3.3.1",
|
||||||
"http-server": "14.1.1",
|
"http-server": "14.1.1",
|
||||||
|
|||||||
@@ -18,22 +18,13 @@ export const isChrome = navigator.userAgent.indexOf("Chrome") !== -1;
|
|||||||
export const isSafari =
|
export const isSafari =
|
||||||
!isChrome && navigator.userAgent.indexOf("Safari") !== -1;
|
!isChrome && navigator.userAgent.indexOf("Safari") !== -1;
|
||||||
export const isIOS =
|
export const isIOS =
|
||||||
/iPad|iPhone/i.test(navigator.platform) ||
|
/iPad|iPhone/.test(navigator.platform) ||
|
||||||
// iPadOS 13+
|
// iPadOS 13+
|
||||||
(navigator.userAgent.includes("Mac") && "ontouchend" in document);
|
(navigator.userAgent.includes("Mac") && "ontouchend" in document);
|
||||||
// keeping function so it can be mocked in test
|
// keeping function so it can be mocked in test
|
||||||
export const isBrave = () =>
|
export const isBrave = () =>
|
||||||
(navigator as any).brave?.isBrave?.name === "isBrave";
|
(navigator as any).brave?.isBrave?.name === "isBrave";
|
||||||
|
|
||||||
export const isMobile =
|
|
||||||
isIOS ||
|
|
||||||
/android|webos|ipod|blackberry|iemobile|opera mini/i.test(
|
|
||||||
navigator.userAgent.toLowerCase(),
|
|
||||||
) ||
|
|
||||||
/android|ios|ipod|blackberry|windows phone/i.test(
|
|
||||||
navigator.platform.toLowerCase(),
|
|
||||||
);
|
|
||||||
|
|
||||||
export const supportsResizeObserver =
|
export const supportsResizeObserver =
|
||||||
typeof window !== "undefined" && "ResizeObserver" in window;
|
typeof window !== "undefined" && "ResizeObserver" in window;
|
||||||
|
|
||||||
|
|||||||
@@ -164,14 +164,9 @@ export class Scene {
|
|||||||
return this.frames;
|
return this.frames;
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(
|
constructor(elements: ElementsMapOrArray | null = null) {
|
||||||
elements: ElementsMapOrArray | null = null,
|
|
||||||
options?: {
|
|
||||||
skipValidation?: true;
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
if (elements) {
|
if (elements) {
|
||||||
this.replaceAllElements(elements, options);
|
this.replaceAllElements(elements);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -268,19 +263,12 @@ export class Scene {
|
|||||||
return didChange;
|
return didChange;
|
||||||
}
|
}
|
||||||
|
|
||||||
replaceAllElements(
|
replaceAllElements(nextElements: ElementsMapOrArray) {
|
||||||
nextElements: ElementsMapOrArray,
|
|
||||||
options?: {
|
|
||||||
skipValidation?: true;
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
// we do trust the insertion order on the map, though maybe we shouldn't and should prefer order defined by fractional indices
|
// we do trust the insertion order on the map, though maybe we shouldn't and should prefer order defined by fractional indices
|
||||||
const _nextElements = toArray(nextElements);
|
const _nextElements = toArray(nextElements);
|
||||||
const nextFrameLikes: ExcalidrawFrameLikeElement[] = [];
|
const nextFrameLikes: ExcalidrawFrameLikeElement[] = [];
|
||||||
|
|
||||||
if (!options?.skipValidation) {
|
validateIndicesThrottled(_nextElements);
|
||||||
validateIndicesThrottled(_nextElements);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.elements = syncInvalidIndices(_nextElements);
|
this.elements = syncInvalidIndices(_nextElements);
|
||||||
this.elementsMap.clear();
|
this.elementsMap.clear();
|
||||||
|
|||||||
+147
-377
@@ -2,6 +2,7 @@ import {
|
|||||||
arrayToMap,
|
arrayToMap,
|
||||||
arrayToObject,
|
arrayToObject,
|
||||||
assertNever,
|
assertNever,
|
||||||
|
invariant,
|
||||||
isDevEnv,
|
isDevEnv,
|
||||||
isShallowEqual,
|
isShallowEqual,
|
||||||
isTestEnv,
|
isTestEnv,
|
||||||
@@ -55,10 +56,10 @@ import { getNonDeletedGroupIds } from "./groups";
|
|||||||
|
|
||||||
import { orderByFractionalIndex, syncMovedIndices } from "./fractionalIndex";
|
import { orderByFractionalIndex, syncMovedIndices } from "./fractionalIndex";
|
||||||
|
|
||||||
import { StoreSnapshot } from "./store";
|
|
||||||
|
|
||||||
import { Scene } from "./Scene";
|
import { Scene } from "./Scene";
|
||||||
|
|
||||||
|
import { StoreSnapshot } from "./store";
|
||||||
|
|
||||||
import type { BindableProp, BindingProp } from "./binding";
|
import type { BindableProp, BindingProp } from "./binding";
|
||||||
|
|
||||||
import type { ElementUpdate } from "./mutateElement";
|
import type { ElementUpdate } from "./mutateElement";
|
||||||
@@ -150,27 +151,13 @@ export class Delta<T> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Merges two deltas into a new one.
|
|
||||||
*/
|
|
||||||
public static merge<T>(
|
|
||||||
delta1: Delta<T>,
|
|
||||||
delta2: Delta<T>,
|
|
||||||
delta3: Delta<T> = Delta.empty(),
|
|
||||||
) {
|
|
||||||
return Delta.create(
|
|
||||||
{ ...delta1.deleted, ...delta2.deleted, ...delta3.deleted },
|
|
||||||
{ ...delta1.inserted, ...delta2.inserted, ...delta3.inserted },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Merges deleted and inserted object partials.
|
* Merges deleted and inserted object partials.
|
||||||
*/
|
*/
|
||||||
public static mergeObjects<T extends { [key: string]: unknown }>(
|
public static mergeObjects<T extends { [key: string]: unknown }>(
|
||||||
prev: T,
|
prev: T,
|
||||||
added: T,
|
added: T,
|
||||||
removed: T = {} as T,
|
removed: T,
|
||||||
) {
|
) {
|
||||||
const cloned = { ...prev };
|
const cloned = { ...prev };
|
||||||
|
|
||||||
@@ -510,11 +497,6 @@ export interface DeltaContainer<T> {
|
|||||||
*/
|
*/
|
||||||
applyTo(previous: T, ...options: unknown[]): [T, boolean];
|
applyTo(previous: T, ...options: unknown[]): [T, boolean];
|
||||||
|
|
||||||
/**
|
|
||||||
* Squashes the current delta with the given one.
|
|
||||||
*/
|
|
||||||
squash(delta: DeltaContainer<T>): this;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks whether all `Delta`s are empty.
|
* Checks whether all `Delta`s are empty.
|
||||||
*/
|
*/
|
||||||
@@ -522,11 +504,7 @@ export interface DeltaContainer<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class AppStateDelta implements DeltaContainer<AppState> {
|
export class AppStateDelta implements DeltaContainer<AppState> {
|
||||||
private constructor(public delta: Delta<ObservedAppState>) {}
|
private constructor(public readonly delta: Delta<ObservedAppState>) {}
|
||||||
|
|
||||||
public static create(delta: Delta<ObservedAppState>): AppStateDelta {
|
|
||||||
return new AppStateDelta(delta);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static calculate<T extends ObservedAppState>(
|
public static calculate<T extends ObservedAppState>(
|
||||||
prevAppState: T,
|
prevAppState: T,
|
||||||
@@ -557,137 +535,76 @@ export class AppStateDelta implements DeltaContainer<AppState> {
|
|||||||
return new AppStateDelta(inversedDelta);
|
return new AppStateDelta(inversedDelta);
|
||||||
}
|
}
|
||||||
|
|
||||||
public squash(delta: AppStateDelta): this {
|
|
||||||
if (delta.isEmpty()) {
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
const mergedDeletedSelectedElementIds = Delta.mergeObjects(
|
|
||||||
this.delta.deleted.selectedElementIds ?? {},
|
|
||||||
delta.delta.deleted.selectedElementIds ?? {},
|
|
||||||
);
|
|
||||||
|
|
||||||
const mergedInsertedSelectedElementIds = Delta.mergeObjects(
|
|
||||||
this.delta.inserted.selectedElementIds ?? {},
|
|
||||||
delta.delta.inserted.selectedElementIds ?? {},
|
|
||||||
);
|
|
||||||
|
|
||||||
const mergedDeletedSelectedGroupIds = Delta.mergeObjects(
|
|
||||||
this.delta.deleted.selectedGroupIds ?? {},
|
|
||||||
delta.delta.deleted.selectedGroupIds ?? {},
|
|
||||||
);
|
|
||||||
|
|
||||||
const mergedInsertedSelectedGroupIds = Delta.mergeObjects(
|
|
||||||
this.delta.inserted.selectedGroupIds ?? {},
|
|
||||||
delta.delta.inserted.selectedGroupIds ?? {},
|
|
||||||
);
|
|
||||||
|
|
||||||
const mergedDeletedLockedMultiSelections = Delta.mergeObjects(
|
|
||||||
this.delta.deleted.lockedMultiSelections ?? {},
|
|
||||||
delta.delta.deleted.lockedMultiSelections ?? {},
|
|
||||||
);
|
|
||||||
|
|
||||||
const mergedInsertedLockedMultiSelections = Delta.mergeObjects(
|
|
||||||
this.delta.inserted.lockedMultiSelections ?? {},
|
|
||||||
delta.delta.inserted.lockedMultiSelections ?? {},
|
|
||||||
);
|
|
||||||
|
|
||||||
const mergedInserted: Partial<ObservedAppState> = {};
|
|
||||||
const mergedDeleted: Partial<ObservedAppState> = {};
|
|
||||||
|
|
||||||
if (
|
|
||||||
Object.keys(mergedDeletedSelectedElementIds).length ||
|
|
||||||
Object.keys(mergedInsertedSelectedElementIds).length
|
|
||||||
) {
|
|
||||||
mergedDeleted.selectedElementIds = mergedDeletedSelectedElementIds;
|
|
||||||
mergedInserted.selectedElementIds = mergedInsertedSelectedElementIds;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
Object.keys(mergedDeletedSelectedGroupIds).length ||
|
|
||||||
Object.keys(mergedInsertedSelectedGroupIds).length
|
|
||||||
) {
|
|
||||||
mergedDeleted.selectedGroupIds = mergedDeletedSelectedGroupIds;
|
|
||||||
mergedInserted.selectedGroupIds = mergedInsertedSelectedGroupIds;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
Object.keys(mergedDeletedLockedMultiSelections).length ||
|
|
||||||
Object.keys(mergedInsertedLockedMultiSelections).length
|
|
||||||
) {
|
|
||||||
mergedDeleted.lockedMultiSelections = mergedDeletedLockedMultiSelections;
|
|
||||||
mergedInserted.lockedMultiSelections =
|
|
||||||
mergedInsertedLockedMultiSelections;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.delta = Delta.merge(
|
|
||||||
this.delta,
|
|
||||||
delta.delta,
|
|
||||||
Delta.create(mergedDeleted, mergedInserted),
|
|
||||||
);
|
|
||||||
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public applyTo(
|
public applyTo(
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
nextElements: SceneElementsMap,
|
nextElements: SceneElementsMap,
|
||||||
): [AppState, boolean] {
|
): [AppState, boolean] {
|
||||||
try {
|
try {
|
||||||
const {
|
const {
|
||||||
selectedElementIds: deletedSelectedElementIds = {},
|
selectedElementIds: removedSelectedElementIds = {},
|
||||||
selectedGroupIds: deletedSelectedGroupIds = {},
|
selectedGroupIds: removedSelectedGroupIds = {},
|
||||||
lockedMultiSelections: deletedLockedMultiSelections = {},
|
|
||||||
} = this.delta.deleted;
|
} = this.delta.deleted;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
selectedElementIds: insertedSelectedElementIds = {},
|
selectedElementIds: addedSelectedElementIds = {},
|
||||||
selectedGroupIds: insertedSelectedGroupIds = {},
|
selectedGroupIds: addedSelectedGroupIds = {},
|
||||||
lockedMultiSelections: insertedLockedMultiSelections = {},
|
selectedLinearElementId,
|
||||||
selectedLinearElement: insertedSelectedLinearElement,
|
selectedLinearElementIsEditing,
|
||||||
...directlyApplicablePartial
|
...directlyApplicablePartial
|
||||||
} = this.delta.inserted;
|
} = this.delta.inserted;
|
||||||
|
|
||||||
const mergedSelectedElementIds = Delta.mergeObjects(
|
const mergedSelectedElementIds = Delta.mergeObjects(
|
||||||
appState.selectedElementIds,
|
appState.selectedElementIds,
|
||||||
insertedSelectedElementIds,
|
addedSelectedElementIds,
|
||||||
deletedSelectedElementIds,
|
removedSelectedElementIds,
|
||||||
);
|
);
|
||||||
|
|
||||||
const mergedSelectedGroupIds = Delta.mergeObjects(
|
const mergedSelectedGroupIds = Delta.mergeObjects(
|
||||||
appState.selectedGroupIds,
|
appState.selectedGroupIds,
|
||||||
insertedSelectedGroupIds,
|
addedSelectedGroupIds,
|
||||||
deletedSelectedGroupIds,
|
removedSelectedGroupIds,
|
||||||
);
|
);
|
||||||
|
|
||||||
const mergedLockedMultiSelections = Delta.mergeObjects(
|
let selectedLinearElement = appState.selectedLinearElement;
|
||||||
appState.lockedMultiSelections,
|
|
||||||
insertedLockedMultiSelections,
|
|
||||||
deletedLockedMultiSelections,
|
|
||||||
);
|
|
||||||
|
|
||||||
const selectedLinearElement =
|
if (selectedLinearElementId === null) {
|
||||||
insertedSelectedLinearElement &&
|
// Unselect linear element (visible change)
|
||||||
nextElements.has(insertedSelectedLinearElement.elementId)
|
selectedLinearElement = null;
|
||||||
? new LinearElementEditor(
|
} else if (
|
||||||
nextElements.get(
|
selectedLinearElementId &&
|
||||||
insertedSelectedLinearElement.elementId,
|
nextElements.has(selectedLinearElementId)
|
||||||
) as NonDeleted<ExcalidrawLinearElement>,
|
) {
|
||||||
nextElements,
|
selectedLinearElement = new LinearElementEditor(
|
||||||
insertedSelectedLinearElement.isEditing,
|
nextElements.get(
|
||||||
)
|
selectedLinearElementId,
|
||||||
: null;
|
) 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 nextAppState = {
|
const nextAppState = {
|
||||||
...appState,
|
...appState,
|
||||||
...directlyApplicablePartial,
|
...directlyApplicablePartial,
|
||||||
selectedElementIds: mergedSelectedElementIds,
|
selectedElementIds: mergedSelectedElementIds,
|
||||||
selectedGroupIds: mergedSelectedGroupIds,
|
selectedGroupIds: mergedSelectedGroupIds,
|
||||||
lockedMultiSelections: mergedLockedMultiSelections,
|
selectedLinearElement,
|
||||||
selectedLinearElement:
|
|
||||||
typeof insertedSelectedLinearElement !== "undefined"
|
|
||||||
? selectedLinearElement
|
|
||||||
: appState.selectedLinearElement,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const constainsVisibleChanges = this.filterInvisibleChanges(
|
const constainsVisibleChanges = this.filterInvisibleChanges(
|
||||||
@@ -816,53 +733,64 @@ export class AppStateDelta implements DeltaContainer<AppState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case "selectedLinearElement":
|
case "selectedLinearElementId": {
|
||||||
const nextLinearElement = nextAppState[key];
|
const appStateKey = AppStateDelta.convertToAppStateKey(key);
|
||||||
|
const linearElement = nextAppState[appStateKey];
|
||||||
|
|
||||||
if (!nextLinearElement) {
|
if (!linearElement) {
|
||||||
// previously there was a linear element (assuming visible), now there is none
|
// previously there was a linear element (assuming visible), now there is none
|
||||||
visibleDifferenceFlag.value = true;
|
visibleDifferenceFlag.value = true;
|
||||||
} else {
|
} else {
|
||||||
const element = nextElements.get(nextLinearElement.elementId);
|
const element = nextElements.get(linearElement.elementId);
|
||||||
|
|
||||||
if (element && !element.isDeleted) {
|
if (element && !element.isDeleted) {
|
||||||
// previously there wasn't a linear element, now there is one which is visible
|
// previously there wasn't a linear element, now there is one which is visible
|
||||||
visibleDifferenceFlag.value = true;
|
visibleDifferenceFlag.value = true;
|
||||||
} else {
|
} else {
|
||||||
// there was assigned a linear element now, but it's deleted
|
// there was assigned a linear element now, but it's deleted
|
||||||
nextAppState[key] = null;
|
nextAppState[appStateKey] = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case "lockedMultiSelections":
|
}
|
||||||
|
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 prevLockedUnits = prevAppState[key] || {};
|
||||||
const nextLockedUnits = nextAppState[key] || {};
|
const nextLockedUnits = nextAppState[key] || {};
|
||||||
|
|
||||||
// TODO: this seems wrong, we are already doing this comparison generically above,
|
|
||||||
// hence instead we should check whether elements are actually visible,
|
|
||||||
// so that once these changes are applied they actually result in a visible change to the user
|
|
||||||
if (!isShallowEqual(prevLockedUnits, nextLockedUnits)) {
|
if (!isShallowEqual(prevLockedUnits, nextLockedUnits)) {
|
||||||
visibleDifferenceFlag.value = true;
|
visibleDifferenceFlag.value = true;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case "activeLockedId":
|
}
|
||||||
|
case "activeLockedId": {
|
||||||
const prevHitLockedId = prevAppState[key] || null;
|
const prevHitLockedId = prevAppState[key] || null;
|
||||||
const nextHitLockedId = nextAppState[key] || null;
|
const nextHitLockedId = nextAppState[key] || null;
|
||||||
|
|
||||||
// TODO: this seems wrong, we are already doing this comparison generically above,
|
|
||||||
// hence instead we should check whether elements are actually visible,
|
|
||||||
// so that once these changes are applied they actually result in a visible change to the user
|
|
||||||
if (prevHitLockedId !== nextHitLockedId) {
|
if (prevHitLockedId !== nextHitLockedId) {
|
||||||
visibleDifferenceFlag.value = true;
|
visibleDifferenceFlag.value = true;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
default:
|
}
|
||||||
|
default: {
|
||||||
assertNever(
|
assertNever(
|
||||||
key,
|
key,
|
||||||
`Unknown ObservedElementsAppState's key "${key}"`,
|
`Unknown ObservedElementsAppState's key "${key}"`,
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -870,6 +798,15 @@ export class AppStateDelta implements DeltaContainer<AppState> {
|
|||||||
return visibleDifferenceFlag.value;
|
return visibleDifferenceFlag.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static convertToAppStateKey(
|
||||||
|
key: keyof Pick<ObservedElementsAppState, "selectedLinearElementId">,
|
||||||
|
): keyof Pick<AppState, "selectedLinearElement"> {
|
||||||
|
switch (key) {
|
||||||
|
case "selectedLinearElementId":
|
||||||
|
return "selectedLinearElement";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static filterSelectedElements(
|
private static filterSelectedElements(
|
||||||
selectedElementIds: AppState["selectedElementIds"],
|
selectedElementIds: AppState["selectedElementIds"],
|
||||||
elements: SceneElementsMap,
|
elements: SceneElementsMap,
|
||||||
@@ -934,7 +871,8 @@ export class AppStateDelta implements DeltaContainer<AppState> {
|
|||||||
editingGroupId,
|
editingGroupId,
|
||||||
selectedGroupIds,
|
selectedGroupIds,
|
||||||
selectedElementIds,
|
selectedElementIds,
|
||||||
selectedLinearElement,
|
selectedLinearElementId,
|
||||||
|
selectedLinearElementIsEditing,
|
||||||
croppingElementId,
|
croppingElementId,
|
||||||
lockedMultiSelections,
|
lockedMultiSelections,
|
||||||
activeLockedId,
|
activeLockedId,
|
||||||
@@ -988,6 +926,12 @@ export class AppStateDelta implements DeltaContainer<AppState> {
|
|||||||
"lockedMultiSelections",
|
"lockedMultiSelections",
|
||||||
(prevValue) => (prevValue ?? {}) as ValueOf<T["lockedMultiSelections"]>,
|
(prevValue) => (prevValue ?? {}) as ValueOf<T["lockedMultiSelections"]>,
|
||||||
);
|
);
|
||||||
|
Delta.diffObjects(
|
||||||
|
deleted,
|
||||||
|
inserted,
|
||||||
|
"activeLockedId",
|
||||||
|
(prevValue) => (prevValue ?? null) as ValueOf<T["activeLockedId"]>,
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// if postprocessing fails it does not make sense to bubble up, but let's make sure we know about it
|
// if postprocessing fails it does not make sense to bubble up, but let's make sure we know about it
|
||||||
console.error(`Couldn't postprocess appstate change deltas.`);
|
console.error(`Couldn't postprocess appstate change deltas.`);
|
||||||
@@ -1016,13 +960,12 @@ type ElementPartial<TElement extends ExcalidrawElement = ExcalidrawElement> =
|
|||||||
Omit<Partial<Ordered<TElement>>, "id" | "updated" | "seed">;
|
Omit<Partial<Ordered<TElement>>, "id" | "updated" | "seed">;
|
||||||
|
|
||||||
export type ApplyToOptions = {
|
export type ApplyToOptions = {
|
||||||
excludedProperties?: Set<keyof ElementPartial>;
|
excludedProperties: Set<keyof ElementPartial>;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ApplyToFlags = {
|
type ApplyToFlags = {
|
||||||
containsVisibleDifference: boolean;
|
containsVisibleDifference: boolean;
|
||||||
containsZindexDifference: boolean;
|
containsZindexDifference: boolean;
|
||||||
applyDirection: "forward" | "backward" | undefined;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1123,15 +1066,6 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
|
|||||||
deleted.version !== inserted.version
|
deleted.version !== inserted.version
|
||||||
);
|
);
|
||||||
|
|
||||||
private static satisfiesUniqueInvariants = (
|
|
||||||
elementsDelta: ElementsDelta,
|
|
||||||
id: string,
|
|
||||||
) => {
|
|
||||||
const { added, removed, updated } = elementsDelta;
|
|
||||||
// it's required that there is only one unique delta type per element
|
|
||||||
return [added[id], removed[id], updated[id]].filter(Boolean).length === 1;
|
|
||||||
};
|
|
||||||
|
|
||||||
private static validate(
|
private static validate(
|
||||||
elementsDelta: ElementsDelta,
|
elementsDelta: ElementsDelta,
|
||||||
type: "added" | "removed" | "updated",
|
type: "added" | "removed" | "updated",
|
||||||
@@ -1140,7 +1074,6 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
|
|||||||
for (const [id, delta] of Object.entries(elementsDelta[type])) {
|
for (const [id, delta] of Object.entries(elementsDelta[type])) {
|
||||||
if (
|
if (
|
||||||
!this.satisfiesCommmonInvariants(delta) ||
|
!this.satisfiesCommmonInvariants(delta) ||
|
||||||
!this.satisfiesUniqueInvariants(elementsDelta, id) ||
|
|
||||||
!satifiesSpecialInvariants(delta)
|
!satifiesSpecialInvariants(delta)
|
||||||
) {
|
) {
|
||||||
console.error(
|
console.error(
|
||||||
@@ -1177,7 +1110,7 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
|
|||||||
const nextElement = nextElements.get(prevElement.id);
|
const nextElement = nextElements.get(prevElement.id);
|
||||||
|
|
||||||
if (!nextElement) {
|
if (!nextElement) {
|
||||||
const deleted = { ...prevElement } as ElementPartial;
|
const deleted = { ...prevElement, isDeleted: false } as ElementPartial;
|
||||||
|
|
||||||
const inserted = {
|
const inserted = {
|
||||||
isDeleted: true,
|
isDeleted: true,
|
||||||
@@ -1191,10 +1124,7 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
|
|||||||
ElementsDelta.stripIrrelevantProps,
|
ElementsDelta.stripIrrelevantProps,
|
||||||
);
|
);
|
||||||
|
|
||||||
// ignore updates which would "delete" already deleted element
|
removed[prevElement.id] = delta;
|
||||||
if (!prevElement.isDeleted) {
|
|
||||||
removed[prevElement.id] = delta;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1210,6 +1140,7 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
|
|||||||
|
|
||||||
const inserted = {
|
const inserted = {
|
||||||
...nextElement,
|
...nextElement,
|
||||||
|
isDeleted: false,
|
||||||
} as ElementPartial;
|
} as ElementPartial;
|
||||||
|
|
||||||
const delta = Delta.create(
|
const delta = Delta.create(
|
||||||
@@ -1218,10 +1149,7 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
|
|||||||
ElementsDelta.stripIrrelevantProps,
|
ElementsDelta.stripIrrelevantProps,
|
||||||
);
|
);
|
||||||
|
|
||||||
// ignore updates which would "delete" already deleted element
|
added[nextElement.id] = delta;
|
||||||
if (!nextElement.isDeleted) {
|
|
||||||
added[nextElement.id] = delta;
|
|
||||||
}
|
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -1250,13 +1178,8 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const strippedDeleted = ElementsDelta.stripVersionProps(delta.deleted);
|
// making sure there are at least some changes
|
||||||
const strippedInserted = ElementsDelta.stripVersionProps(
|
if (!Delta.isEmpty(delta)) {
|
||||||
delta.inserted,
|
|
||||||
);
|
|
||||||
|
|
||||||
// making sure there are at least some changes and only changed version & versionNonce does not count!
|
|
||||||
if (Delta.isInnerDifferent(strippedDeleted, strippedInserted, true)) {
|
|
||||||
updated[nextElement.id] = delta;
|
updated[nextElement.id] = delta;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1273,8 +1196,8 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
|
|||||||
const inverseInternal = (deltas: Record<string, Delta<ElementPartial>>) => {
|
const inverseInternal = (deltas: Record<string, Delta<ElementPartial>>) => {
|
||||||
const inversedDeltas: Record<string, Delta<ElementPartial>> = {};
|
const inversedDeltas: Record<string, Delta<ElementPartial>> = {};
|
||||||
|
|
||||||
for (const [id, { inserted, deleted }] of Object.entries(deltas)) {
|
for (const [id, delta] of Object.entries(deltas)) {
|
||||||
inversedDeltas[id] = Delta.create({ ...inserted }, { ...deleted });
|
inversedDeltas[id] = Delta.create(delta.inserted, delta.deleted);
|
||||||
}
|
}
|
||||||
|
|
||||||
return inversedDeltas;
|
return inversedDeltas;
|
||||||
@@ -1372,15 +1295,8 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
|
|||||||
latestDelta = delta;
|
latestDelta = delta;
|
||||||
}
|
}
|
||||||
|
|
||||||
const strippedDeleted = ElementsDelta.stripVersionProps(
|
|
||||||
latestDelta.deleted,
|
|
||||||
);
|
|
||||||
const strippedInserted = ElementsDelta.stripVersionProps(
|
|
||||||
latestDelta.inserted,
|
|
||||||
);
|
|
||||||
|
|
||||||
// it might happen that after applying latest changes the delta itself does not contain any changes
|
// it might happen that after applying latest changes the delta itself does not contain any changes
|
||||||
if (Delta.isInnerDifferent(strippedDeleted, strippedInserted)) {
|
if (Delta.isInnerDifferent(latestDelta.deleted, latestDelta.inserted)) {
|
||||||
modifiedDeltas[id] = latestDelta;
|
modifiedDeltas[id] = latestDelta;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1400,7 +1316,9 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
|
|||||||
public applyTo(
|
public applyTo(
|
||||||
elements: SceneElementsMap,
|
elements: SceneElementsMap,
|
||||||
snapshot: StoreSnapshot["elements"] = StoreSnapshot.empty().elements,
|
snapshot: StoreSnapshot["elements"] = StoreSnapshot.empty().elements,
|
||||||
options?: ApplyToOptions,
|
options: ApplyToOptions = {
|
||||||
|
excludedProperties: new Set(),
|
||||||
|
},
|
||||||
): [SceneElementsMap, boolean] {
|
): [SceneElementsMap, boolean] {
|
||||||
let nextElements = new Map(elements) as SceneElementsMap;
|
let nextElements = new Map(elements) as SceneElementsMap;
|
||||||
let changedElements: Map<string, OrderedExcalidrawElement>;
|
let changedElements: Map<string, OrderedExcalidrawElement>;
|
||||||
@@ -1408,28 +1326,22 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
|
|||||||
const flags: ApplyToFlags = {
|
const flags: ApplyToFlags = {
|
||||||
containsVisibleDifference: false,
|
containsVisibleDifference: false,
|
||||||
containsZindexDifference: false,
|
containsZindexDifference: false,
|
||||||
applyDirection: undefined,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// mimic a transaction by applying deltas into `nextElements` (always new instance, no mutation)
|
// mimic a transaction by applying deltas into `nextElements` (always new instance, no mutation)
|
||||||
try {
|
try {
|
||||||
const applyDeltas = ElementsDelta.createApplier(
|
const applyDeltas = ElementsDelta.createApplier(
|
||||||
elements,
|
|
||||||
nextElements,
|
nextElements,
|
||||||
snapshot,
|
snapshot,
|
||||||
flags,
|
|
||||||
options,
|
options,
|
||||||
|
flags,
|
||||||
);
|
);
|
||||||
|
|
||||||
const addedElements = applyDeltas(this.added);
|
const addedElements = applyDeltas(this.added);
|
||||||
const removedElements = applyDeltas(this.removed);
|
const removedElements = applyDeltas(this.removed);
|
||||||
const updatedElements = applyDeltas(this.updated);
|
const updatedElements = applyDeltas(this.updated);
|
||||||
|
|
||||||
const affectedElements = this.resolveConflicts(
|
const affectedElements = this.resolveConflicts(elements, nextElements);
|
||||||
elements,
|
|
||||||
nextElements,
|
|
||||||
flags.applyDirection,
|
|
||||||
);
|
|
||||||
|
|
||||||
// TODO: #7348 validate elements semantically and syntactically the changed elements, in case they would result data integrity issues
|
// TODO: #7348 validate elements semantically and syntactically the changed elements, in case they would result data integrity issues
|
||||||
changedElements = new Map([
|
changedElements = new Map([
|
||||||
@@ -1453,15 +1365,22 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// the following reorder performs mutations, but only on new instances of changed elements,
|
// the following reorder performs also mutations, but only on new instances of changed elements
|
||||||
// unless something goes really bad and it fallbacks to fixing all invalid indices
|
// (unless something goes really bad and it fallbacks to fixing all invalid indices)
|
||||||
nextElements = ElementsDelta.reorderElements(
|
nextElements = ElementsDelta.reorderElements(
|
||||||
nextElements,
|
nextElements,
|
||||||
changedElements,
|
changedElements,
|
||||||
flags,
|
flags,
|
||||||
);
|
);
|
||||||
|
|
||||||
ElementsDelta.redrawElements(nextElements, changedElements);
|
// we don't have an up-to-date scene, as we can be just in the middle of applying history entry
|
||||||
|
// we also don't have a scene on the server
|
||||||
|
// so we are creating a temp scene just to query and mutate elements
|
||||||
|
const tempScene = new Scene(nextElements);
|
||||||
|
|
||||||
|
ElementsDelta.redrawTextBoundingBoxes(tempScene, changedElements);
|
||||||
|
// Need ordered nextElements to avoid z-index binding issues
|
||||||
|
ElementsDelta.redrawBoundArrows(tempScene, changedElements);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(
|
console.error(
|
||||||
`Couldn't mutate elements after applying elements change`,
|
`Couldn't mutate elements after applying elements change`,
|
||||||
@@ -1476,113 +1395,12 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public squash(delta: ElementsDelta): this {
|
|
||||||
if (delta.isEmpty()) {
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { added, removed, updated } = delta;
|
|
||||||
|
|
||||||
const mergeBoundElements = (
|
|
||||||
prevDelta: Delta<ElementPartial>,
|
|
||||||
nextDelta: Delta<ElementPartial>,
|
|
||||||
) => {
|
|
||||||
const mergedDeletedBoundElements =
|
|
||||||
Delta.mergeArrays(
|
|
||||||
prevDelta.deleted.boundElements ?? [],
|
|
||||||
nextDelta.deleted.boundElements ?? [],
|
|
||||||
undefined,
|
|
||||||
(x) => x.id,
|
|
||||||
) ?? [];
|
|
||||||
|
|
||||||
const mergedInsertedBoundElements =
|
|
||||||
Delta.mergeArrays(
|
|
||||||
prevDelta.inserted.boundElements ?? [],
|
|
||||||
nextDelta.inserted.boundElements ?? [],
|
|
||||||
undefined,
|
|
||||||
(x) => x.id,
|
|
||||||
) ?? [];
|
|
||||||
|
|
||||||
if (
|
|
||||||
!mergedDeletedBoundElements.length &&
|
|
||||||
!mergedInsertedBoundElements.length
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Delta.create(
|
|
||||||
{
|
|
||||||
boundElements: mergedDeletedBoundElements,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
boundElements: mergedInsertedBoundElements,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
for (const [id, nextDelta] of Object.entries(added)) {
|
|
||||||
const prevDelta = this.added[id] ?? this.removed[id] ?? this.updated[id];
|
|
||||||
|
|
||||||
if (!prevDelta) {
|
|
||||||
this.added[id] = nextDelta;
|
|
||||||
} else {
|
|
||||||
const mergedDelta = mergeBoundElements(prevDelta, nextDelta);
|
|
||||||
delete this.removed[id];
|
|
||||||
delete this.updated[id];
|
|
||||||
|
|
||||||
this.added[id] = Delta.merge(prevDelta, nextDelta, mergedDelta);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const [id, nextDelta] of Object.entries(removed)) {
|
|
||||||
const prevDelta = this.added[id] ?? this.removed[id] ?? this.updated[id];
|
|
||||||
|
|
||||||
if (!prevDelta) {
|
|
||||||
this.removed[id] = nextDelta;
|
|
||||||
} else {
|
|
||||||
const mergedDelta = mergeBoundElements(prevDelta, nextDelta);
|
|
||||||
delete this.added[id];
|
|
||||||
delete this.updated[id];
|
|
||||||
|
|
||||||
this.removed[id] = Delta.merge(prevDelta, nextDelta, mergedDelta);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const [id, nextDelta] of Object.entries(updated)) {
|
|
||||||
const prevDelta = this.added[id] ?? this.removed[id] ?? this.updated[id];
|
|
||||||
|
|
||||||
if (!prevDelta) {
|
|
||||||
this.updated[id] = nextDelta;
|
|
||||||
} else {
|
|
||||||
const mergedDelta = mergeBoundElements(prevDelta, nextDelta);
|
|
||||||
const updatedDelta = Delta.merge(prevDelta, nextDelta, mergedDelta);
|
|
||||||
|
|
||||||
if (prevDelta === this.added[id]) {
|
|
||||||
this.added[id] = updatedDelta;
|
|
||||||
} else if (prevDelta === this.removed[id]) {
|
|
||||||
this.removed[id] = updatedDelta;
|
|
||||||
} else {
|
|
||||||
this.updated[id] = updatedDelta;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isTestEnv() || isDevEnv()) {
|
|
||||||
ElementsDelta.validate(this, "added", ElementsDelta.satisfiesAddition);
|
|
||||||
ElementsDelta.validate(this, "removed", ElementsDelta.satisfiesRemoval);
|
|
||||||
ElementsDelta.validate(this, "updated", ElementsDelta.satisfiesUpdate);
|
|
||||||
}
|
|
||||||
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static createApplier =
|
private static createApplier =
|
||||||
(
|
(
|
||||||
prevElements: SceneElementsMap,
|
|
||||||
nextElements: SceneElementsMap,
|
nextElements: SceneElementsMap,
|
||||||
snapshot: StoreSnapshot["elements"],
|
snapshot: StoreSnapshot["elements"],
|
||||||
|
options: ApplyToOptions,
|
||||||
flags: ApplyToFlags,
|
flags: ApplyToFlags,
|
||||||
options?: ApplyToOptions,
|
|
||||||
) =>
|
) =>
|
||||||
(deltas: Record<string, Delta<ElementPartial>>) => {
|
(deltas: Record<string, Delta<ElementPartial>>) => {
|
||||||
const getElement = ElementsDelta.createGetter(
|
const getElement = ElementsDelta.createGetter(
|
||||||
@@ -1595,26 +1413,15 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
|
|||||||
const element = getElement(id, delta.inserted);
|
const element = getElement(id, delta.inserted);
|
||||||
|
|
||||||
if (element) {
|
if (element) {
|
||||||
const nextElement = ElementsDelta.applyDelta(
|
const newElement = ElementsDelta.applyDelta(
|
||||||
element,
|
element,
|
||||||
delta,
|
delta,
|
||||||
flags,
|
|
||||||
options,
|
options,
|
||||||
|
flags,
|
||||||
);
|
);
|
||||||
|
|
||||||
nextElements.set(nextElement.id, nextElement);
|
nextElements.set(newElement.id, newElement);
|
||||||
acc.set(nextElement.id, nextElement);
|
acc.set(newElement.id, newElement);
|
||||||
|
|
||||||
if (!flags.applyDirection) {
|
|
||||||
const prevElement = prevElements.get(id);
|
|
||||||
|
|
||||||
if (prevElement) {
|
|
||||||
flags.applyDirection =
|
|
||||||
prevElement.version > nextElement.version
|
|
||||||
? "backward"
|
|
||||||
: "forward";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return acc;
|
return acc;
|
||||||
@@ -1659,8 +1466,8 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
|
|||||||
private static applyDelta(
|
private static applyDelta(
|
||||||
element: OrderedExcalidrawElement,
|
element: OrderedExcalidrawElement,
|
||||||
delta: Delta<ElementPartial>,
|
delta: Delta<ElementPartial>,
|
||||||
|
options: ApplyToOptions,
|
||||||
flags: ApplyToFlags,
|
flags: ApplyToFlags,
|
||||||
options?: ApplyToOptions,
|
|
||||||
) {
|
) {
|
||||||
const directlyApplicablePartial: Mutable<ElementPartial> = {};
|
const directlyApplicablePartial: Mutable<ElementPartial> = {};
|
||||||
|
|
||||||
@@ -1674,7 +1481,7 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options?.excludedProperties?.has(key)) {
|
if (options.excludedProperties.has(key)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1714,7 +1521,7 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
|
|||||||
delta.deleted.index !== delta.inserted.index;
|
delta.deleted.index !== delta.inserted.index;
|
||||||
}
|
}
|
||||||
|
|
||||||
return newElementWith(element, directlyApplicablePartial, true);
|
return newElementWith(element, directlyApplicablePartial);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1754,7 +1561,6 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
|
|||||||
private resolveConflicts(
|
private resolveConflicts(
|
||||||
prevElements: SceneElementsMap,
|
prevElements: SceneElementsMap,
|
||||||
nextElements: SceneElementsMap,
|
nextElements: SceneElementsMap,
|
||||||
applyDirection: "forward" | "backward" = "forward",
|
|
||||||
) {
|
) {
|
||||||
const nextAffectedElements = new Map<string, OrderedExcalidrawElement>();
|
const nextAffectedElements = new Map<string, OrderedExcalidrawElement>();
|
||||||
const updater = (
|
const updater = (
|
||||||
@@ -1766,36 +1572,21 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const prevElement = prevElements.get(element.id);
|
|
||||||
const nextVersion =
|
|
||||||
applyDirection === "forward"
|
|
||||||
? nextElement.version + 1
|
|
||||||
: nextElement.version - 1;
|
|
||||||
|
|
||||||
const elementUpdates = updates as ElementUpdate<OrderedExcalidrawElement>;
|
|
||||||
|
|
||||||
let affectedElement: OrderedExcalidrawElement;
|
let affectedElement: OrderedExcalidrawElement;
|
||||||
|
|
||||||
if (prevElement === nextElement) {
|
if (prevElements.get(element.id) === nextElement) {
|
||||||
// create the new element instance in case we didn't modify the element yet
|
// create the new element instance in case we didn't modify the element yet
|
||||||
// so that we won't end up in an incosistent state in case we would fail in the middle of mutations
|
// so that we won't end up in an incosistent state in case we would fail in the middle of mutations
|
||||||
affectedElement = newElementWith(
|
affectedElement = newElementWith(
|
||||||
nextElement,
|
nextElement,
|
||||||
{
|
updates as ElementUpdate<OrderedExcalidrawElement>,
|
||||||
...elementUpdates,
|
|
||||||
version: nextVersion,
|
|
||||||
},
|
|
||||||
true,
|
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
affectedElement = mutateElement(nextElement, nextElements, {
|
affectedElement = mutateElement(
|
||||||
...elementUpdates,
|
nextElement,
|
||||||
// don't modify the version further, if it's already different
|
nextElements,
|
||||||
version:
|
updates as ElementUpdate<OrderedExcalidrawElement>,
|
||||||
prevElement?.version !== nextElement.version
|
);
|
||||||
? nextElement.version
|
|
||||||
: nextVersion,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
nextAffectedElements.set(affectedElement.id, affectedElement);
|
nextAffectedElements.set(affectedElement.id, affectedElement);
|
||||||
@@ -1833,12 +1624,25 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
|
|||||||
Array.from(prevElements).filter(([id]) => nextAffectedElements.has(id)),
|
Array.from(prevElements).filter(([id]) => nextAffectedElements.has(id)),
|
||||||
);
|
);
|
||||||
|
|
||||||
// calculate complete deltas for affected elements, and squash them back to the current deltas
|
// calculate complete deltas for affected elements, and assign them back to all the deltas
|
||||||
this.squash(
|
// technically we could do better here if perf. would become an issue
|
||||||
// technically we could do better here if perf. would become an issue
|
const { added, removed, updated } = ElementsDelta.calculate(
|
||||||
ElementsDelta.calculate(prevAffectedElements, nextAffectedElements),
|
prevAffectedElements,
|
||||||
|
nextAffectedElements,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
for (const [id, delta] of Object.entries(added)) {
|
||||||
|
this.added[id] = delta;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [id, delta] of Object.entries(removed)) {
|
||||||
|
this.removed[id] = delta;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [id, delta] of Object.entries(updated)) {
|
||||||
|
this.updated[id] = delta;
|
||||||
|
}
|
||||||
|
|
||||||
return nextAffectedElements;
|
return nextAffectedElements;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1900,31 +1704,6 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
|
|||||||
BindableElement.rebindAffected(nextElements, nextElement(), updater);
|
BindableElement.rebindAffected(nextElements, nextElement(), updater);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static redrawElements(
|
|
||||||
nextElements: SceneElementsMap,
|
|
||||||
changedElements: Map<string, OrderedExcalidrawElement>,
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
// we don't have an up-to-date scene, as we can be just in the middle of applying history entry
|
|
||||||
// we also don't have a scene on the server
|
|
||||||
// so we are creating a temp scene just to query and mutate elements
|
|
||||||
const tempScene = new Scene(nextElements, { skipValidation: true });
|
|
||||||
|
|
||||||
ElementsDelta.redrawTextBoundingBoxes(tempScene, changedElements);
|
|
||||||
|
|
||||||
// needs ordered nextElements to avoid z-index binding issues
|
|
||||||
ElementsDelta.redrawBoundArrows(tempScene, changedElements);
|
|
||||||
} catch (e) {
|
|
||||||
console.error(`Couldn't redraw elements`, e);
|
|
||||||
|
|
||||||
if (isTestEnv() || isDevEnv()) {
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
return nextElements;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static redrawTextBoundingBoxes(
|
private static redrawTextBoundingBoxes(
|
||||||
scene: Scene,
|
scene: Scene,
|
||||||
changed: Map<string, OrderedExcalidrawElement>,
|
changed: Map<string, OrderedExcalidrawElement>,
|
||||||
@@ -1979,7 +1758,6 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
|
|||||||
) {
|
) {
|
||||||
for (const element of changed.values()) {
|
for (const element of changed.values()) {
|
||||||
if (!element.isDeleted && isBindableElement(element)) {
|
if (!element.isDeleted && isBindableElement(element)) {
|
||||||
// TODO: with precise bindings this is quite expensive, so consider optimisation so it's only triggered when the arrow does not intersect (imprecise) element bounds
|
|
||||||
updateBoundElements(element, scene, {
|
updateBoundElements(element, scene, {
|
||||||
changedElements: changed,
|
changedElements: changed,
|
||||||
});
|
});
|
||||||
@@ -2075,12 +1853,4 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
|
|||||||
|
|
||||||
return strippedPartial;
|
return strippedPartial;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static stripVersionProps(
|
|
||||||
partial: Partial<OrderedExcalidrawElement>,
|
|
||||||
): ElementPartial {
|
|
||||||
const { version, versionNonce, ...strippedPartial } = partial;
|
|
||||||
|
|
||||||
return strippedPartial;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,9 +76,8 @@ type MicroActionsQueue = (() => void)[];
|
|||||||
* Store which captures the observed changes and emits them as `StoreIncrement` events.
|
* Store which captures the observed changes and emits them as `StoreIncrement` events.
|
||||||
*/
|
*/
|
||||||
export class Store {
|
export class Store {
|
||||||
// for internal use by history
|
// internally used by history
|
||||||
public readonly onDurableIncrementEmitter = new Emitter<[DurableIncrement]>();
|
public readonly onDurableIncrementEmitter = new Emitter<[DurableIncrement]>();
|
||||||
// for public use as part of onIncrement API
|
|
||||||
public readonly onStoreIncrementEmitter = new Emitter<
|
public readonly onStoreIncrementEmitter = new Emitter<
|
||||||
[DurableIncrement | EphemeralIncrement]
|
[DurableIncrement | EphemeralIncrement]
|
||||||
>();
|
>();
|
||||||
@@ -240,6 +239,7 @@ export class Store {
|
|||||||
if (!storeDelta.isEmpty()) {
|
if (!storeDelta.isEmpty()) {
|
||||||
const increment = new DurableIncrement(storeChange, storeDelta);
|
const increment = new DurableIncrement(storeChange, storeDelta);
|
||||||
|
|
||||||
|
// Notify listeners with the increment
|
||||||
this.onDurableIncrementEmitter.trigger(increment);
|
this.onDurableIncrementEmitter.trigger(increment);
|
||||||
this.onStoreIncrementEmitter.trigger(increment);
|
this.onStoreIncrementEmitter.trigger(increment);
|
||||||
}
|
}
|
||||||
@@ -552,26 +552,10 @@ export class StoreDelta {
|
|||||||
public static load({
|
public static load({
|
||||||
id,
|
id,
|
||||||
elements: { added, removed, updated },
|
elements: { added, removed, updated },
|
||||||
appState: { delta: appStateDelta },
|
|
||||||
}: DTO<StoreDelta>) {
|
}: DTO<StoreDelta>) {
|
||||||
const elements = ElementsDelta.create(added, removed, updated);
|
const elements = ElementsDelta.create(added, removed, updated);
|
||||||
const appState = AppStateDelta.create(appStateDelta);
|
|
||||||
|
|
||||||
return new this(id, elements, appState);
|
return new this(id, elements, AppStateDelta.empty());
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Squash the passed deltas into the aggregated delta instance.
|
|
||||||
*/
|
|
||||||
public static squash(...deltas: StoreDelta[]) {
|
|
||||||
const aggregatedDelta = StoreDelta.empty();
|
|
||||||
|
|
||||||
for (const delta of deltas) {
|
|
||||||
aggregatedDelta.elements.squash(delta.elements);
|
|
||||||
aggregatedDelta.appState.squash(delta.appState);
|
|
||||||
}
|
|
||||||
|
|
||||||
return aggregatedDelta;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -588,7 +572,9 @@ export class StoreDelta {
|
|||||||
delta: StoreDelta,
|
delta: StoreDelta,
|
||||||
elements: SceneElementsMap,
|
elements: SceneElementsMap,
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
options?: ApplyToOptions,
|
options: ApplyToOptions = {
|
||||||
|
excludedProperties: new Set(),
|
||||||
|
},
|
||||||
): [SceneElementsMap, AppState, boolean] {
|
): [SceneElementsMap, AppState, boolean] {
|
||||||
const [nextElements, elementsContainVisibleChange] = delta.elements.applyTo(
|
const [nextElements, elementsContainVisibleChange] = delta.elements.applyTo(
|
||||||
elements,
|
elements,
|
||||||
@@ -627,10 +613,6 @@ export class StoreDelta {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static empty() {
|
|
||||||
return StoreDelta.create(ElementsDelta.empty(), AppStateDelta.empty());
|
|
||||||
}
|
|
||||||
|
|
||||||
public isEmpty() {
|
public isEmpty() {
|
||||||
return this.elements.isEmpty() && this.appState.isEmpty();
|
return this.elements.isEmpty() && this.appState.isEmpty();
|
||||||
}
|
}
|
||||||
@@ -996,7 +978,8 @@ const getDefaultObservedAppState = (): ObservedAppState => {
|
|||||||
viewBackgroundColor: COLOR_PALETTE.white,
|
viewBackgroundColor: COLOR_PALETTE.white,
|
||||||
selectedElementIds: {},
|
selectedElementIds: {},
|
||||||
selectedGroupIds: {},
|
selectedGroupIds: {},
|
||||||
selectedLinearElement: null,
|
selectedLinearElementId: null,
|
||||||
|
selectedLinearElementIsEditing: null,
|
||||||
croppingElementId: null,
|
croppingElementId: null,
|
||||||
activeLockedId: null,
|
activeLockedId: null,
|
||||||
lockedMultiSelections: {},
|
lockedMultiSelections: {},
|
||||||
@@ -1015,12 +998,14 @@ export const getObservedAppState = (
|
|||||||
croppingElementId: appState.croppingElementId,
|
croppingElementId: appState.croppingElementId,
|
||||||
activeLockedId: appState.activeLockedId,
|
activeLockedId: appState.activeLockedId,
|
||||||
lockedMultiSelections: appState.lockedMultiSelections,
|
lockedMultiSelections: appState.lockedMultiSelections,
|
||||||
selectedLinearElement: appState.selectedLinearElement
|
selectedLinearElementId:
|
||||||
? {
|
(appState as AppState).selectedLinearElement?.elementId ??
|
||||||
elementId: appState.selectedLinearElement.elementId,
|
(appState as ObservedAppState).selectedLinearElementId ??
|
||||||
isEditing: !!appState.selectedLinearElement.isEditing,
|
null,
|
||||||
}
|
selectedLinearElementIsEditing:
|
||||||
: null,
|
(appState as AppState).selectedLinearElement?.isEditing ??
|
||||||
|
(appState as ObservedAppState).selectedLinearElementIsEditing ??
|
||||||
|
null,
|
||||||
};
|
};
|
||||||
|
|
||||||
Reflect.defineProperty(observedAppState, hiddenObservedAppStateProp, {
|
Reflect.defineProperty(observedAppState, hiddenObservedAppStateProp, {
|
||||||
|
|||||||
@@ -1,328 +1,13 @@
|
|||||||
import { API } from "@excalidraw/excalidraw/tests/helpers/api";
|
|
||||||
|
|
||||||
import type { ObservedAppState } from "@excalidraw/excalidraw/types";
|
import type { ObservedAppState } from "@excalidraw/excalidraw/types";
|
||||||
import type { LinearElementEditor } from "@excalidraw/element";
|
import type { LinearElementEditor } from "@excalidraw/element";
|
||||||
import type { SceneElementsMap } from "@excalidraw/element/types";
|
|
||||||
|
|
||||||
import { AppStateDelta, Delta, ElementsDelta } from "../src/delta";
|
import { AppStateDelta } from "../src/delta";
|
||||||
|
|
||||||
describe("ElementsDelta", () => {
|
|
||||||
describe("elements delta calculation", () => {
|
|
||||||
it("should not create removed delta when element gets removed but was already deleted", () => {
|
|
||||||
const element = API.createElement({
|
|
||||||
type: "rectangle",
|
|
||||||
x: 100,
|
|
||||||
y: 100,
|
|
||||||
isDeleted: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const prevElements = new Map([[element.id, element]]);
|
|
||||||
const nextElements = new Map();
|
|
||||||
|
|
||||||
const delta = ElementsDelta.calculate(prevElements, nextElements);
|
|
||||||
|
|
||||||
expect(delta.isEmpty()).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should not create added delta when adding element as already deleted", () => {
|
|
||||||
const element = API.createElement({
|
|
||||||
type: "rectangle",
|
|
||||||
x: 100,
|
|
||||||
y: 100,
|
|
||||||
isDeleted: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const prevElements = new Map();
|
|
||||||
const nextElements = new Map([[element.id, element]]);
|
|
||||||
|
|
||||||
const delta = ElementsDelta.calculate(prevElements, nextElements);
|
|
||||||
|
|
||||||
expect(delta.isEmpty()).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should not create updated delta when there is only version and versionNonce change", () => {
|
|
||||||
const baseElement = API.createElement({
|
|
||||||
type: "rectangle",
|
|
||||||
x: 100,
|
|
||||||
y: 100,
|
|
||||||
strokeColor: "#000000",
|
|
||||||
backgroundColor: "#ffffff",
|
|
||||||
});
|
|
||||||
|
|
||||||
const modifiedElement = {
|
|
||||||
...baseElement,
|
|
||||||
version: baseElement.version + 1,
|
|
||||||
versionNonce: baseElement.versionNonce + 1,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create maps for the delta calculation
|
|
||||||
const prevElements = new Map([[baseElement.id, baseElement]]);
|
|
||||||
const nextElements = new Map([[modifiedElement.id, modifiedElement]]);
|
|
||||||
|
|
||||||
// Calculate the delta
|
|
||||||
const delta = ElementsDelta.calculate(
|
|
||||||
prevElements as SceneElementsMap,
|
|
||||||
nextElements as SceneElementsMap,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(delta.isEmpty()).toBeTruthy();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("squash", () => {
|
|
||||||
it("should not squash when second delta is empty", () => {
|
|
||||||
const updatedDelta = Delta.create(
|
|
||||||
{ x: 100, version: 1, versionNonce: 1 },
|
|
||||||
{ x: 200, version: 2, versionNonce: 2 },
|
|
||||||
);
|
|
||||||
|
|
||||||
const elementsDelta1 = ElementsDelta.create(
|
|
||||||
{},
|
|
||||||
{},
|
|
||||||
{ id1: updatedDelta },
|
|
||||||
);
|
|
||||||
const elementsDelta2 = ElementsDelta.empty();
|
|
||||||
const elementsDelta = elementsDelta1.squash(elementsDelta2);
|
|
||||||
|
|
||||||
expect(elementsDelta.isEmpty()).toBeFalsy();
|
|
||||||
expect(elementsDelta).toBe(elementsDelta1);
|
|
||||||
expect(elementsDelta.updated.id1).toBe(updatedDelta);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should squash mutually exclusive delta types", () => {
|
|
||||||
const addedDelta = Delta.create(
|
|
||||||
{ x: 100, version: 1, versionNonce: 1, isDeleted: true },
|
|
||||||
{ x: 200, version: 2, versionNonce: 2, isDeleted: false },
|
|
||||||
);
|
|
||||||
|
|
||||||
const removedDelta = Delta.create(
|
|
||||||
{ x: 100, version: 1, versionNonce: 1, isDeleted: false },
|
|
||||||
{ x: 200, version: 2, versionNonce: 2, isDeleted: true },
|
|
||||||
);
|
|
||||||
|
|
||||||
const updatedDelta = Delta.create(
|
|
||||||
{ x: 100, version: 1, versionNonce: 1 },
|
|
||||||
{ x: 200, version: 2, versionNonce: 2 },
|
|
||||||
);
|
|
||||||
|
|
||||||
const elementsDelta1 = ElementsDelta.create(
|
|
||||||
{ id1: addedDelta },
|
|
||||||
{ id2: removedDelta },
|
|
||||||
{},
|
|
||||||
);
|
|
||||||
|
|
||||||
const elementsDelta2 = ElementsDelta.create(
|
|
||||||
{},
|
|
||||||
{},
|
|
||||||
{ id3: updatedDelta },
|
|
||||||
);
|
|
||||||
|
|
||||||
const elementsDelta = elementsDelta1.squash(elementsDelta2);
|
|
||||||
|
|
||||||
expect(elementsDelta.isEmpty()).toBeFalsy();
|
|
||||||
expect(elementsDelta).toBe(elementsDelta1);
|
|
||||||
expect(elementsDelta.added.id1).toBe(addedDelta);
|
|
||||||
expect(elementsDelta.removed.id2).toBe(removedDelta);
|
|
||||||
expect(elementsDelta.updated.id3).toBe(updatedDelta);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should squash the same delta types", () => {
|
|
||||||
const elementsDelta1 = ElementsDelta.create(
|
|
||||||
{
|
|
||||||
id1: Delta.create(
|
|
||||||
{ x: 100, version: 1, versionNonce: 1, isDeleted: true },
|
|
||||||
{ x: 200, version: 2, versionNonce: 2, isDeleted: false },
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id2: Delta.create(
|
|
||||||
{ x: 100, version: 1, versionNonce: 1, isDeleted: false },
|
|
||||||
{ x: 200, version: 2, versionNonce: 2, isDeleted: true },
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id3: Delta.create(
|
|
||||||
{ x: 100, version: 1, versionNonce: 1 },
|
|
||||||
{ x: 200, version: 2, versionNonce: 2 },
|
|
||||||
),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const elementsDelta2 = ElementsDelta.create(
|
|
||||||
{
|
|
||||||
id1: Delta.create(
|
|
||||||
{ y: 100, version: 2, versionNonce: 2, isDeleted: true },
|
|
||||||
{ y: 200, version: 3, versionNonce: 3, isDeleted: false },
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id2: Delta.create(
|
|
||||||
{ y: 100, version: 2, versionNonce: 2, isDeleted: false },
|
|
||||||
{ y: 200, version: 3, versionNonce: 3, isDeleted: true },
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id3: Delta.create(
|
|
||||||
{ y: 100, version: 2, versionNonce: 2 },
|
|
||||||
{ y: 200, version: 3, versionNonce: 3 },
|
|
||||||
),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const elementsDelta = elementsDelta1.squash(elementsDelta2);
|
|
||||||
|
|
||||||
expect(elementsDelta.isEmpty()).toBeFalsy();
|
|
||||||
expect(elementsDelta).toBe(elementsDelta1);
|
|
||||||
expect(elementsDelta.added.id1).toEqual(
|
|
||||||
Delta.create(
|
|
||||||
{ x: 100, y: 100, version: 2, versionNonce: 2, isDeleted: true },
|
|
||||||
{ x: 200, y: 200, version: 3, versionNonce: 3, isDeleted: false },
|
|
||||||
),
|
|
||||||
);
|
|
||||||
expect(elementsDelta.removed.id2).toEqual(
|
|
||||||
Delta.create(
|
|
||||||
{ x: 100, y: 100, version: 2, versionNonce: 2, isDeleted: false },
|
|
||||||
{ x: 200, y: 200, version: 3, versionNonce: 3, isDeleted: true },
|
|
||||||
),
|
|
||||||
);
|
|
||||||
expect(elementsDelta.updated.id3).toEqual(
|
|
||||||
Delta.create(
|
|
||||||
{ x: 100, y: 100, version: 2, versionNonce: 2 },
|
|
||||||
{ x: 200, y: 200, version: 3, versionNonce: 3 },
|
|
||||||
),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should squash different delta types ", () => {
|
|
||||||
// id1: added -> updated => added
|
|
||||||
// id2: removed -> added => added
|
|
||||||
// id3: updated -> removed => removed
|
|
||||||
const elementsDelta1 = ElementsDelta.create(
|
|
||||||
{
|
|
||||||
id1: Delta.create(
|
|
||||||
{ x: 100, version: 1, versionNonce: 1, isDeleted: true },
|
|
||||||
{ x: 101, version: 2, versionNonce: 2, isDeleted: false },
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id2: Delta.create(
|
|
||||||
{ x: 200, version: 1, versionNonce: 1, isDeleted: false },
|
|
||||||
{ x: 201, version: 2, versionNonce: 2, isDeleted: true },
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id3: Delta.create(
|
|
||||||
{ x: 300, version: 1, versionNonce: 1 },
|
|
||||||
{ x: 301, version: 2, versionNonce: 2 },
|
|
||||||
),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const elementsDelta2 = ElementsDelta.create(
|
|
||||||
{
|
|
||||||
id2: Delta.create(
|
|
||||||
{ y: 200, version: 2, versionNonce: 2, isDeleted: true },
|
|
||||||
{ y: 201, version: 3, versionNonce: 3, isDeleted: false },
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id3: Delta.create(
|
|
||||||
{ y: 300, version: 2, versionNonce: 2, isDeleted: false },
|
|
||||||
{ y: 301, version: 3, versionNonce: 3, isDeleted: true },
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id1: Delta.create(
|
|
||||||
{ y: 100, version: 2, versionNonce: 2 },
|
|
||||||
{ y: 101, version: 3, versionNonce: 3 },
|
|
||||||
),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const elementsDelta = elementsDelta1.squash(elementsDelta2);
|
|
||||||
|
|
||||||
expect(elementsDelta.isEmpty()).toBeFalsy();
|
|
||||||
expect(elementsDelta).toBe(elementsDelta1);
|
|
||||||
expect(elementsDelta.added).toEqual({
|
|
||||||
id1: Delta.create(
|
|
||||||
{ x: 100, y: 100, version: 2, versionNonce: 2, isDeleted: true },
|
|
||||||
{ x: 101, y: 101, version: 3, versionNonce: 3, isDeleted: false },
|
|
||||||
),
|
|
||||||
id2: Delta.create(
|
|
||||||
{ x: 200, y: 200, version: 2, versionNonce: 2, isDeleted: true },
|
|
||||||
{ x: 201, y: 201, version: 3, versionNonce: 3, isDeleted: false },
|
|
||||||
),
|
|
||||||
});
|
|
||||||
expect(elementsDelta.removed).toEqual({
|
|
||||||
id3: Delta.create(
|
|
||||||
{ x: 300, y: 300, version: 2, versionNonce: 2, isDeleted: false },
|
|
||||||
{ x: 301, y: 301, version: 3, versionNonce: 3, isDeleted: true },
|
|
||||||
),
|
|
||||||
});
|
|
||||||
expect(elementsDelta.updated).toEqual({});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should squash bound elements", () => {
|
|
||||||
const elementsDelta1 = ElementsDelta.create(
|
|
||||||
{},
|
|
||||||
{},
|
|
||||||
{
|
|
||||||
id1: Delta.create(
|
|
||||||
{
|
|
||||||
version: 1,
|
|
||||||
versionNonce: 1,
|
|
||||||
boundElements: [{ id: "t1", type: "text" }],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
version: 2,
|
|
||||||
versionNonce: 2,
|
|
||||||
boundElements: [{ id: "t2", type: "text" }],
|
|
||||||
},
|
|
||||||
),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const elementsDelta2 = ElementsDelta.create(
|
|
||||||
{},
|
|
||||||
{},
|
|
||||||
{
|
|
||||||
id1: Delta.create(
|
|
||||||
{
|
|
||||||
version: 2,
|
|
||||||
versionNonce: 2,
|
|
||||||
boundElements: [{ id: "a1", type: "arrow" }],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
version: 3,
|
|
||||||
versionNonce: 3,
|
|
||||||
boundElements: [{ id: "a2", type: "arrow" }],
|
|
||||||
},
|
|
||||||
),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const elementsDelta = elementsDelta1.squash(elementsDelta2);
|
|
||||||
|
|
||||||
expect(elementsDelta.updated.id1.deleted.boundElements).toEqual([
|
|
||||||
{ id: "t1", type: "text" },
|
|
||||||
{ id: "a1", type: "arrow" },
|
|
||||||
]);
|
|
||||||
expect(elementsDelta.updated.id1.inserted.boundElements).toEqual([
|
|
||||||
{ id: "t2", type: "text" },
|
|
||||||
{ id: "a2", type: "arrow" },
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("AppStateDelta", () => {
|
describe("AppStateDelta", () => {
|
||||||
describe("ensure stable delta properties order", () => {
|
describe("ensure stable delta properties order", () => {
|
||||||
it("should maintain stable order for root properties", () => {
|
it("should maintain stable order for root properties", () => {
|
||||||
const name = "untitled scene";
|
const name = "untitled scene";
|
||||||
const selectedLinearElement = {
|
const selectedLinearElementId = "id1" as LinearElementEditor["elementId"];
|
||||||
elementId: "id1" as LinearElementEditor["elementId"],
|
|
||||||
isEditing: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
const commonAppState = {
|
const commonAppState = {
|
||||||
viewBackgroundColor: "#ffffff",
|
viewBackgroundColor: "#ffffff",
|
||||||
@@ -339,23 +24,23 @@ describe("AppStateDelta", () => {
|
|||||||
const prevAppState1: ObservedAppState = {
|
const prevAppState1: ObservedAppState = {
|
||||||
...commonAppState,
|
...commonAppState,
|
||||||
name: "",
|
name: "",
|
||||||
selectedLinearElement: null,
|
selectedLinearElementId: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
const nextAppState1: ObservedAppState = {
|
const nextAppState1: ObservedAppState = {
|
||||||
...commonAppState,
|
...commonAppState,
|
||||||
name,
|
name,
|
||||||
selectedLinearElement,
|
selectedLinearElementId,
|
||||||
};
|
};
|
||||||
|
|
||||||
const prevAppState2: ObservedAppState = {
|
const prevAppState2: ObservedAppState = {
|
||||||
selectedLinearElement: null,
|
selectedLinearElementId: null,
|
||||||
name: "",
|
name: "",
|
||||||
...commonAppState,
|
...commonAppState,
|
||||||
};
|
};
|
||||||
|
|
||||||
const nextAppState2: ObservedAppState = {
|
const nextAppState2: ObservedAppState = {
|
||||||
selectedLinearElement,
|
selectedLinearElementId,
|
||||||
name,
|
name,
|
||||||
...commonAppState,
|
...commonAppState,
|
||||||
};
|
};
|
||||||
@@ -373,7 +58,9 @@ describe("AppStateDelta", () => {
|
|||||||
selectedGroupIds: {},
|
selectedGroupIds: {},
|
||||||
editingGroupId: null,
|
editingGroupId: null,
|
||||||
croppingElementId: null,
|
croppingElementId: null,
|
||||||
selectedLinearElement: null,
|
selectedLinearElementId: null,
|
||||||
|
selectedLinearElementIsEditing: null,
|
||||||
|
editingLinearElementId: null,
|
||||||
activeLockedId: null,
|
activeLockedId: null,
|
||||||
lockedMultiSelections: {},
|
lockedMultiSelections: {},
|
||||||
};
|
};
|
||||||
@@ -419,7 +106,9 @@ describe("AppStateDelta", () => {
|
|||||||
selectedElementIds: {},
|
selectedElementIds: {},
|
||||||
editingGroupId: null,
|
editingGroupId: null,
|
||||||
croppingElementId: null,
|
croppingElementId: null,
|
||||||
selectedLinearElement: null,
|
selectedLinearElementId: null,
|
||||||
|
selectedLinearElementIsEditing: null,
|
||||||
|
editingLinearElementId: null,
|
||||||
activeLockedId: null,
|
activeLockedId: null,
|
||||||
lockedMultiSelections: {},
|
lockedMultiSelections: {},
|
||||||
};
|
};
|
||||||
@@ -460,97 +149,4 @@ describe("AppStateDelta", () => {
|
|||||||
expect(JSON.stringify(delta1)).toBe(JSON.stringify(delta2));
|
expect(JSON.stringify(delta1)).toBe(JSON.stringify(delta2));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("squash", () => {
|
|
||||||
it("should not squash when second delta is empty", () => {
|
|
||||||
const delta = Delta.create(
|
|
||||||
{ name: "untitled scene" },
|
|
||||||
{ name: "titled scene" },
|
|
||||||
);
|
|
||||||
|
|
||||||
const appStateDelta1 = AppStateDelta.create(delta);
|
|
||||||
const appStateDelta2 = AppStateDelta.empty();
|
|
||||||
const appStateDelta = appStateDelta1.squash(appStateDelta2);
|
|
||||||
|
|
||||||
expect(appStateDelta.isEmpty()).toBeFalsy();
|
|
||||||
expect(appStateDelta).toBe(appStateDelta1);
|
|
||||||
expect(appStateDelta.delta).toBe(delta);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should squash exclusive properties", () => {
|
|
||||||
const delta1 = Delta.create(
|
|
||||||
{ name: "untitled scene" },
|
|
||||||
{ name: "titled scene" },
|
|
||||||
);
|
|
||||||
const delta2 = Delta.create(
|
|
||||||
{ viewBackgroundColor: "#ffffff" },
|
|
||||||
{ viewBackgroundColor: "#000000" },
|
|
||||||
);
|
|
||||||
|
|
||||||
const appStateDelta1 = AppStateDelta.create(delta1);
|
|
||||||
const appStateDelta2 = AppStateDelta.create(delta2);
|
|
||||||
const appStateDelta = appStateDelta1.squash(appStateDelta2);
|
|
||||||
|
|
||||||
expect(appStateDelta.isEmpty()).toBeFalsy();
|
|
||||||
expect(appStateDelta).toBe(appStateDelta1);
|
|
||||||
expect(appStateDelta.delta).toEqual(
|
|
||||||
Delta.create(
|
|
||||||
{ name: "untitled scene", viewBackgroundColor: "#ffffff" },
|
|
||||||
{ name: "titled scene", viewBackgroundColor: "#000000" },
|
|
||||||
),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should squash selectedElementIds, selectedGroupIds and lockedMultiSelections", () => {
|
|
||||||
const delta1 = Delta.create<Partial<ObservedAppState>>(
|
|
||||||
{
|
|
||||||
name: "untitled scene",
|
|
||||||
selectedElementIds: { id1: true },
|
|
||||||
selectedGroupIds: {},
|
|
||||||
lockedMultiSelections: { g1: true },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "titled scene",
|
|
||||||
selectedElementIds: { id2: true },
|
|
||||||
selectedGroupIds: { g1: true },
|
|
||||||
lockedMultiSelections: {},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
const delta2 = Delta.create<Partial<ObservedAppState>>(
|
|
||||||
{
|
|
||||||
selectedElementIds: { id3: true },
|
|
||||||
selectedGroupIds: { g1: true },
|
|
||||||
lockedMultiSelections: {},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
selectedElementIds: { id2: true },
|
|
||||||
selectedGroupIds: { g2: true, g3: true },
|
|
||||||
lockedMultiSelections: { g3: true },
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const appStateDelta1 = AppStateDelta.create(delta1);
|
|
||||||
const appStateDelta2 = AppStateDelta.create(delta2);
|
|
||||||
const appStateDelta = appStateDelta1.squash(appStateDelta2);
|
|
||||||
|
|
||||||
expect(appStateDelta.isEmpty()).toBeFalsy();
|
|
||||||
expect(appStateDelta).toBe(appStateDelta1);
|
|
||||||
expect(appStateDelta.delta).toEqual(
|
|
||||||
Delta.create<Partial<ObservedAppState>>(
|
|
||||||
{
|
|
||||||
name: "untitled scene",
|
|
||||||
selectedElementIds: { id1: true, id3: true },
|
|
||||||
selectedGroupIds: { g1: true },
|
|
||||||
lockedMultiSelections: { g1: true },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "titled scene",
|
|
||||||
selectedElementIds: { id2: true },
|
|
||||||
selectedGroupIds: { g1: true, g2: true, g3: true },
|
|
||||||
lockedMultiSelections: { g3: true },
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
rules: {
|
|
||||||
"no-binding-direct-mod": require("./no-binding-direct-mod"),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
/** @type {import('eslint').Rule.RuleModule} */
|
|
||||||
module.exports = {
|
|
||||||
meta: {
|
|
||||||
type: "problem",
|
|
||||||
docs: {
|
|
||||||
description:
|
|
||||||
"disallow direct mutation of startBinding or endBinding via mutateElement",
|
|
||||||
category: "Best Practices",
|
|
||||||
recommended: false,
|
|
||||||
},
|
|
||||||
fixable: null,
|
|
||||||
schema: [],
|
|
||||||
messages: {
|
|
||||||
noDirectBindingMutation:
|
|
||||||
"Direct mutation of {{ property }} via mutateElement() is not allowed. Use proper binding update functions instead.",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
create(context) {
|
|
||||||
return {
|
|
||||||
CallExpression(node) {
|
|
||||||
// Check if this is a call to mutateElement (direct call or method call)
|
|
||||||
let isMutateElementCall = false;
|
|
||||||
|
|
||||||
if (
|
|
||||||
node.callee.type === "Identifier" &&
|
|
||||||
node.callee.name === "mutateElement"
|
|
||||||
) {
|
|
||||||
// Direct call: mutateElement()
|
|
||||||
isMutateElementCall = true;
|
|
||||||
} else if (
|
|
||||||
node.callee.type === "MemberExpression" &&
|
|
||||||
node.callee.property.type === "Identifier" &&
|
|
||||||
node.callee.property.name === "mutateElement"
|
|
||||||
) {
|
|
||||||
// Method call: something.mutateElement() or this.scene.mutateElement()
|
|
||||||
isMutateElementCall = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isMutateElementCall) {
|
|
||||||
// mutateElement can have different argument patterns:
|
|
||||||
// 1. mutateElement(element, updates) - 2 args
|
|
||||||
// 2. mutateElement(element, elementsMap, updates) - 3 args
|
|
||||||
// 3. mutateElement(element, updates, options) - 3 args
|
|
||||||
let updatesArg = null;
|
|
||||||
|
|
||||||
if (node.arguments.length >= 2) {
|
|
||||||
// Try second argument first (most common pattern)
|
|
||||||
const secondArg = node.arguments[1];
|
|
||||||
if (secondArg.type === "ObjectExpression") {
|
|
||||||
updatesArg = secondArg;
|
|
||||||
} else if (node.arguments.length >= 3) {
|
|
||||||
// If second arg is not an object, try third argument
|
|
||||||
const thirdArg = node.arguments[2];
|
|
||||||
if (thirdArg.type === "ObjectExpression") {
|
|
||||||
updatesArg = thirdArg;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (updatesArg) {
|
|
||||||
// Look for startBinding or endBinding properties
|
|
||||||
for (const property of updatesArg.properties) {
|
|
||||||
if (
|
|
||||||
property.type === "Property" &&
|
|
||||||
property.key.type === "Identifier" &&
|
|
||||||
(property.key.name === "startBinding" ||
|
|
||||||
property.key.name === "endBinding")
|
|
||||||
) {
|
|
||||||
context.report({
|
|
||||||
node: property,
|
|
||||||
messageId: "noDirectBindingMutation",
|
|
||||||
data: {
|
|
||||||
property: property.key.name,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "eslint-plugin-excalidraw",
|
|
||||||
"version": "0.1.0",
|
|
||||||
"main": "index.js",
|
|
||||||
"scripts": {
|
|
||||||
"lint": "eslint ."
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"eslint": "^7.32.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -7,7 +7,6 @@ import {
|
|||||||
getFontString,
|
getFontString,
|
||||||
} from "@excalidraw/common";
|
} from "@excalidraw/common";
|
||||||
import {
|
import {
|
||||||
bindOrUnbindLinearElement,
|
|
||||||
getOriginalContainerHeightFromCache,
|
getOriginalContainerHeightFromCache,
|
||||||
resetOriginalContainerCache,
|
resetOriginalContainerCache,
|
||||||
updateOriginalContainerCache,
|
updateOriginalContainerCache,
|
||||||
@@ -37,7 +36,6 @@ import { newElement } from "@excalidraw/element";
|
|||||||
import { CaptureUpdateAction } from "@excalidraw/element";
|
import { CaptureUpdateAction } from "@excalidraw/element";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
ExcalidrawBindableElement,
|
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
ExcalidrawLinearElement,
|
ExcalidrawLinearElement,
|
||||||
ExcalidrawTextContainer,
|
ExcalidrawTextContainer,
|
||||||
@@ -272,7 +270,7 @@ export const actionWrapTextInContainer = register({
|
|||||||
),
|
),
|
||||||
groupIds: textElement.groupIds,
|
groupIds: textElement.groupIds,
|
||||||
frameId: textElement.frameId,
|
frameId: textElement.frameId,
|
||||||
}) as ExcalidrawBindableElement;
|
});
|
||||||
|
|
||||||
// update bindings
|
// update bindings
|
||||||
if (textElement.boundElements?.length) {
|
if (textElement.boundElements?.length) {
|
||||||
@@ -283,14 +281,26 @@ export const actionWrapTextInContainer = register({
|
|||||||
linearElementIds.includes(ele.id),
|
linearElementIds.includes(ele.id),
|
||||||
) as ExcalidrawLinearElement[];
|
) as ExcalidrawLinearElement[];
|
||||||
linearElements.forEach((ele) => {
|
linearElements.forEach((ele) => {
|
||||||
bindOrUnbindLinearElement(
|
let startBinding = ele.startBinding;
|
||||||
ele,
|
let endBinding = ele.endBinding;
|
||||||
ele.startBinding?.elementId === textElement.id
|
|
||||||
? container
|
if (startBinding?.elementId === textElement.id) {
|
||||||
: "keep",
|
startBinding = {
|
||||||
ele.endBinding?.elementId === textElement.id ? container : "keep",
|
...startBinding,
|
||||||
app.scene,
|
elementId: container.id,
|
||||||
);
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (endBinding?.elementId === textElement.id) {
|
||||||
|
endBinding = { ...endBinding, elementId: container.id };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (startBinding || endBinding) {
|
||||||
|
app.scene.mutateElement(ele, {
|
||||||
|
startBinding,
|
||||||
|
endBinding,
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ export const actionClearCanvas = register({
|
|||||||
pasteDialog: appState.pasteDialog,
|
pasteDialog: appState.pasteDialog,
|
||||||
activeTool:
|
activeTool:
|
||||||
appState.activeTool.type === "image"
|
appState.activeTool.type === "image"
|
||||||
? { ...appState.activeTool, type: app.defaultSelectionTool }
|
? { ...appState.activeTool, type: "selection" }
|
||||||
: appState.activeTool,
|
: appState.activeTool,
|
||||||
},
|
},
|
||||||
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
|
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
|
||||||
@@ -494,13 +494,13 @@ export const actionToggleEraserTool = register({
|
|||||||
name: "toggleEraserTool",
|
name: "toggleEraserTool",
|
||||||
label: "toolBar.eraser",
|
label: "toolBar.eraser",
|
||||||
trackEvent: { category: "toolbar" },
|
trackEvent: { category: "toolbar" },
|
||||||
perform: (elements, appState, _, app) => {
|
perform: (elements, appState) => {
|
||||||
let activeTool: AppState["activeTool"];
|
let activeTool: AppState["activeTool"];
|
||||||
|
|
||||||
if (isEraserActive(appState)) {
|
if (isEraserActive(appState)) {
|
||||||
activeTool = updateActiveTool(appState, {
|
activeTool = updateActiveTool(appState, {
|
||||||
...(appState.activeTool.lastActiveTool || {
|
...(appState.activeTool.lastActiveTool || {
|
||||||
type: app.defaultSelectionTool,
|
type: "selection",
|
||||||
}),
|
}),
|
||||||
lastActiveToolBeforeEraser: null,
|
lastActiveToolBeforeEraser: null,
|
||||||
});
|
});
|
||||||
@@ -530,9 +530,6 @@ export const actionToggleLassoTool = register({
|
|||||||
label: "toolBar.lasso",
|
label: "toolBar.lasso",
|
||||||
icon: LassoIcon,
|
icon: LassoIcon,
|
||||||
trackEvent: { category: "toolbar" },
|
trackEvent: { category: "toolbar" },
|
||||||
predicate: (elements, appState, props, app) => {
|
|
||||||
return app.defaultSelectionTool !== "lasso";
|
|
||||||
},
|
|
||||||
perform: (elements, appState, _, app) => {
|
perform: (elements, appState, _, app) => {
|
||||||
let activeTool: AppState["activeTool"];
|
let activeTool: AppState["activeTool"];
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
import { KEYS, updateActiveTool } from "@excalidraw/common";
|
import { KEYS, updateActiveTool } from "@excalidraw/common";
|
||||||
|
|
||||||
import {
|
import { getNonDeletedElements } from "@excalidraw/element";
|
||||||
bindOrUnbindLinearElement,
|
|
||||||
getNonDeletedElements,
|
|
||||||
} from "@excalidraw/element";
|
|
||||||
import { fixBindingsAfterDeletion } from "@excalidraw/element";
|
import { fixBindingsAfterDeletion } from "@excalidraw/element";
|
||||||
import { LinearElementEditor } from "@excalidraw/element";
|
import { LinearElementEditor } from "@excalidraw/element";
|
||||||
import { newElementWith } from "@excalidraw/element";
|
import { newElementWith } from "@excalidraw/element";
|
||||||
@@ -95,14 +92,14 @@ const deleteSelectedElements = (
|
|||||||
el.boundElements.forEach((candidate) => {
|
el.boundElements.forEach((candidate) => {
|
||||||
const bound = app.scene.getNonDeletedElementsMap().get(candidate.id);
|
const bound = app.scene.getNonDeletedElementsMap().get(candidate.id);
|
||||||
if (bound && isElbowArrow(bound)) {
|
if (bound && isElbowArrow(bound)) {
|
||||||
if (el.id === bound.startBinding?.elementId) {
|
app.scene.mutateElement(bound, {
|
||||||
bindOrUnbindLinearElement(
|
startBinding:
|
||||||
bound,
|
el.id === bound.startBinding?.elementId
|
||||||
el.id === bound.startBinding?.elementId ? null : "keep",
|
? null
|
||||||
el.id === bound.endBinding?.elementId ? null : "keep",
|
: bound.startBinding,
|
||||||
app.scene,
|
endBinding:
|
||||||
);
|
el.id === bound.endBinding?.elementId ? null : bound.endBinding,
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -301,9 +298,7 @@ export const actionDeleteSelected = register({
|
|||||||
elements: nextElements,
|
elements: nextElements,
|
||||||
appState: {
|
appState: {
|
||||||
...nextAppState,
|
...nextAppState,
|
||||||
activeTool: updateActiveTool(appState, {
|
activeTool: updateActiveTool(appState, { type: "selection" }),
|
||||||
type: app.defaultSelectionTool,
|
|
||||||
}),
|
|
||||||
multiElement: null,
|
multiElement: null,
|
||||||
activeEmbeddable: null,
|
activeEmbeddable: null,
|
||||||
selectedLinearElement: null,
|
selectedLinearElement: null,
|
||||||
|
|||||||
@@ -5,11 +5,7 @@ import {
|
|||||||
bindOrUnbindLinearElement,
|
bindOrUnbindLinearElement,
|
||||||
isBindingEnabled,
|
isBindingEnabled,
|
||||||
} from "@excalidraw/element/binding";
|
} from "@excalidraw/element/binding";
|
||||||
import {
|
import { isValidPolygon, LinearElementEditor } from "@excalidraw/element";
|
||||||
isValidPolygon,
|
|
||||||
LinearElementEditor,
|
|
||||||
newElementWith,
|
|
||||||
} from "@excalidraw/element";
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
isBindingElement,
|
isBindingElement,
|
||||||
@@ -82,14 +78,7 @@ export const actionFinalize = register({
|
|||||||
let newElements = elements;
|
let newElements = elements;
|
||||||
if (element && isInvisiblySmallElement(element)) {
|
if (element && isInvisiblySmallElement(element)) {
|
||||||
// TODO: #7348 in theory this gets recorded by the store, so the invisible elements could be restored by the undo/redo, which might be not what we would want
|
// TODO: #7348 in theory this gets recorded by the store, so the invisible elements could be restored by the undo/redo, which might be not what we would want
|
||||||
newElements = newElements.map((el) => {
|
newElements = newElements.filter((el) => el.id !== element!.id);
|
||||||
if (el.id === element.id) {
|
|
||||||
return newElementWith(el, {
|
|
||||||
isDeleted: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return el;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
elements: newElements,
|
elements: newElements,
|
||||||
@@ -128,12 +117,7 @@ export const actionFinalize = register({
|
|||||||
return {
|
return {
|
||||||
elements:
|
elements:
|
||||||
element.points.length < 2 || isInvisiblySmallElement(element)
|
element.points.length < 2 || isInvisiblySmallElement(element)
|
||||||
? elements.map((el) => {
|
? elements.filter((el) => el.id !== element.id)
|
||||||
if (el.id === element.id) {
|
|
||||||
return newElementWith(el, { isDeleted: true });
|
|
||||||
}
|
|
||||||
return el;
|
|
||||||
})
|
|
||||||
: undefined,
|
: undefined,
|
||||||
appState: {
|
appState: {
|
||||||
...appState,
|
...appState,
|
||||||
@@ -188,12 +172,7 @@ export const actionFinalize = register({
|
|||||||
|
|
||||||
if (element && isInvisiblySmallElement(element)) {
|
if (element && isInvisiblySmallElement(element)) {
|
||||||
// TODO: #7348 in theory this gets recorded by the store, so the invisible elements could be restored by the undo/redo, which might be not what we would want
|
// TODO: #7348 in theory this gets recorded by the store, so the invisible elements could be restored by the undo/redo, which might be not what we would want
|
||||||
newElements = newElements.map((el) => {
|
newElements = newElements.filter((el) => el.id !== element!.id);
|
||||||
if (el.id === element?.id) {
|
|
||||||
return newElementWith(el, { isDeleted: true });
|
|
||||||
}
|
|
||||||
return el;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isLinearElement(element) || isFreeDrawElement(element)) {
|
if (isLinearElement(element) || isFreeDrawElement(element)) {
|
||||||
@@ -261,13 +240,13 @@ export const actionFinalize = register({
|
|||||||
if (appState.activeTool.type === "eraser") {
|
if (appState.activeTool.type === "eraser") {
|
||||||
activeTool = updateActiveTool(appState, {
|
activeTool = updateActiveTool(appState, {
|
||||||
...(appState.activeTool.lastActiveTool || {
|
...(appState.activeTool.lastActiveTool || {
|
||||||
type: app.defaultSelectionTool,
|
type: "selection",
|
||||||
}),
|
}),
|
||||||
lastActiveToolBeforeEraser: null,
|
lastActiveToolBeforeEraser: null,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
activeTool = updateActiveTool(appState, {
|
activeTool = updateActiveTool(appState, {
|
||||||
type: app.defaultSelectionTool,
|
type: "selection",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ import {
|
|||||||
hasStrokeWidth,
|
hasStrokeWidth,
|
||||||
} from "../scene";
|
} from "../scene";
|
||||||
|
|
||||||
import { getToolbarTools } from "./shapes";
|
import { SHAPES } from "./shapes";
|
||||||
|
|
||||||
import "./Actions.scss";
|
import "./Actions.scss";
|
||||||
|
|
||||||
@@ -295,8 +295,7 @@ export const ShapesSwitcher = ({
|
|||||||
|
|
||||||
const frameToolSelected = activeTool.type === "frame";
|
const frameToolSelected = activeTool.type === "frame";
|
||||||
const laserToolSelected = activeTool.type === "laser";
|
const laserToolSelected = activeTool.type === "laser";
|
||||||
const lassoToolSelected =
|
const lassoToolSelected = activeTool.type === "lasso";
|
||||||
activeTool.type === "lasso" && app.defaultSelectionTool !== "lasso";
|
|
||||||
|
|
||||||
const embeddableToolSelected = activeTool.type === "embeddable";
|
const embeddableToolSelected = activeTool.type === "embeddable";
|
||||||
|
|
||||||
@@ -304,68 +303,63 @@ export const ShapesSwitcher = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{getToolbarTools(app).map(
|
{SHAPES.map(({ value, icon, key, numericKey, fillable }, index) => {
|
||||||
({ value, icon, key, numericKey, fillable }, index) => {
|
if (
|
||||||
if (
|
UIOptions.tools?.[
|
||||||
UIOptions.tools?.[
|
value as Extract<typeof value, keyof AppProps["UIOptions"]["tools"]>
|
||||||
value as Extract<
|
] === false
|
||||||
typeof value,
|
) {
|
||||||
keyof AppProps["UIOptions"]["tools"]
|
return null;
|
||||||
>
|
}
|
||||||
] === false
|
|
||||||
) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const label = t(`toolBar.${value}`);
|
const label = t(`toolBar.${value}`);
|
||||||
const letter =
|
const letter =
|
||||||
key && capitalizeString(typeof key === "string" ? key : key[0]);
|
key && capitalizeString(typeof key === "string" ? key : key[0]);
|
||||||
const shortcut = letter
|
const shortcut = letter
|
||||||
? `${letter} ${t("helpDialog.or")} ${numericKey}`
|
? `${letter} ${t("helpDialog.or")} ${numericKey}`
|
||||||
: `${numericKey}`;
|
: `${numericKey}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ToolButton
|
<ToolButton
|
||||||
className={clsx("Shape", { fillable })}
|
className={clsx("Shape", { fillable })}
|
||||||
key={value}
|
key={value}
|
||||||
type="radio"
|
type="radio"
|
||||||
icon={icon}
|
icon={icon}
|
||||||
checked={activeTool.type === value}
|
checked={activeTool.type === value}
|
||||||
name="editor-current-shape"
|
name="editor-current-shape"
|
||||||
title={`${capitalizeString(label)} — ${shortcut}`}
|
title={`${capitalizeString(label)} — ${shortcut}`}
|
||||||
keyBindingLabel={numericKey || letter}
|
keyBindingLabel={numericKey || letter}
|
||||||
aria-label={capitalizeString(label)}
|
aria-label={capitalizeString(label)}
|
||||||
aria-keyshortcuts={shortcut}
|
aria-keyshortcuts={shortcut}
|
||||||
data-testid={`toolbar-${value}`}
|
data-testid={`toolbar-${value}`}
|
||||||
onPointerDown={({ pointerType }) => {
|
onPointerDown={({ pointerType }) => {
|
||||||
if (!appState.penDetected && pointerType === "pen") {
|
if (!appState.penDetected && pointerType === "pen") {
|
||||||
app.togglePenMode(true);
|
app.togglePenMode(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (value === "selection") {
|
if (value === "selection") {
|
||||||
if (appState.activeTool.type === "selection") {
|
if (appState.activeTool.type === "selection") {
|
||||||
app.setActiveTool({ type: "lasso" });
|
app.setActiveTool({ type: "lasso" });
|
||||||
} else {
|
|
||||||
app.setActiveTool({ type: "selection" });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onChange={({ pointerType }) => {
|
|
||||||
if (appState.activeTool.type !== value) {
|
|
||||||
trackEvent("toolbar", value, "ui");
|
|
||||||
}
|
|
||||||
if (value === "image") {
|
|
||||||
app.setActiveTool({
|
|
||||||
type: value,
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
app.setActiveTool({ type: value });
|
app.setActiveTool({ type: "selection" });
|
||||||
}
|
}
|
||||||
}}
|
}
|
||||||
/>
|
}}
|
||||||
);
|
onChange={({ pointerType }) => {
|
||||||
},
|
if (appState.activeTool.type !== value) {
|
||||||
)}
|
trackEvent("toolbar", value, "ui");
|
||||||
|
}
|
||||||
|
if (value === "image") {
|
||||||
|
app.setActiveTool({
|
||||||
|
type: value,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
app.setActiveTool({ type: value });
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
<div className="App-toolbar__divider" />
|
<div className="App-toolbar__divider" />
|
||||||
|
|
||||||
<DropdownMenu open={isExtraToolsMenuOpen}>
|
<DropdownMenu open={isExtraToolsMenuOpen}>
|
||||||
@@ -424,16 +418,14 @@ export const ShapesSwitcher = ({
|
|||||||
>
|
>
|
||||||
{t("toolBar.laser")}
|
{t("toolBar.laser")}
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
{app.defaultSelectionTool !== "lasso" && (
|
<DropdownMenu.Item
|
||||||
<DropdownMenu.Item
|
onSelect={() => app.setActiveTool({ type: "lasso" })}
|
||||||
onSelect={() => app.setActiveTool({ type: "lasso" })}
|
icon={LassoIcon}
|
||||||
icon={LassoIcon}
|
data-testid="toolbar-lasso"
|
||||||
data-testid="toolbar-lasso"
|
selected={lassoToolSelected}
|
||||||
selected={lassoToolSelected}
|
>
|
||||||
>
|
{t("toolBar.lasso")}
|
||||||
{t("toolBar.lasso")}
|
</DropdownMenu.Item>
|
||||||
</DropdownMenu.Item>
|
|
||||||
)}
|
|
||||||
<div style={{ margin: "6px 0", fontSize: 14, fontWeight: 600 }}>
|
<div style={{ margin: "6px 0", fontSize: 14, fontWeight: 600 }}>
|
||||||
Generate
|
Generate
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -100,7 +100,6 @@ import {
|
|||||||
randomInteger,
|
randomInteger,
|
||||||
CLASSES,
|
CLASSES,
|
||||||
Emitter,
|
Emitter,
|
||||||
isMobile,
|
|
||||||
MINIMUM_ARROW_SIZE,
|
MINIMUM_ARROW_SIZE,
|
||||||
} from "@excalidraw/common";
|
} from "@excalidraw/common";
|
||||||
|
|
||||||
@@ -234,8 +233,6 @@ import {
|
|||||||
hitElementBoundingBox,
|
hitElementBoundingBox,
|
||||||
isLineElement,
|
isLineElement,
|
||||||
isSimpleArrow,
|
isSimpleArrow,
|
||||||
StoreDelta,
|
|
||||||
type ApplyToOptions,
|
|
||||||
} from "@excalidraw/element";
|
} from "@excalidraw/element";
|
||||||
|
|
||||||
import type { LocalPoint, Radians } from "@excalidraw/math";
|
import type { LocalPoint, Radians } from "@excalidraw/math";
|
||||||
@@ -262,7 +259,6 @@ import type {
|
|||||||
MagicGenerationData,
|
MagicGenerationData,
|
||||||
ExcalidrawArrowElement,
|
ExcalidrawArrowElement,
|
||||||
ExcalidrawElbowArrowElement,
|
ExcalidrawElbowArrowElement,
|
||||||
SceneElementsMap,
|
|
||||||
} from "@excalidraw/element/types";
|
} from "@excalidraw/element/types";
|
||||||
|
|
||||||
import type { Mutable, ValueOf } from "@excalidraw/common/utility-types";
|
import type { Mutable, ValueOf } from "@excalidraw/common/utility-types";
|
||||||
@@ -654,14 +650,9 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
>();
|
>();
|
||||||
onRemoveEventListenersEmitter = new Emitter<[]>();
|
onRemoveEventListenersEmitter = new Emitter<[]>();
|
||||||
|
|
||||||
defaultSelectionTool: "selection" | "lasso" = "selection";
|
|
||||||
|
|
||||||
constructor(props: AppProps) {
|
constructor(props: AppProps) {
|
||||||
super(props);
|
super(props);
|
||||||
const defaultAppState = getDefaultAppState();
|
const defaultAppState = getDefaultAppState();
|
||||||
this.defaultSelectionTool = this.isMobileOrTablet()
|
|
||||||
? ("lasso" as const)
|
|
||||||
: ("selection" as const);
|
|
||||||
const {
|
const {
|
||||||
excalidrawAPI,
|
excalidrawAPI,
|
||||||
viewModeEnabled = false,
|
viewModeEnabled = false,
|
||||||
@@ -706,7 +697,6 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
if (excalidrawAPI) {
|
if (excalidrawAPI) {
|
||||||
const api: ExcalidrawImperativeAPI = {
|
const api: ExcalidrawImperativeAPI = {
|
||||||
updateScene: this.updateScene,
|
updateScene: this.updateScene,
|
||||||
applyDeltas: this.applyDeltas,
|
|
||||||
mutateElement: this.mutateElement,
|
mutateElement: this.mutateElement,
|
||||||
updateLibrary: this.library.updateLibrary,
|
updateLibrary: this.library.updateLibrary,
|
||||||
addFiles: this.addFiles,
|
addFiles: this.addFiles,
|
||||||
@@ -1612,8 +1602,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
renderWelcomeScreen={
|
renderWelcomeScreen={
|
||||||
!this.state.isLoading &&
|
!this.state.isLoading &&
|
||||||
this.state.showWelcomeScreen &&
|
this.state.showWelcomeScreen &&
|
||||||
this.state.activeTool.type ===
|
this.state.activeTool.type === "selection" &&
|
||||||
this.defaultSelectionTool &&
|
|
||||||
!this.state.zenModeEnabled &&
|
!this.state.zenModeEnabled &&
|
||||||
!this.scene.getElementsIncludingDeleted().length
|
!this.scene.getElementsIncludingDeleted().length
|
||||||
}
|
}
|
||||||
@@ -2353,11 +2342,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const scene = restore(initialData, null, null, {
|
const scene = restore(initialData, null, null, { repairBindings: true });
|
||||||
repairBindings: true,
|
|
||||||
deleteInvisibleElements: true,
|
|
||||||
});
|
|
||||||
const activeTool = scene.appState.activeTool;
|
|
||||||
scene.appState = {
|
scene.appState = {
|
||||||
...scene.appState,
|
...scene.appState,
|
||||||
theme: this.props.theme || scene.appState.theme,
|
theme: this.props.theme || scene.appState.theme,
|
||||||
@@ -2367,13 +2352,8 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
// with a library install link, which should auto-open the library)
|
// with a library install link, which should auto-open the library)
|
||||||
openSidebar: scene.appState?.openSidebar || this.state.openSidebar,
|
openSidebar: scene.appState?.openSidebar || this.state.openSidebar,
|
||||||
activeTool:
|
activeTool:
|
||||||
activeTool.type === "image" ||
|
scene.appState.activeTool.type === "image"
|
||||||
activeTool.type === "lasso" ||
|
? { ...scene.appState.activeTool, type: "selection" }
|
||||||
activeTool.type === "selection"
|
|
||||||
? {
|
|
||||||
...activeTool,
|
|
||||||
type: this.defaultSelectionTool,
|
|
||||||
}
|
|
||||||
: scene.appState.activeTool,
|
: scene.appState.activeTool,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
toast: this.state.toast,
|
toast: this.state.toast,
|
||||||
@@ -2412,16 +2392,6 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private isMobileOrTablet = (): boolean => {
|
|
||||||
const hasTouch = "ontouchstart" in window || navigator.maxTouchPoints > 0;
|
|
||||||
const hasCoarsePointer =
|
|
||||||
"matchMedia" in window &&
|
|
||||||
window?.matchMedia("(pointer: coarse)")?.matches;
|
|
||||||
const isTouchMobile = hasTouch && hasCoarsePointer;
|
|
||||||
|
|
||||||
return isMobile || isTouchMobile;
|
|
||||||
};
|
|
||||||
|
|
||||||
private isMobileBreakpoint = (width: number, height: number) => {
|
private isMobileBreakpoint = (width: number, height: number) => {
|
||||||
return (
|
return (
|
||||||
width < MQ_MAX_WIDTH_PORTRAIT ||
|
width < MQ_MAX_WIDTH_PORTRAIT ||
|
||||||
@@ -3140,7 +3110,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.addElementsFromPasteOrLibrary({
|
this.addElementsFromPasteOrLibrary({
|
||||||
elements,
|
elements,
|
||||||
files: data.files || null,
|
files: data.files || null,
|
||||||
position: this.isMobileOrTablet() ? "center" : "cursor",
|
position: "cursor",
|
||||||
retainSeed: isPlainPaste,
|
retainSeed: isPlainPaste,
|
||||||
});
|
});
|
||||||
} else if (data.text) {
|
} else if (data.text) {
|
||||||
@@ -3158,7 +3128,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.addElementsFromPasteOrLibrary({
|
this.addElementsFromPasteOrLibrary({
|
||||||
elements,
|
elements,
|
||||||
files,
|
files,
|
||||||
position: this.isMobileOrTablet() ? "center" : "cursor",
|
position: "cursor",
|
||||||
});
|
});
|
||||||
|
|
||||||
return;
|
return;
|
||||||
@@ -3218,7 +3188,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
}
|
}
|
||||||
this.addTextFromPaste(data.text, isPlainPaste);
|
this.addTextFromPaste(data.text, isPlainPaste);
|
||||||
}
|
}
|
||||||
this.setActiveTool({ type: this.defaultSelectionTool }, true);
|
this.setActiveTool({ type: "selection" });
|
||||||
event?.preventDefault();
|
event?.preventDefault();
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -3230,9 +3200,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
retainSeed?: boolean;
|
retainSeed?: boolean;
|
||||||
fitToContent?: boolean;
|
fitToContent?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const elements = restoreElements(opts.elements, null, {
|
const elements = restoreElements(opts.elements, null, undefined);
|
||||||
deleteInvisibleElements: true,
|
|
||||||
});
|
|
||||||
const [minX, minY, maxX, maxY] = getCommonBounds(elements);
|
const [minX, minY, maxX, maxY] = getCommonBounds(elements);
|
||||||
|
|
||||||
const elementsCenterX = distance(minX, maxX) / 2;
|
const elementsCenterX = distance(minX, maxX) / 2;
|
||||||
@@ -3364,7 +3332,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
this.setActiveTool({ type: this.defaultSelectionTool }, true);
|
this.setActiveTool({ type: "selection" });
|
||||||
|
|
||||||
if (opts.fitToContent) {
|
if (opts.fitToContent) {
|
||||||
this.scrollToContent(duplicatedElements, {
|
this.scrollToContent(duplicatedElements, {
|
||||||
@@ -3610,7 +3578,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
...updateActiveTool(
|
...updateActiveTool(
|
||||||
this.state,
|
this.state,
|
||||||
prevState.activeTool.locked
|
prevState.activeTool.locked
|
||||||
? { type: this.defaultSelectionTool }
|
? { type: "selection" }
|
||||||
: prevState.activeTool,
|
: prevState.activeTool,
|
||||||
),
|
),
|
||||||
locked: !prevState.activeTool.locked,
|
locked: !prevState.activeTool.locked,
|
||||||
@@ -3965,27 +3933,6 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
public applyDeltas = (
|
|
||||||
deltas: StoreDelta[],
|
|
||||||
options?: ApplyToOptions,
|
|
||||||
): [SceneElementsMap, AppState, boolean] => {
|
|
||||||
// squash all deltas together, starting with a fresh new delta instance
|
|
||||||
const aggregatedDelta = StoreDelta.squash(...deltas);
|
|
||||||
|
|
||||||
// create new instance of elements map & appState, so we don't accidentaly mutate existing ones
|
|
||||||
const nextAppState = { ...this.state };
|
|
||||||
const nextElements = new Map(
|
|
||||||
this.scene.getElementsMapIncludingDeleted(),
|
|
||||||
) as SceneElementsMap;
|
|
||||||
|
|
||||||
return StoreDelta.applyTo(
|
|
||||||
aggregatedDelta,
|
|
||||||
nextElements,
|
|
||||||
nextAppState,
|
|
||||||
options,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
public mutateElement = <TElement extends Mutable<ExcalidrawElement>>(
|
public mutateElement = <TElement extends Mutable<ExcalidrawElement>>(
|
||||||
element: TElement,
|
element: TElement,
|
||||||
updates: ElementUpdate<TElement>,
|
updates: ElementUpdate<TElement>,
|
||||||
@@ -4523,7 +4470,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
!this.state.selectionElement &&
|
!this.state.selectionElement &&
|
||||||
!this.state.selectedElementsAreBeingDragged
|
!this.state.selectedElementsAreBeingDragged
|
||||||
) {
|
) {
|
||||||
const shape = findShapeByKey(event.key, this);
|
const shape = findShapeByKey(event.key);
|
||||||
if (shape) {
|
if (shape) {
|
||||||
if (this.state.activeTool.type !== shape) {
|
if (this.state.activeTool.type !== shape) {
|
||||||
trackEvent(
|
trackEvent(
|
||||||
@@ -4616,7 +4563,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
|
|
||||||
if (event.key === KEYS.K && !event.altKey && !event[KEYS.CTRL_OR_CMD]) {
|
if (event.key === KEYS.K && !event.altKey && !event[KEYS.CTRL_OR_CMD]) {
|
||||||
if (this.state.activeTool.type === "laser") {
|
if (this.state.activeTool.type === "laser") {
|
||||||
this.setActiveTool({ type: this.defaultSelectionTool });
|
this.setActiveTool({ type: "selection" });
|
||||||
} else {
|
} else {
|
||||||
this.setActiveTool({ type: "laser" });
|
this.setActiveTool({ type: "laser" });
|
||||||
}
|
}
|
||||||
@@ -4980,8 +4927,17 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
}),
|
}),
|
||||||
onSubmit: withBatchedUpdates(({ viaKeyboard, nextOriginalText }) => {
|
onSubmit: withBatchedUpdates(({ viaKeyboard, nextOriginalText }) => {
|
||||||
const isDeleted = !nextOriginalText.trim();
|
const isDeleted = !nextOriginalText.trim();
|
||||||
updateElement(nextOriginalText, isDeleted);
|
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
// select the created text element only if submitting via keyboard
|
// select the created text element only if submitting via keyboard
|
||||||
// (when submitting via click it should act as signal to deselect)
|
// (when submitting via click it should act as signal to deselect)
|
||||||
if (!isDeleted && viaKeyboard) {
|
if (!isDeleted && viaKeyboard) {
|
||||||
@@ -5005,16 +4961,15 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isDeleted) {
|
if (isDeleted) {
|
||||||
fixBindingsAfterDeletion(this.scene.getNonDeletedElements(), [
|
fixBindingsAfterDeletion(this.scene.getNonDeletedElements(), [
|
||||||
element,
|
element,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isDeleted || isExistingElement) {
|
// we need to record either way, whether the text element was added or removed
|
||||||
this.store.scheduleCapture();
|
// since we need to sync this delta to other clients, otherwise it would end up with inconsistencies
|
||||||
}
|
this.store.scheduleCapture();
|
||||||
|
|
||||||
flushSync(() => {
|
flushSync(() => {
|
||||||
this.setState({
|
this.setState({
|
||||||
@@ -5461,7 +5416,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// we should only be able to double click when mode is selection
|
// we should only be able to double click when mode is selection
|
||||||
if (this.state.activeTool.type !== this.defaultSelectionTool) {
|
if (this.state.activeTool.type !== "selection") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5753,9 +5708,8 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
const elementsMap = this.scene.getNonDeletedElementsMap();
|
const elementsMap = this.scene.getNonDeletedElementsMap();
|
||||||
const frames = this.scene
|
const frames = this.scene
|
||||||
.getNonDeletedFramesLikes()
|
.getNonDeletedFramesLikes()
|
||||||
.filter(
|
.filter((frame): frame is ExcalidrawFrameLikeElement =>
|
||||||
(frame): frame is ExcalidrawFrameLikeElement =>
|
isCursorInFrame(sceneCoords, frame, elementsMap),
|
||||||
!frame.locked && isCursorInFrame(sceneCoords, frame, elementsMap),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return frames.length ? frames[frames.length - 1] : null;
|
return frames.length ? frames[frames.length - 1] : null;
|
||||||
@@ -6073,7 +6027,6 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
if (
|
if (
|
||||||
hasDeselectedButton ||
|
hasDeselectedButton ||
|
||||||
(this.state.activeTool.type !== "selection" &&
|
(this.state.activeTool.type !== "selection" &&
|
||||||
this.state.activeTool.type !== "lasso" &&
|
|
||||||
this.state.activeTool.type !== "text" &&
|
this.state.activeTool.type !== "text" &&
|
||||||
this.state.activeTool.type !== "eraser")
|
this.state.activeTool.type !== "eraser")
|
||||||
) {
|
) {
|
||||||
@@ -6236,12 +6189,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
!isElbowArrow(hitElement) ||
|
!isElbowArrow(hitElement) ||
|
||||||
!(hitElement.startBinding || hitElement.endBinding)
|
!(hitElement.startBinding || hitElement.endBinding)
|
||||||
) {
|
) {
|
||||||
if (
|
setCursor(this.interactiveCanvas, CURSOR_TYPE.MOVE);
|
||||||
this.state.activeTool.type !== "lasso" ||
|
|
||||||
selectedElements.length > 0
|
|
||||||
) {
|
|
||||||
setCursor(this.interactiveCanvas, CURSOR_TYPE.MOVE);
|
|
||||||
}
|
|
||||||
if (this.state.activeEmbeddable?.state === "hover") {
|
if (this.state.activeEmbeddable?.state === "hover") {
|
||||||
this.setState({ activeEmbeddable: null });
|
this.setState({ activeEmbeddable: null });
|
||||||
}
|
}
|
||||||
@@ -6358,12 +6306,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
!isElbowArrow(element) ||
|
!isElbowArrow(element) ||
|
||||||
!(element.startBinding || element.endBinding)
|
!(element.startBinding || element.endBinding)
|
||||||
) {
|
) {
|
||||||
if (
|
setCursor(this.interactiveCanvas, CURSOR_TYPE.MOVE);
|
||||||
this.state.activeTool.type !== "lasso" ||
|
|
||||||
Object.keys(this.state.selectedElementIds).length > 0
|
|
||||||
) {
|
|
||||||
setCursor(this.interactiveCanvas, CURSOR_TYPE.MOVE);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (this.hitElement(scenePointerX, scenePointerY, element)) {
|
} else if (this.hitElement(scenePointerX, scenePointerY, element)) {
|
||||||
@@ -6372,12 +6315,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
!isElbowArrow(element) ||
|
!isElbowArrow(element) ||
|
||||||
!(element.startBinding || element.endBinding)
|
!(element.startBinding || element.endBinding)
|
||||||
) {
|
) {
|
||||||
if (
|
setCursor(this.interactiveCanvas, CURSOR_TYPE.MOVE);
|
||||||
this.state.activeTool.type !== "lasso" ||
|
|
||||||
Object.keys(this.state.selectedElementIds).length > 0
|
|
||||||
) {
|
|
||||||
setCursor(this.interactiveCanvas, CURSOR_TYPE.MOVE);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -6639,119 +6577,11 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this.state.activeTool.type === "lasso") {
|
if (this.state.activeTool.type === "lasso") {
|
||||||
const hitSelectedElement =
|
this.lassoTrail.startPath(
|
||||||
pointerDownState.hit.element &&
|
pointerDownState.origin.x,
|
||||||
this.isASelectedElement(pointerDownState.hit.element);
|
pointerDownState.origin.y,
|
||||||
|
event.shiftKey,
|
||||||
const isMobileOrTablet = this.isMobileOrTablet();
|
);
|
||||||
|
|
||||||
if (
|
|
||||||
!pointerDownState.hit.hasHitCommonBoundingBoxOfSelectedElements &&
|
|
||||||
!pointerDownState.resize.handleType &&
|
|
||||||
!hitSelectedElement
|
|
||||||
) {
|
|
||||||
this.lassoTrail.startPath(
|
|
||||||
pointerDownState.origin.x,
|
|
||||||
pointerDownState.origin.y,
|
|
||||||
event.shiftKey,
|
|
||||||
);
|
|
||||||
|
|
||||||
// block dragging after lasso selection on PCs until the next pointer down
|
|
||||||
// (on mobile or tablet, we want to allow user to drag immediately)
|
|
||||||
pointerDownState.drag.blockDragging = !isMobileOrTablet;
|
|
||||||
}
|
|
||||||
|
|
||||||
// only for mobile or tablet, if we hit an element, select it immediately like normal selection
|
|
||||||
if (
|
|
||||||
isMobileOrTablet &&
|
|
||||||
pointerDownState.hit.element &&
|
|
||||||
!hitSelectedElement
|
|
||||||
) {
|
|
||||||
this.setState((prevState) => {
|
|
||||||
const nextSelectedElementIds: { [id: string]: true } = {
|
|
||||||
...prevState.selectedElementIds,
|
|
||||||
[pointerDownState.hit.element!.id]: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
const previouslySelectedElements: ExcalidrawElement[] = [];
|
|
||||||
|
|
||||||
Object.keys(prevState.selectedElementIds).forEach((id) => {
|
|
||||||
const element = this.scene.getElement(id);
|
|
||||||
element && previouslySelectedElements.push(element);
|
|
||||||
});
|
|
||||||
|
|
||||||
const hitElement = pointerDownState.hit.element!;
|
|
||||||
|
|
||||||
// if hitElement is frame-like, deselect all of its elements
|
|
||||||
// if they are selected
|
|
||||||
if (isFrameLikeElement(hitElement)) {
|
|
||||||
getFrameChildren(previouslySelectedElements, hitElement.id).forEach(
|
|
||||||
(element) => {
|
|
||||||
delete nextSelectedElementIds[element.id];
|
|
||||||
},
|
|
||||||
);
|
|
||||||
} else if (hitElement.frameId) {
|
|
||||||
// if hitElement is in a frame and its frame has been selected
|
|
||||||
// disable selection for the given element
|
|
||||||
if (nextSelectedElementIds[hitElement.frameId]) {
|
|
||||||
delete nextSelectedElementIds[hitElement.id];
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// hitElement is neither a frame nor an element in a frame
|
|
||||||
// but since hitElement could be in a group with some frames
|
|
||||||
// this means selecting hitElement will have the frames selected as well
|
|
||||||
// because we want to keep the invariant:
|
|
||||||
// - frames and their elements are not selected at the same time
|
|
||||||
// we deselect elements in those frames that were previously selected
|
|
||||||
|
|
||||||
const groupIds = hitElement.groupIds;
|
|
||||||
const framesInGroups = new Set(
|
|
||||||
groupIds
|
|
||||||
.flatMap((gid) =>
|
|
||||||
getElementsInGroup(this.scene.getNonDeletedElements(), gid),
|
|
||||||
)
|
|
||||||
.filter((element) => isFrameLikeElement(element))
|
|
||||||
.map((frame) => frame.id),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (framesInGroups.size > 0) {
|
|
||||||
previouslySelectedElements.forEach((element) => {
|
|
||||||
if (element.frameId && framesInGroups.has(element.frameId)) {
|
|
||||||
// deselect element and groups containing the element
|
|
||||||
delete nextSelectedElementIds[element.id];
|
|
||||||
element.groupIds
|
|
||||||
.flatMap((gid) =>
|
|
||||||
getElementsInGroup(
|
|
||||||
this.scene.getNonDeletedElements(),
|
|
||||||
gid,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.forEach((element) => {
|
|
||||||
delete nextSelectedElementIds[element.id];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
...selectGroupsForSelectedElements(
|
|
||||||
{
|
|
||||||
editingGroupId: prevState.editingGroupId,
|
|
||||||
selectedElementIds: nextSelectedElementIds,
|
|
||||||
},
|
|
||||||
this.scene.getNonDeletedElements(),
|
|
||||||
prevState,
|
|
||||||
this,
|
|
||||||
),
|
|
||||||
showHyperlinkPopup:
|
|
||||||
hitElement.link || isEmbeddableElement(hitElement)
|
|
||||||
? "info"
|
|
||||||
: false,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
pointerDownState.hit.wasAddedToSelection = true;
|
|
||||||
}
|
|
||||||
} else if (this.state.activeTool.type === "text") {
|
} else if (this.state.activeTool.type === "text") {
|
||||||
this.handleTextOnPointerDown(event, pointerDownState);
|
this.handleTextOnPointerDown(event, pointerDownState);
|
||||||
} else if (
|
} else if (
|
||||||
@@ -7131,7 +6961,6 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
hasOccurred: false,
|
hasOccurred: false,
|
||||||
offset: null,
|
offset: null,
|
||||||
origin: { ...origin },
|
origin: { ...origin },
|
||||||
blockDragging: false,
|
|
||||||
},
|
},
|
||||||
eventListeners: {
|
eventListeners: {
|
||||||
onMove: null,
|
onMove: null,
|
||||||
@@ -7207,10 +7036,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
event: React.PointerEvent<HTMLElement>,
|
event: React.PointerEvent<HTMLElement>,
|
||||||
pointerDownState: PointerDownState,
|
pointerDownState: PointerDownState,
|
||||||
): boolean => {
|
): boolean => {
|
||||||
if (
|
if (this.state.activeTool.type === "selection") {
|
||||||
this.state.activeTool.type === "selection" ||
|
|
||||||
this.state.activeTool.type === "lasso"
|
|
||||||
) {
|
|
||||||
const elements = this.scene.getNonDeletedElements();
|
const elements = this.scene.getNonDeletedElements();
|
||||||
const elementsMap = this.scene.getNonDeletedElementsMap();
|
const elementsMap = this.scene.getNonDeletedElementsMap();
|
||||||
const selectedElements = this.scene.getSelectedElements(this.state);
|
const selectedElements = this.scene.getSelectedElements(this.state);
|
||||||
@@ -7417,18 +7243,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
// on CMD/CTRL, drill down to hit element regardless of groups etc.
|
// on CMD/CTRL, drill down to hit element regardless of groups etc.
|
||||||
if (event[KEYS.CTRL_OR_CMD]) {
|
if (event[KEYS.CTRL_OR_CMD]) {
|
||||||
if (event.altKey) {
|
if (event.altKey) {
|
||||||
// ctrl + alt means we're lasso selecting - start lasso trail and switch to lasso tool
|
// ctrl + alt means we're lasso selecting
|
||||||
|
|
||||||
// Close any open dialogs that might interfere with lasso selection
|
|
||||||
if (this.state.openDialog?.name === "elementLinkSelector") {
|
|
||||||
this.setOpenDialog(null);
|
|
||||||
}
|
|
||||||
this.lassoTrail.startPath(
|
|
||||||
pointerDownState.origin.x,
|
|
||||||
pointerDownState.origin.y,
|
|
||||||
event.shiftKey,
|
|
||||||
);
|
|
||||||
this.setActiveTool({ type: "lasso", fromSelection: true });
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!this.state.selectedElementIds[hitElement.id]) {
|
if (!this.state.selectedElementIds[hitElement.id]) {
|
||||||
@@ -7649,9 +7464,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
resetCursor(this.interactiveCanvas);
|
resetCursor(this.interactiveCanvas);
|
||||||
if (!this.state.activeTool.locked) {
|
if (!this.state.activeTool.locked) {
|
||||||
this.setState({
|
this.setState({
|
||||||
activeTool: updateActiveTool(this.state, {
|
activeTool: updateActiveTool(this.state, { type: "selection" }),
|
||||||
type: this.defaultSelectionTool,
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -8435,18 +8248,15 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
event.shiftKey &&
|
event.shiftKey &&
|
||||||
this.state.selectedLinearElement.elementId ===
|
this.state.selectedLinearElement.elementId ===
|
||||||
pointerDownState.hit.element?.id;
|
pointerDownState.hit.element?.id;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
(hasHitASelectedElement ||
|
(hasHitASelectedElement ||
|
||||||
pointerDownState.hit.hasHitCommonBoundingBoxOfSelectedElements) &&
|
pointerDownState.hit.hasHitCommonBoundingBoxOfSelectedElements) &&
|
||||||
!isSelectingPointsInLineEditor &&
|
!isSelectingPointsInLineEditor &&
|
||||||
!pointerDownState.drag.blockDragging
|
this.state.activeTool.type !== "lasso"
|
||||||
) {
|
) {
|
||||||
const selectedElements = this.scene.getSelectedElements(this.state);
|
const selectedElements = this.scene.getSelectedElements(this.state);
|
||||||
if (
|
|
||||||
selectedElements.length > 0 &&
|
if (selectedElements.every((element) => element.locked)) {
|
||||||
selectedElements.every((element) => element.locked)
|
|
||||||
) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -8467,29 +8277,6 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
// if elements should be deselected on pointerup
|
// if elements should be deselected on pointerup
|
||||||
pointerDownState.drag.hasOccurred = true;
|
pointerDownState.drag.hasOccurred = true;
|
||||||
|
|
||||||
// prevent immediate dragging during lasso selection to avoid element displacement
|
|
||||||
// only allow dragging if we're not in the middle of lasso selection
|
|
||||||
// (on mobile, allow dragging if we hit an element)
|
|
||||||
if (
|
|
||||||
this.state.activeTool.type === "lasso" &&
|
|
||||||
this.lassoTrail.hasCurrentTrail &&
|
|
||||||
!(this.isMobileOrTablet() && pointerDownState.hit.element) &&
|
|
||||||
!this.state.activeTool.fromSelection
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear lasso trail when starting to drag selected elements with lasso tool
|
|
||||||
// Only clear if we're actually dragging (not during lasso selection)
|
|
||||||
if (
|
|
||||||
this.state.activeTool.type === "lasso" &&
|
|
||||||
selectedElements.length > 0 &&
|
|
||||||
pointerDownState.drag.hasOccurred &&
|
|
||||||
!this.state.activeTool.fromSelection
|
|
||||||
) {
|
|
||||||
this.lassoTrail.endPath();
|
|
||||||
}
|
|
||||||
|
|
||||||
// prevent dragging even if we're no longer holding cmd/ctrl otherwise
|
// prevent dragging even if we're no longer holding cmd/ctrl otherwise
|
||||||
// it would have weird results (stuff jumping all over the screen)
|
// it would have weird results (stuff jumping all over the screen)
|
||||||
// Checking for editingTextElement to avoid jump while editing on mobile #6503
|
// Checking for editingTextElement to avoid jump while editing on mobile #6503
|
||||||
@@ -9084,7 +8871,6 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
): (event: PointerEvent) => void {
|
): (event: PointerEvent) => void {
|
||||||
return withBatchedUpdates((childEvent: PointerEvent) => {
|
return withBatchedUpdates((childEvent: PointerEvent) => {
|
||||||
this.removePointer(childEvent);
|
this.removePointer(childEvent);
|
||||||
pointerDownState.drag.blockDragging = false;
|
|
||||||
if (pointerDownState.eventListeners.onMove) {
|
if (pointerDownState.eventListeners.onMove) {
|
||||||
pointerDownState.eventListeners.onMove.flush();
|
pointerDownState.eventListeners.onMove.flush();
|
||||||
}
|
}
|
||||||
@@ -9373,7 +9159,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.setState((prevState) => ({
|
this.setState((prevState) => ({
|
||||||
newElement: null,
|
newElement: null,
|
||||||
activeTool: updateActiveTool(this.state, {
|
activeTool: updateActiveTool(this.state, {
|
||||||
type: this.defaultSelectionTool,
|
type: "selection",
|
||||||
}),
|
}),
|
||||||
selectedElementIds: makeNextSelectedElementIds(
|
selectedElementIds: makeNextSelectedElementIds(
|
||||||
{
|
{
|
||||||
@@ -9989,9 +9775,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.setState({
|
this.setState({
|
||||||
newElement: null,
|
newElement: null,
|
||||||
suggestedBindings: [],
|
suggestedBindings: [],
|
||||||
activeTool: updateActiveTool(this.state, {
|
activeTool: updateActiveTool(this.state, { type: "selection" }),
|
||||||
type: this.defaultSelectionTool,
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.setState({
|
this.setState({
|
||||||
@@ -10285,9 +10069,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.setState(
|
this.setState(
|
||||||
{
|
{
|
||||||
newElement: null,
|
newElement: null,
|
||||||
activeTool: updateActiveTool(this.state, {
|
activeTool: updateActiveTool(this.state, { type: "selection" }),
|
||||||
type: this.defaultSelectionTool,
|
|
||||||
}),
|
|
||||||
},
|
},
|
||||||
() => {
|
() => {
|
||||||
this.actionManager.executeAction(actionFinalize);
|
this.actionManager.executeAction(actionFinalize);
|
||||||
@@ -10339,7 +10121,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
if (erroredFiles.size) {
|
if (erroredFiles.size) {
|
||||||
this.store.scheduleAction(CaptureUpdateAction.NEVER);
|
this.store.scheduleAction(CaptureUpdateAction.NEVER);
|
||||||
this.scene.replaceAllElements(
|
this.scene.replaceAllElements(
|
||||||
this.scene.getElementsIncludingDeleted().map((element) => {
|
elements.map((element) => {
|
||||||
if (
|
if (
|
||||||
isInitializedImageElement(element) &&
|
isInitializedImageElement(element) &&
|
||||||
erroredFiles.has(element.fileId)
|
erroredFiles.has(element.fileId)
|
||||||
@@ -10660,7 +10442,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
event.nativeEvent.pointerType === "pen" &&
|
event.nativeEvent.pointerType === "pen" &&
|
||||||
// always allow if user uses a pen secondary button
|
// always allow if user uses a pen secondary button
|
||||||
event.button !== POINTER_BUTTON.SECONDARY)) &&
|
event.button !== POINTER_BUTTON.SECONDARY)) &&
|
||||||
this.state.activeTool.type !== this.defaultSelectionTool
|
this.state.activeTool.type !== "selection"
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,8 +13,6 @@ import {
|
|||||||
EraserIcon,
|
EraserIcon,
|
||||||
} from "./icons";
|
} from "./icons";
|
||||||
|
|
||||||
import type { AppClassProperties } from "../types";
|
|
||||||
|
|
||||||
export const SHAPES = [
|
export const SHAPES = [
|
||||||
{
|
{
|
||||||
icon: SelectionIcon,
|
icon: SelectionIcon,
|
||||||
@@ -88,23 +86,8 @@ export const SHAPES = [
|
|||||||
},
|
},
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export const getToolbarTools = (app: AppClassProperties) => {
|
export const findShapeByKey = (key: string) => {
|
||||||
return app.defaultSelectionTool === "lasso"
|
const shape = SHAPES.find((shape, index) => {
|
||||||
? ([
|
|
||||||
{
|
|
||||||
value: "lasso",
|
|
||||||
icon: SelectionIcon,
|
|
||||||
key: KEYS.V,
|
|
||||||
numericKey: KEYS["1"],
|
|
||||||
fillable: true,
|
|
||||||
},
|
|
||||||
...SHAPES.slice(1),
|
|
||||||
] as const)
|
|
||||||
: SHAPES;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const findShapeByKey = (key: string, app: AppClassProperties) => {
|
|
||||||
const shape = getToolbarTools(app).find((shape, index) => {
|
|
||||||
return (
|
return (
|
||||||
(shape.numericKey != null && key === shape.numericKey.toString()) ||
|
(shape.numericKey != null && key === shape.numericKey.toString()) ||
|
||||||
(shape.key &&
|
(shape.key &&
|
||||||
|
|||||||
@@ -170,11 +170,7 @@ export const loadSceneOrLibraryFromBlob = async (
|
|||||||
},
|
},
|
||||||
localAppState,
|
localAppState,
|
||||||
localElements,
|
localElements,
|
||||||
{
|
{ repairBindings: true, refreshDimensions: false },
|
||||||
repairBindings: true,
|
|
||||||
refreshDimensions: false,
|
|
||||||
deleteInvisibleElements: true,
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
} else if (isValidLibrary(data)) {
|
} else if (isValidLibrary(data)) {
|
||||||
|
|||||||
@@ -4,7 +4,13 @@ import {
|
|||||||
supported as nativeFileSystemSupported,
|
supported as nativeFileSystemSupported,
|
||||||
} from "browser-fs-access";
|
} from "browser-fs-access";
|
||||||
|
|
||||||
import { EVENT, MIME_TYPES, debounce } from "@excalidraw/common";
|
import {
|
||||||
|
EVENT,
|
||||||
|
MIME_TYPES,
|
||||||
|
debounce,
|
||||||
|
isIOS,
|
||||||
|
isAndroid,
|
||||||
|
} from "@excalidraw/common";
|
||||||
|
|
||||||
import { AbortError } from "../errors";
|
import { AbortError } from "../errors";
|
||||||
|
|
||||||
@@ -13,6 +19,8 @@ import type { FileSystemHandle } from "browser-fs-access";
|
|||||||
type FILE_EXTENSION = Exclude<keyof typeof MIME_TYPES, "binary">;
|
type FILE_EXTENSION = Exclude<keyof typeof MIME_TYPES, "binary">;
|
||||||
|
|
||||||
const INPUT_CHANGE_INTERVAL_MS = 500;
|
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: {
|
export const fileOpen = <M extends boolean | undefined = false>(opts: {
|
||||||
extensions?: FILE_EXTENSION[];
|
extensions?: FILE_EXTENSION[];
|
||||||
@@ -41,13 +49,22 @@ export const fileOpen = <M extends boolean | undefined = false>(opts: {
|
|||||||
mimeTypes,
|
mimeTypes,
|
||||||
multiple: opts.multiple ?? false,
|
multiple: opts.multiple ?? false,
|
||||||
legacySetup: (resolve, reject, input) => {
|
legacySetup: (resolve, reject, input) => {
|
||||||
const scheduleRejection = debounce(reject, INPUT_CHANGE_INTERVAL_MS);
|
const isMobile = isIOS || isAndroid;
|
||||||
|
const intervalMs = isMobile
|
||||||
|
? MOBILE_INPUT_CHANGE_INTERVAL_MS
|
||||||
|
: INPUT_CHANGE_INTERVAL_MS;
|
||||||
|
const scheduleRejection = debounce(reject, intervalMs);
|
||||||
|
|
||||||
const focusHandler = () => {
|
const focusHandler = () => {
|
||||||
checkForFile();
|
checkForFile();
|
||||||
document.addEventListener(EVENT.KEYUP, scheduleRejection);
|
// on mobile, be less aggressive with rejection
|
||||||
document.addEventListener(EVENT.POINTER_UP, scheduleRejection);
|
if (!isMobile) {
|
||||||
scheduleRejection();
|
document.addEventListener(EVENT.KEYUP, scheduleRejection);
|
||||||
|
document.addEventListener(EVENT.POINTER_UP, scheduleRejection);
|
||||||
|
scheduleRejection();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const checkForFile = () => {
|
const checkForFile = () => {
|
||||||
// this hack might not work when expecting multiple files
|
// this hack might not work when expecting multiple files
|
||||||
if (input.files?.length) {
|
if (input.files?.length) {
|
||||||
@@ -55,12 +72,15 @@ export const fileOpen = <M extends boolean | undefined = false>(opts: {
|
|||||||
resolve(ret as RetType);
|
resolve(ret as RetType);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
window.addEventListener(EVENT.FOCUS, focusHandler);
|
window.addEventListener(EVENT.FOCUS, focusHandler);
|
||||||
});
|
});
|
||||||
|
|
||||||
const interval = window.setInterval(() => {
|
const interval = window.setInterval(() => {
|
||||||
checkForFile();
|
checkForFile();
|
||||||
}, INPUT_CHANGE_INTERVAL_MS);
|
}, intervalMs);
|
||||||
|
|
||||||
return (rejectPromise) => {
|
return (rejectPromise) => {
|
||||||
clearInterval(interval);
|
clearInterval(interval);
|
||||||
scheduleRejection.cancel();
|
scheduleRejection.cancel();
|
||||||
@@ -69,7 +89,9 @@ export const fileOpen = <M extends boolean | undefined = false>(opts: {
|
|||||||
document.removeEventListener(EVENT.POINTER_UP, scheduleRejection);
|
document.removeEventListener(EVENT.POINTER_UP, scheduleRejection);
|
||||||
if (rejectPromise) {
|
if (rejectPromise) {
|
||||||
// so that something is shown in console if we need to debug this
|
// so that something is shown in console if we need to debug this
|
||||||
console.warn("Opening the file was canceled (legacy-fs).");
|
console.warn(
|
||||||
|
"Opening the file was canceled (legacy-fs). This may happen on mobile devices.",
|
||||||
|
);
|
||||||
rejectPromise(new AbortError());
|
rejectPromise(new AbortError());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export type ReconciledExcalidrawElement = OrderedExcalidrawElement &
|
|||||||
export type RemoteExcalidrawElement = OrderedExcalidrawElement &
|
export type RemoteExcalidrawElement = OrderedExcalidrawElement &
|
||||||
MakeBrand<"RemoteExcalidrawElement">;
|
MakeBrand<"RemoteExcalidrawElement">;
|
||||||
|
|
||||||
export const shouldDiscardRemoteElement = (
|
const shouldDiscardRemoteElement = (
|
||||||
localAppState: AppState,
|
localAppState: AppState,
|
||||||
local: OrderedExcalidrawElement | undefined,
|
local: OrderedExcalidrawElement | undefined,
|
||||||
remote: RemoteExcalidrawElement,
|
remote: RemoteExcalidrawElement,
|
||||||
@@ -30,7 +30,7 @@ export const shouldDiscardRemoteElement = (
|
|||||||
// local element is being edited
|
// local element is being edited
|
||||||
(local.id === localAppState.editingTextElement?.id ||
|
(local.id === localAppState.editingTextElement?.id ||
|
||||||
local.id === localAppState.resizingElement?.id ||
|
local.id === localAppState.resizingElement?.id ||
|
||||||
local.id === localAppState.newElement?.id ||
|
local.id === localAppState.newElement?.id || // TODO: Is this still valid? As newElement is selection element, which is never part of the elements array
|
||||||
// local element is newer
|
// local element is newer
|
||||||
local.version > remote.version ||
|
local.version > remote.version ||
|
||||||
// resolve conflicting edits deterministically by taking the one with
|
// resolve conflicting edits deterministically by taking the one with
|
||||||
|
|||||||
@@ -241,9 +241,8 @@ const restoreElementWithProperties = <
|
|||||||
return ret;
|
return ret;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const restoreElement = (
|
const restoreElement = (
|
||||||
element: Exclude<ExcalidrawElement, ExcalidrawSelectionElement>,
|
element: Exclude<ExcalidrawElement, ExcalidrawSelectionElement>,
|
||||||
opts?: { deleteInvisibleElements?: boolean },
|
|
||||||
): typeof element | null => {
|
): typeof element | null => {
|
||||||
element = { ...element };
|
element = { ...element };
|
||||||
|
|
||||||
@@ -291,8 +290,7 @@ export const restoreElement = (
|
|||||||
|
|
||||||
// if empty text, mark as deleted. We keep in array
|
// if empty text, mark as deleted. We keep in array
|
||||||
// for data integrity purposes (collab etc.)
|
// for data integrity purposes (collab etc.)
|
||||||
if (opts?.deleteInvisibleElements && !text && !element.isDeleted) {
|
if (!text && !element.isDeleted) {
|
||||||
// TODO: we should not do this since it breaks sync / versioning when we exchange / apply just deltas and restore the elements (deletion isn't recorded)
|
|
||||||
element = { ...element, originalText: text, isDeleted: true };
|
element = { ...element, originalText: text, isDeleted: true };
|
||||||
element = bumpVersion(element);
|
element = bumpVersion(element);
|
||||||
}
|
}
|
||||||
@@ -525,13 +523,7 @@ export const restoreElements = (
|
|||||||
elements: ImportedDataState["elements"],
|
elements: ImportedDataState["elements"],
|
||||||
/** NOTE doesn't serve for reconciliation */
|
/** NOTE doesn't serve for reconciliation */
|
||||||
localElements: readonly ExcalidrawElement[] | null | undefined,
|
localElements: readonly ExcalidrawElement[] | null | undefined,
|
||||||
opts?:
|
opts?: { refreshDimensions?: boolean; repairBindings?: boolean } | undefined,
|
||||||
| {
|
|
||||||
refreshDimensions?: boolean;
|
|
||||||
repairBindings?: boolean;
|
|
||||||
deleteInvisibleElements?: boolean;
|
|
||||||
}
|
|
||||||
| undefined,
|
|
||||||
): OrderedExcalidrawElement[] => {
|
): OrderedExcalidrawElement[] => {
|
||||||
// used to detect duplicate top-level element ids
|
// used to detect duplicate top-level element ids
|
||||||
const existingIds = new Set<string>();
|
const existingIds = new Set<string>();
|
||||||
@@ -540,38 +532,24 @@ export const restoreElements = (
|
|||||||
(elements || []).reduce((elements, element) => {
|
(elements || []).reduce((elements, element) => {
|
||||||
// filtering out selection, which is legacy, no longer kept in elements,
|
// filtering out selection, which is legacy, no longer kept in elements,
|
||||||
// and causing issues if retained
|
// and causing issues if retained
|
||||||
if (element.type === "selection") {
|
if (element.type !== "selection" && !isInvisiblySmallElement(element)) {
|
||||||
return elements;
|
let migratedElement: ExcalidrawElement | null = restoreElement(element);
|
||||||
|
if (migratedElement) {
|
||||||
|
const localElement = localElementsMap?.get(element.id);
|
||||||
|
if (localElement && localElement.version > migratedElement.version) {
|
||||||
|
migratedElement = bumpVersion(
|
||||||
|
migratedElement,
|
||||||
|
localElement.version,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (existingIds.has(migratedElement.id)) {
|
||||||
|
migratedElement = { ...migratedElement, id: randomId() };
|
||||||
|
}
|
||||||
|
existingIds.add(migratedElement.id);
|
||||||
|
|
||||||
|
elements.push(migratedElement);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let migratedElement: ExcalidrawElement | null = restoreElement(element, {
|
|
||||||
deleteInvisibleElements: opts?.deleteInvisibleElements,
|
|
||||||
});
|
|
||||||
if (migratedElement) {
|
|
||||||
const localElement = localElementsMap?.get(element.id);
|
|
||||||
|
|
||||||
const shouldMarkAsDeleted =
|
|
||||||
opts?.deleteInvisibleElements && isInvisiblySmallElement(element);
|
|
||||||
|
|
||||||
if (
|
|
||||||
shouldMarkAsDeleted ||
|
|
||||||
(localElement && localElement.version > migratedElement.version)
|
|
||||||
) {
|
|
||||||
migratedElement = bumpVersion(migratedElement, localElement?.version);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (shouldMarkAsDeleted) {
|
|
||||||
migratedElement = { ...migratedElement, isDeleted: true };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (existingIds.has(migratedElement.id)) {
|
|
||||||
migratedElement = { ...migratedElement, id: randomId() };
|
|
||||||
}
|
|
||||||
existingIds.add(migratedElement.id);
|
|
||||||
|
|
||||||
elements.push(migratedElement);
|
|
||||||
}
|
|
||||||
|
|
||||||
return elements;
|
return elements;
|
||||||
}, [] as ExcalidrawElement[]),
|
}, [] as ExcalidrawElement[]),
|
||||||
);
|
);
|
||||||
@@ -812,11 +790,7 @@ export const restore = (
|
|||||||
*/
|
*/
|
||||||
localAppState: Partial<AppState> | null | undefined,
|
localAppState: Partial<AppState> | null | undefined,
|
||||||
localElements: readonly ExcalidrawElement[] | null | undefined,
|
localElements: readonly ExcalidrawElement[] | null | undefined,
|
||||||
elementsConfig?: {
|
elementsConfig?: { refreshDimensions?: boolean; repairBindings?: boolean },
|
||||||
refreshDimensions?: boolean;
|
|
||||||
repairBindings?: boolean;
|
|
||||||
deleteInvisibleElements?: boolean;
|
|
||||||
},
|
|
||||||
): RestoredDataState => {
|
): RestoredDataState => {
|
||||||
return {
|
return {
|
||||||
elements: restoreElements(data?.elements, localElements, elementsConfig),
|
elements: restoreElements(data?.elements, localElements, elementsConfig),
|
||||||
|
|||||||
@@ -175,7 +175,7 @@ export class History {
|
|||||||
let nextAppState = appState;
|
let nextAppState = appState;
|
||||||
let containsVisibleChange = false;
|
let containsVisibleChange = false;
|
||||||
|
|
||||||
// iterate through the history entries in case they result in no visible changes
|
// iterate through the history entries in case ;they result in no visible changes
|
||||||
while (historyDelta) {
|
while (historyDelta) {
|
||||||
try {
|
try {
|
||||||
[nextElements, nextAppState, containsVisibleChange] =
|
[nextElements, nextAppState, containsVisibleChange] =
|
||||||
|
|||||||
@@ -229,7 +229,6 @@ export { defaultLang, useI18n, languages } from "./i18n";
|
|||||||
export {
|
export {
|
||||||
restore,
|
restore,
|
||||||
restoreAppState,
|
restoreAppState,
|
||||||
restoreElement,
|
|
||||||
restoreElements,
|
restoreElements,
|
||||||
restoreLibraryItems,
|
restoreLibraryItems,
|
||||||
} from "./data/restore";
|
} from "./data/restore";
|
||||||
|
|||||||
@@ -169,14 +169,8 @@ export const isSnappingEnabled = ({
|
|||||||
selectedElements: NonDeletedExcalidrawElement[];
|
selectedElements: NonDeletedExcalidrawElement[];
|
||||||
}) => {
|
}) => {
|
||||||
if (event) {
|
if (event) {
|
||||||
// Allow snapping for lasso tool when dragging selected elements
|
|
||||||
// but not during lasso selection phase
|
|
||||||
const isLassoDragging =
|
|
||||||
app.state.activeTool.type === "lasso" &&
|
|
||||||
app.state.selectedElementsAreBeingDragged;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
(app.state.activeTool.type !== "lasso" || isLassoDragging) &&
|
app.state.activeTool.type !== "lasso" &&
|
||||||
((app.state.objectsSnapModeEnabled && !event[KEYS.CTRL_OR_CMD]) ||
|
((app.state.objectsSnapModeEnabled && !event[KEYS.CTRL_OR_CMD]) ||
|
||||||
(!app.state.objectsSnapModeEnabled &&
|
(!app.state.objectsSnapModeEnabled &&
|
||||||
event[KEYS.CTRL_OR_CMD] &&
|
event[KEYS.CTRL_OR_CMD] &&
|
||||||
|
|||||||
@@ -3682,14 +3682,14 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e
|
|||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
"roundness": null,
|
"roundness": null,
|
||||||
"seed": 400692809,
|
"seed": 1116226695,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 4,
|
"version": 4,
|
||||||
"versionNonce": 81784553,
|
"versionNonce": 23633383,
|
||||||
"width": 20,
|
"width": 20,
|
||||||
"x": 20,
|
"x": 20,
|
||||||
"y": 30,
|
"y": 30,
|
||||||
@@ -3714,14 +3714,14 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e
|
|||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
"roundness": null,
|
"roundness": null,
|
||||||
"seed": 449462985,
|
"seed": 1278240551,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 3,
|
"version": 3,
|
||||||
"versionNonce": 1150084233,
|
"versionNonce": 401146281,
|
||||||
"width": 20,
|
"width": 20,
|
||||||
"x": -10,
|
"x": -10,
|
||||||
"y": 0,
|
"y": 0,
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -2216,7 +2216,16 @@ exports[`regression tests > alt-drag duplicates an element > [end of test] undo
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"updated": {},
|
"updated": {
|
||||||
|
"id0": {
|
||||||
|
"deleted": {
|
||||||
|
"version": 5,
|
||||||
|
},
|
||||||
|
"inserted": {
|
||||||
|
"version": 3,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"id": "id6",
|
"id": "id6",
|
||||||
},
|
},
|
||||||
@@ -6385,16 +6394,15 @@ exports[`regression tests > draw every type of shape > [end of test] undo stack
|
|||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id9": true,
|
"id9": true,
|
||||||
},
|
},
|
||||||
"selectedLinearElement": {
|
"selectedLinearElementId": "id9",
|
||||||
"elementId": "id9",
|
"selectedLinearElementIsEditing": false,
|
||||||
"isEditing": false,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id6": true,
|
"id6": true,
|
||||||
},
|
},
|
||||||
"selectedLinearElement": null,
|
"selectedLinearElementId": null,
|
||||||
|
"selectedLinearElementIsEditing": null,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -6462,19 +6470,13 @@ exports[`regression tests > draw every type of shape > [end of test] undo stack
|
|||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id12": true,
|
"id12": true,
|
||||||
},
|
},
|
||||||
"selectedLinearElement": {
|
"selectedLinearElementId": "id12",
|
||||||
"elementId": "id12",
|
|
||||||
"isEditing": false,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id9": true,
|
"id9": true,
|
||||||
},
|
},
|
||||||
"selectedLinearElement": {
|
"selectedLinearElementId": "id9",
|
||||||
"elementId": "id9",
|
|
||||||
"isEditing": false,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -6540,16 +6542,15 @@ exports[`regression tests > draw every type of shape > [end of test] undo stack
|
|||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id15": true,
|
"id15": true,
|
||||||
},
|
},
|
||||||
"selectedLinearElement": null,
|
"selectedLinearElementId": null,
|
||||||
|
"selectedLinearElementIsEditing": null,
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id12": true,
|
"id12": true,
|
||||||
},
|
},
|
||||||
"selectedLinearElement": {
|
"selectedLinearElementId": "id12",
|
||||||
"elementId": "id12",
|
"selectedLinearElementIsEditing": false,
|
||||||
"isEditing": false,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -6676,13 +6677,12 @@ exports[`regression tests > draw every type of shape > [end of test] undo stack
|
|||||||
"appState": AppStateDelta {
|
"appState": AppStateDelta {
|
||||||
"delta": Delta {
|
"delta": Delta {
|
||||||
"deleted": {
|
"deleted": {
|
||||||
"selectedLinearElement": {
|
"selectedLinearElementId": "id15",
|
||||||
"elementId": "id15",
|
"selectedLinearElementIsEditing": false,
|
||||||
"isEditing": false,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"selectedLinearElement": null,
|
"selectedLinearElementId": null,
|
||||||
|
"selectedLinearElementIsEditing": null,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -6700,16 +6700,15 @@ exports[`regression tests > draw every type of shape > [end of test] undo stack
|
|||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id22": true,
|
"id22": true,
|
||||||
},
|
},
|
||||||
"selectedLinearElement": null,
|
"selectedLinearElementId": null,
|
||||||
|
"selectedLinearElementIsEditing": null,
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id15": true,
|
"id15": true,
|
||||||
},
|
},
|
||||||
"selectedLinearElement": {
|
"selectedLinearElementId": "id15",
|
||||||
"elementId": "id15",
|
"selectedLinearElementIsEditing": false,
|
||||||
"isEditing": false,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -6834,13 +6833,12 @@ exports[`regression tests > draw every type of shape > [end of test] undo stack
|
|||||||
"appState": AppStateDelta {
|
"appState": AppStateDelta {
|
||||||
"delta": Delta {
|
"delta": Delta {
|
||||||
"deleted": {
|
"deleted": {
|
||||||
"selectedLinearElement": {
|
"selectedLinearElementId": "id22",
|
||||||
"elementId": "id22",
|
"selectedLinearElementIsEditing": false,
|
||||||
"isEditing": false,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"selectedLinearElement": null,
|
"selectedLinearElementId": null,
|
||||||
|
"selectedLinearElementIsEditing": null,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -6875,13 +6873,12 @@ exports[`regression tests > draw every type of shape > [end of test] undo stack
|
|||||||
"appState": AppStateDelta {
|
"appState": AppStateDelta {
|
||||||
"delta": Delta {
|
"delta": Delta {
|
||||||
"deleted": {
|
"deleted": {
|
||||||
"selectedLinearElement": null,
|
"selectedLinearElementId": null,
|
||||||
|
"selectedLinearElementIsEditing": null,
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"selectedLinearElement": {
|
"selectedLinearElementId": "id22",
|
||||||
"elementId": "id22",
|
"selectedLinearElementIsEditing": false,
|
||||||
"isEditing": false,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -8722,14 +8719,13 @@ exports[`regression tests > key 5 selects arrow tool > [end of test] undo stack
|
|||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id0": true,
|
"id0": true,
|
||||||
},
|
},
|
||||||
"selectedLinearElement": {
|
"selectedLinearElementId": "id0",
|
||||||
"elementId": "id0",
|
"selectedLinearElementIsEditing": false,
|
||||||
"isEditing": false,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"selectedElementIds": {},
|
"selectedElementIds": {},
|
||||||
"selectedLinearElement": null,
|
"selectedLinearElementId": null,
|
||||||
|
"selectedLinearElementIsEditing": null,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -8950,14 +8946,13 @@ exports[`regression tests > key 6 selects line tool > [end of test] undo stack 1
|
|||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id0": true,
|
"id0": true,
|
||||||
},
|
},
|
||||||
"selectedLinearElement": {
|
"selectedLinearElementId": "id0",
|
||||||
"elementId": "id0",
|
"selectedLinearElementIsEditing": false,
|
||||||
"isEditing": false,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"selectedElementIds": {},
|
"selectedElementIds": {},
|
||||||
"selectedLinearElement": null,
|
"selectedLinearElementId": null,
|
||||||
|
"selectedLinearElementIsEditing": null,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -9370,14 +9365,13 @@ exports[`regression tests > key a selects arrow tool > [end of test] undo stack
|
|||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id0": true,
|
"id0": true,
|
||||||
},
|
},
|
||||||
"selectedLinearElement": {
|
"selectedLinearElementId": "id0",
|
||||||
"elementId": "id0",
|
"selectedLinearElementIsEditing": false,
|
||||||
"isEditing": false,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"selectedElementIds": {},
|
"selectedElementIds": {},
|
||||||
"selectedLinearElement": null,
|
"selectedLinearElementId": null,
|
||||||
|
"selectedLinearElementIsEditing": null,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -9776,14 +9770,13 @@ exports[`regression tests > key l selects line tool > [end of test] undo stack 1
|
|||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id0": true,
|
"id0": true,
|
||||||
},
|
},
|
||||||
"selectedLinearElement": {
|
"selectedLinearElementId": "id0",
|
||||||
"elementId": "id0",
|
"selectedLinearElementIsEditing": false,
|
||||||
"isEditing": false,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"selectedElementIds": {},
|
"selectedElementIds": {},
|
||||||
"selectedLinearElement": null,
|
"selectedLinearElementId": null,
|
||||||
|
"selectedLinearElementIsEditing": null,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -10892,7 +10885,32 @@ exports[`regression tests > make a group and duplicate it > [end of test] undo s
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"updated": {},
|
"updated": {
|
||||||
|
"id0": {
|
||||||
|
"deleted": {
|
||||||
|
"version": 6,
|
||||||
|
},
|
||||||
|
"inserted": {
|
||||||
|
"version": 4,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"id3": {
|
||||||
|
"deleted": {
|
||||||
|
"version": 6,
|
||||||
|
},
|
||||||
|
"inserted": {
|
||||||
|
"version": 4,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"id6": {
|
||||||
|
"deleted": {
|
||||||
|
"version": 6,
|
||||||
|
},
|
||||||
|
"inserted": {
|
||||||
|
"version": 4,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"id": "id21",
|
"id": "id21",
|
||||||
},
|
},
|
||||||
@@ -14476,13 +14494,12 @@ exports[`regression tests > undo/redo drawing an element > [end of test] redo st
|
|||||||
"appState": AppStateDelta {
|
"appState": AppStateDelta {
|
||||||
"delta": Delta {
|
"delta": Delta {
|
||||||
"deleted": {
|
"deleted": {
|
||||||
"selectedLinearElement": null,
|
"selectedLinearElementId": null,
|
||||||
|
"selectedLinearElementIsEditing": null,
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"selectedLinearElement": {
|
"selectedLinearElementId": "id6",
|
||||||
"elementId": "id6",
|
"selectedLinearElementIsEditing": false,
|
||||||
"isEditing": false,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -60,11 +60,7 @@ describe("restoreElements", () => {
|
|||||||
const rectElement = API.createElement({ type: "rectangle" });
|
const rectElement = API.createElement({ type: "rectangle" });
|
||||||
mockSizeHelper.mockImplementation(() => true);
|
mockSizeHelper.mockImplementation(() => true);
|
||||||
|
|
||||||
expect(
|
expect(restore.restoreElements([rectElement], null).length).toBe(0);
|
||||||
restore.restoreElements([rectElement], null, {
|
|
||||||
deleteInvisibleElements: true,
|
|
||||||
}),
|
|
||||||
).toEqual([expect.objectContaining({ isDeleted: true })]);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should restore text element correctly passing value for each attribute", () => {
|
it("should restore text element correctly passing value for each attribute", () => {
|
||||||
@@ -89,23 +85,6 @@ describe("restoreElements", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should not delete empty text element when opts.deleteInvisibleElements is not defined", () => {
|
|
||||||
const textElement = API.createElement({
|
|
||||||
type: "text",
|
|
||||||
text: "",
|
|
||||||
isDeleted: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const restoredElements = restore.restoreElements([textElement], null);
|
|
||||||
|
|
||||||
expect(restoredElements).toEqual([
|
|
||||||
expect.objectContaining({
|
|
||||||
id: textElement.id,
|
|
||||||
isDeleted: false,
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should restore text element correctly with unknown font family, null text and undefined alignment", () => {
|
it("should restore text element correctly with unknown font family, null text and undefined alignment", () => {
|
||||||
const textElement: any = API.createElement({
|
const textElement: any = API.createElement({
|
||||||
type: "text",
|
type: "text",
|
||||||
@@ -118,9 +97,10 @@ describe("restoreElements", () => {
|
|||||||
textElement.font = "10 unknown";
|
textElement.font = "10 unknown";
|
||||||
|
|
||||||
expect(textElement.isDeleted).toBe(false);
|
expect(textElement.isDeleted).toBe(false);
|
||||||
const restoredText = restore.restoreElements([textElement], null, {
|
const restoredText = restore.restoreElements(
|
||||||
deleteInvisibleElements: true,
|
[textElement],
|
||||||
})[0] as ExcalidrawTextElement;
|
null,
|
||||||
|
)[0] as ExcalidrawTextElement;
|
||||||
expect(restoredText.isDeleted).toBe(true);
|
expect(restoredText.isDeleted).toBe(true);
|
||||||
expect(restoredText).toMatchSnapshot({
|
expect(restoredText).toMatchSnapshot({
|
||||||
seed: expect.any(Number),
|
seed: expect.any(Number),
|
||||||
@@ -197,16 +177,13 @@ describe("restoreElements", () => {
|
|||||||
y: 0,
|
y: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
const restoredElements = restore.restoreElements([arrowElement], null, {
|
const restoredElements = restore.restoreElements([arrowElement], null);
|
||||||
deleteInvisibleElements: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const restoredArrow = restoredElements[0] as
|
const restoredArrow = restoredElements[0] as
|
||||||
| ExcalidrawArrowElement
|
| ExcalidrawArrowElement
|
||||||
| undefined;
|
| undefined;
|
||||||
|
|
||||||
expect(restoredArrow).not.toBeUndefined();
|
expect(restoredArrow).toBeUndefined();
|
||||||
expect(restoredArrow?.isDeleted).toBe(true);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should keep 'imperceptibly' small freedraw/line elements", () => {
|
it("should keep 'imperceptibly' small freedraw/line elements", () => {
|
||||||
@@ -871,7 +848,6 @@ describe("repairing bindings", () => {
|
|||||||
let restoredElements = restore.restoreElements(
|
let restoredElements = restore.restoreElements(
|
||||||
[container, invisibleBoundElement, boundElement],
|
[container, invisibleBoundElement, boundElement],
|
||||||
null,
|
null,
|
||||||
{ deleteInvisibleElements: true },
|
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(restoredElements).toEqual([
|
expect(restoredElements).toEqual([
|
||||||
@@ -879,11 +855,6 @@ describe("repairing bindings", () => {
|
|||||||
id: container.id,
|
id: container.id,
|
||||||
boundElements: [obsoleteBinding, invisibleBinding, nonExistentBinding],
|
boundElements: [obsoleteBinding, invisibleBinding, nonExistentBinding],
|
||||||
}),
|
}),
|
||||||
expect.objectContaining({
|
|
||||||
id: invisibleBoundElement.id,
|
|
||||||
containerId: container.id,
|
|
||||||
isDeleted: true,
|
|
||||||
}),
|
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: boundElement.id,
|
id: boundElement.id,
|
||||||
containerId: container.id,
|
containerId: container.id,
|
||||||
@@ -893,7 +864,7 @@ describe("repairing bindings", () => {
|
|||||||
restoredElements = restore.restoreElements(
|
restoredElements = restore.restoreElements(
|
||||||
[container, invisibleBoundElement, boundElement],
|
[container, invisibleBoundElement, boundElement],
|
||||||
null,
|
null,
|
||||||
{ repairBindings: true, deleteInvisibleElements: true },
|
{ repairBindings: true },
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(restoredElements).toEqual([
|
expect(restoredElements).toEqual([
|
||||||
@@ -901,11 +872,6 @@ describe("repairing bindings", () => {
|
|||||||
id: container.id,
|
id: container.id,
|
||||||
boundElements: [],
|
boundElements: [],
|
||||||
}),
|
}),
|
||||||
expect.objectContaining({
|
|
||||||
id: invisibleBoundElement.id,
|
|
||||||
containerId: container.id,
|
|
||||||
isDeleted: true,
|
|
||||||
}),
|
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: boundElement.id,
|
id: boundElement.id,
|
||||||
containerId: container.id,
|
containerId: container.id,
|
||||||
|
|||||||
@@ -315,12 +315,7 @@ describe("Test dragCreate", () => {
|
|||||||
);
|
);
|
||||||
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`5`);
|
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`5`);
|
||||||
expect(h.state.selectionElement).toBeNull();
|
expect(h.state.selectionElement).toBeNull();
|
||||||
expect(h.elements).toEqual([
|
expect(h.elements.length).toEqual(0);
|
||||||
expect.objectContaining({
|
|
||||||
type: "arrow",
|
|
||||||
isDeleted: true,
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("line", async () => {
|
it("line", async () => {
|
||||||
@@ -349,12 +344,7 @@ describe("Test dragCreate", () => {
|
|||||||
);
|
);
|
||||||
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`5`);
|
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`5`);
|
||||||
expect(h.state.selectionElement).toBeNull();
|
expect(h.state.selectionElement).toBeNull();
|
||||||
expect(h.elements).toEqual([
|
expect(h.elements.length).toEqual(0);
|
||||||
expect.objectContaining({
|
|
||||||
type: "line",
|
|
||||||
isDeleted: true,
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3043,14 +3043,15 @@ describe("history", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
Keyboard.undo();
|
Keyboard.undo();
|
||||||
expect(API.getUndoStack().length).toBe(0);
|
expect(API.getUndoStack().length).toBe(3);
|
||||||
expect(API.getRedoStack().length).toBe(4);
|
expect(API.getRedoStack().length).toBe(1);
|
||||||
expect(h.state.selectedLinearElement).toBeNull();
|
expect(h.state.selectedLinearElement).not.toBeNull();
|
||||||
|
expect(h.state.selectedLinearElement?.isEditing).toBe(true);
|
||||||
|
|
||||||
Keyboard.redo();
|
Keyboard.redo();
|
||||||
expect(API.getUndoStack().length).toBe(4);
|
expect(API.getUndoStack().length).toBe(4);
|
||||||
expect(API.getRedoStack().length).toBe(0);
|
expect(API.getRedoStack().length).toBe(0);
|
||||||
expect(h.state.selectedLinearElement).toBeNull();
|
expect(h.state.selectedLinearElement).not.toBeNull();
|
||||||
expect(h.state.selectedLinearElement?.isEditing ?? false).toBe(false);
|
expect(h.state.selectedLinearElement?.isEditing ?? false).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -4055,7 +4056,7 @@ describe("history", () => {
|
|||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: container.id,
|
id: container.id,
|
||||||
boundElements: [{ id: remoteText.id, type: "text" }],
|
boundElements: [{ id: remoteText.id, type: "text" }],
|
||||||
isDeleted: true,
|
isDeleted: false, // isDeleted got remotely updated to false
|
||||||
}),
|
}),
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: text.id,
|
id: text.id,
|
||||||
@@ -4065,7 +4066,7 @@ describe("history", () => {
|
|||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: remoteText.id,
|
id: remoteText.id,
|
||||||
// unbound
|
// unbound
|
||||||
containerId: null,
|
containerId: container.id,
|
||||||
isDeleted: false,
|
isDeleted: false,
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -248,10 +248,8 @@ export type ObservedElementsAppState = {
|
|||||||
editingGroupId: AppState["editingGroupId"];
|
editingGroupId: AppState["editingGroupId"];
|
||||||
selectedElementIds: AppState["selectedElementIds"];
|
selectedElementIds: AppState["selectedElementIds"];
|
||||||
selectedGroupIds: AppState["selectedGroupIds"];
|
selectedGroupIds: AppState["selectedGroupIds"];
|
||||||
selectedLinearElement: {
|
selectedLinearElementId: LinearElementEditor["elementId"] | null;
|
||||||
elementId: LinearElementEditor["elementId"];
|
selectedLinearElementIsEditing: boolean | null;
|
||||||
isEditing: boolean;
|
|
||||||
} | null;
|
|
||||||
croppingElementId: AppState["croppingElementId"];
|
croppingElementId: AppState["croppingElementId"];
|
||||||
lockedMultiSelections: AppState["lockedMultiSelections"];
|
lockedMultiSelections: AppState["lockedMultiSelections"];
|
||||||
activeLockedId: AppState["activeLockedId"];
|
activeLockedId: AppState["activeLockedId"];
|
||||||
@@ -731,8 +729,6 @@ export type AppClassProperties = {
|
|||||||
|
|
||||||
onPointerUpEmitter: App["onPointerUpEmitter"];
|
onPointerUpEmitter: App["onPointerUpEmitter"];
|
||||||
updateEditorAtom: App["updateEditorAtom"];
|
updateEditorAtom: App["updateEditorAtom"];
|
||||||
|
|
||||||
defaultSelectionTool: "selection" | "lasso";
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PointerDownState = Readonly<{
|
export type PointerDownState = Readonly<{
|
||||||
@@ -782,10 +778,6 @@ export type PointerDownState = Readonly<{
|
|||||||
// by default same as PointerDownState.origin. On alt-duplication, reset
|
// by default same as PointerDownState.origin. On alt-duplication, reset
|
||||||
// to current pointer position at time of duplication.
|
// to current pointer position at time of duplication.
|
||||||
origin: { x: number; y: number };
|
origin: { x: number; y: number };
|
||||||
// Whether to block drag after lasso selection
|
|
||||||
// this is meant to be used to block dragging after lasso selection on PCs
|
|
||||||
// until the next pointer down
|
|
||||||
blockDragging: boolean;
|
|
||||||
};
|
};
|
||||||
// We need to have these in the state so that we can unsubscribe them
|
// We need to have these in the state so that we can unsubscribe them
|
||||||
eventListeners: {
|
eventListeners: {
|
||||||
@@ -807,7 +799,6 @@ export type UnsubscribeCallback = () => void;
|
|||||||
|
|
||||||
export interface ExcalidrawImperativeAPI {
|
export interface ExcalidrawImperativeAPI {
|
||||||
updateScene: InstanceType<typeof App>["updateScene"];
|
updateScene: InstanceType<typeof App>["updateScene"];
|
||||||
applyDeltas: InstanceType<typeof App>["applyDeltas"];
|
|
||||||
mutateElement: InstanceType<typeof App>["mutateElement"];
|
mutateElement: InstanceType<typeof App>["mutateElement"];
|
||||||
updateLibrary: InstanceType<typeof Library>["updateLibrary"];
|
updateLibrary: InstanceType<typeof Library>["updateLibrary"];
|
||||||
resetScene: InstanceType<typeof App>["resetScene"];
|
resetScene: InstanceType<typeof App>["resetScene"];
|
||||||
|
|||||||
@@ -704,7 +704,7 @@ describe("textWysiwyg", () => {
|
|||||||
rectangle.x + rectangle.width / 2,
|
rectangle.x + rectangle.width / 2,
|
||||||
rectangle.y + rectangle.height / 2,
|
rectangle.y + rectangle.height / 2,
|
||||||
);
|
);
|
||||||
expect(h.elements.length).toBe(3);
|
expect(h.elements.length).toBe(2);
|
||||||
|
|
||||||
text = h.elements[1] as ExcalidrawTextElementWithContainer;
|
text = h.elements[1] as ExcalidrawTextElementWithContainer;
|
||||||
expect(text.type).toBe("text");
|
expect(text.type).toBe("text");
|
||||||
@@ -1198,11 +1198,7 @@ describe("textWysiwyg", () => {
|
|||||||
updateTextEditor(editor, " ");
|
updateTextEditor(editor, " ");
|
||||||
Keyboard.exitTextEditor(editor);
|
Keyboard.exitTextEditor(editor);
|
||||||
expect(rectangle.boundElements).toStrictEqual([]);
|
expect(rectangle.boundElements).toStrictEqual([]);
|
||||||
expect(h.elements[1]).toEqual(
|
expect(h.elements[1]).toBeUndefined();
|
||||||
expect.objectContaining({
|
|
||||||
isDeleted: true,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should restore original container height and clear cache once text is unbind", async () => {
|
it("should restore original container height and clear cache once text is unbind", async () => {
|
||||||
|
|||||||
@@ -49,7 +49,6 @@ export const exportToCanvas = ({
|
|||||||
{ elements, appState },
|
{ elements, appState },
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
{ deleteInvisibleElements: true },
|
|
||||||
);
|
);
|
||||||
const { exportBackground, viewBackgroundColor } = restoredAppState;
|
const { exportBackground, viewBackgroundColor } = restoredAppState;
|
||||||
return _exportToCanvas(
|
return _exportToCanvas(
|
||||||
@@ -180,7 +179,6 @@ export const exportToSvg = async ({
|
|||||||
{ elements, appState },
|
{ elements, appState },
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
{ deleteInvisibleElements: true },
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const exportAppState = {
|
const exportAppState = {
|
||||||
|
|||||||
+2
-2
@@ -144,7 +144,7 @@ const askToCommit = (tag, nextVersion) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
rl.question(
|
rl.question(
|
||||||
"Would you like to commit these changes to git? (Y/n): ",
|
"Do you want to commit these changes to git? (Y/n): ",
|
||||||
(answer) => {
|
(answer) => {
|
||||||
rl.close();
|
rl.close();
|
||||||
|
|
||||||
@@ -189,7 +189,7 @@ const askToPublish = (tag, version) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
rl.question(
|
rl.question(
|
||||||
"Would you like to publish these changes to npm? (Y/n): ",
|
"Do you want to publish these changes to npm? (Y/n): ",
|
||||||
(answer) => {
|
(answer) => {
|
||||||
rl.close();
|
rl.close();
|
||||||
|
|
||||||
|
|||||||
@@ -40,15 +40,6 @@
|
|||||||
js-tokens "^4.0.0"
|
js-tokens "^4.0.0"
|
||||||
picocolors "^1.0.0"
|
picocolors "^1.0.0"
|
||||||
|
|
||||||
"@babel/code-frame@^7.27.1":
|
|
||||||
version "7.27.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.27.1.tgz#200f715e66d52a23b221a9435534a91cc13ad5be"
|
|
||||||
integrity sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==
|
|
||||||
dependencies:
|
|
||||||
"@babel/helper-validator-identifier" "^7.27.1"
|
|
||||||
js-tokens "^4.0.0"
|
|
||||||
picocolors "^1.1.1"
|
|
||||||
|
|
||||||
"@babel/compat-data@^7.22.6", "@babel/compat-data@^7.26.5", "@babel/compat-data@^7.26.8":
|
"@babel/compat-data@^7.22.6", "@babel/compat-data@^7.26.5", "@babel/compat-data@^7.26.8":
|
||||||
version "7.26.8"
|
version "7.26.8"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.26.8.tgz#821c1d35641c355284d4a870b8a4a7b0c141e367"
|
resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.26.8.tgz#821c1d35641c355284d4a870b8a4a7b0c141e367"
|
||||||
@@ -95,17 +86,6 @@
|
|||||||
"@jridgewell/trace-mapping" "^0.3.25"
|
"@jridgewell/trace-mapping" "^0.3.25"
|
||||||
jsesc "^3.0.2"
|
jsesc "^3.0.2"
|
||||||
|
|
||||||
"@babel/generator@^7.28.3":
|
|
||||||
version "7.28.3"
|
|
||||||
resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.28.3.tgz#9626c1741c650cbac39121694a0f2d7451b8ef3e"
|
|
||||||
integrity sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==
|
|
||||||
dependencies:
|
|
||||||
"@babel/parser" "^7.28.3"
|
|
||||||
"@babel/types" "^7.28.2"
|
|
||||||
"@jridgewell/gen-mapping" "^0.3.12"
|
|
||||||
"@jridgewell/trace-mapping" "^0.3.28"
|
|
||||||
jsesc "^3.0.2"
|
|
||||||
|
|
||||||
"@babel/helper-annotate-as-pure@^7.18.6", "@babel/helper-annotate-as-pure@^7.25.9":
|
"@babel/helper-annotate-as-pure@^7.18.6", "@babel/helper-annotate-as-pure@^7.25.9":
|
||||||
version "7.25.9"
|
version "7.25.9"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.9.tgz#d8eac4d2dc0d7b6e11fa6e535332e0d3184f06b4"
|
resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.9.tgz#d8eac4d2dc0d7b6e11fa6e535332e0d3184f06b4"
|
||||||
@@ -157,11 +137,6 @@
|
|||||||
lodash.debounce "^4.0.8"
|
lodash.debounce "^4.0.8"
|
||||||
resolve "^1.14.2"
|
resolve "^1.14.2"
|
||||||
|
|
||||||
"@babel/helper-globals@^7.28.0":
|
|
||||||
version "7.28.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/@babel/helper-globals/-/helper-globals-7.28.0.tgz#b9430df2aa4e17bc28665eadeae8aa1d985e6674"
|
|
||||||
integrity sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==
|
|
||||||
|
|
||||||
"@babel/helper-member-expression-to-functions@^7.25.9":
|
"@babel/helper-member-expression-to-functions@^7.25.9":
|
||||||
version "7.25.9"
|
version "7.25.9"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.25.9.tgz#9dfffe46f727005a5ea29051ac835fb735e4c1a3"
|
resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.25.9.tgz#9dfffe46f727005a5ea29051ac835fb735e4c1a3"
|
||||||
@@ -199,11 +174,6 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.26.5.tgz#18580d00c9934117ad719392c4f6585c9333cc35"
|
resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.26.5.tgz#18580d00c9934117ad719392c4f6585c9333cc35"
|
||||||
integrity sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg==
|
integrity sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg==
|
||||||
|
|
||||||
"@babel/helper-plugin-utils@^7.27.1":
|
|
||||||
version "7.27.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz#ddb2f876534ff8013e6c2b299bf4d39b3c51d44c"
|
|
||||||
integrity sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==
|
|
||||||
|
|
||||||
"@babel/helper-remap-async-to-generator@^7.25.9":
|
"@babel/helper-remap-async-to-generator@^7.25.9":
|
||||||
version "7.25.9"
|
version "7.25.9"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.25.9.tgz#e53956ab3d5b9fb88be04b3e2f31b523afd34b92"
|
resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.25.9.tgz#e53956ab3d5b9fb88be04b3e2f31b523afd34b92"
|
||||||
@@ -235,21 +205,11 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz#1aabb72ee72ed35789b4bbcad3ca2862ce614e8c"
|
resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz#1aabb72ee72ed35789b4bbcad3ca2862ce614e8c"
|
||||||
integrity sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==
|
integrity sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==
|
||||||
|
|
||||||
"@babel/helper-string-parser@^7.27.1":
|
|
||||||
version "7.27.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz#54da796097ab19ce67ed9f88b47bb2ec49367687"
|
|
||||||
integrity sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==
|
|
||||||
|
|
||||||
"@babel/helper-validator-identifier@^7.25.9":
|
"@babel/helper-validator-identifier@^7.25.9":
|
||||||
version "7.25.9"
|
version "7.25.9"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz#24b64e2c3ec7cd3b3c547729b8d16871f22cbdc7"
|
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz#24b64e2c3ec7cd3b3c547729b8d16871f22cbdc7"
|
||||||
integrity sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==
|
integrity sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==
|
||||||
|
|
||||||
"@babel/helper-validator-identifier@^7.27.1":
|
|
||||||
version "7.27.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz#a7054dcc145a967dd4dc8fee845a57c1316c9df8"
|
|
||||||
integrity sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==
|
|
||||||
|
|
||||||
"@babel/helper-validator-option@^7.25.9":
|
"@babel/helper-validator-option@^7.25.9":
|
||||||
version "7.25.9"
|
version "7.25.9"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz#86e45bd8a49ab7e03f276577f96179653d41da72"
|
resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz#86e45bd8a49ab7e03f276577f96179653d41da72"
|
||||||
@@ -289,13 +249,6 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@babel/types" "^7.26.9"
|
"@babel/types" "^7.26.9"
|
||||||
|
|
||||||
"@babel/parser@^7.27.2", "@babel/parser@^7.28.3":
|
|
||||||
version "7.28.3"
|
|
||||||
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.28.3.tgz#d2d25b814621bca5fe9d172bc93792547e7a2a71"
|
|
||||||
integrity sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA==
|
|
||||||
dependencies:
|
|
||||||
"@babel/types" "^7.28.2"
|
|
||||||
|
|
||||||
"@babel/plugin-bugfix-firefox-class-in-computed-class-key@^7.25.9":
|
"@babel/plugin-bugfix-firefox-class-in-computed-class-key@^7.25.9":
|
||||||
version "7.25.9"
|
version "7.25.9"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.25.9.tgz#cc2e53ebf0a0340777fff5ed521943e253b4d8fe"
|
resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.25.9.tgz#cc2e53ebf0a0340777fff5ed521943e253b4d8fe"
|
||||||
@@ -560,14 +513,6 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@babel/helper-plugin-utils" "^7.25.9"
|
"@babel/helper-plugin-utils" "^7.25.9"
|
||||||
|
|
||||||
"@babel/plugin-transform-destructuring@^7.28.0":
|
|
||||||
version "7.28.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.28.0.tgz#0f156588f69c596089b7d5b06f5af83d9aa7f97a"
|
|
||||||
integrity sha512-v1nrSMBiKcodhsyJ4Gf+Z0U/yawmJDBOTpEB3mcQY52r9RIyPneGyAS/yM6seP/8I+mWI3elOMtT5dB8GJVs+A==
|
|
||||||
dependencies:
|
|
||||||
"@babel/helper-plugin-utils" "^7.27.1"
|
|
||||||
"@babel/traverse" "^7.28.0"
|
|
||||||
|
|
||||||
"@babel/plugin-transform-dotall-regex@^7.25.9":
|
"@babel/plugin-transform-dotall-regex@^7.25.9":
|
||||||
version "7.25.9"
|
version "7.25.9"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.25.9.tgz#bad7945dd07734ca52fe3ad4e872b40ed09bb09a"
|
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.25.9.tgz#bad7945dd07734ca52fe3ad4e872b40ed09bb09a"
|
||||||
@@ -598,14 +543,6 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@babel/helper-plugin-utils" "^7.25.9"
|
"@babel/helper-plugin-utils" "^7.25.9"
|
||||||
|
|
||||||
"@babel/plugin-transform-explicit-resource-management@7.28.0":
|
|
||||||
version "7.28.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-explicit-resource-management/-/plugin-transform-explicit-resource-management-7.28.0.tgz#45be6211b778dbf4b9d54c4e8a2b42fa72e09a1a"
|
|
||||||
integrity sha512-K8nhUcn3f6iB+P3gwCv/no7OdzOZQcKchW6N389V6PD8NUWKZHzndOd9sPDVbMoBsbmjMqlB4L9fm+fEFNVlwQ==
|
|
||||||
dependencies:
|
|
||||||
"@babel/helper-plugin-utils" "^7.27.1"
|
|
||||||
"@babel/plugin-transform-destructuring" "^7.28.0"
|
|
||||||
|
|
||||||
"@babel/plugin-transform-exponentiation-operator@^7.26.3":
|
"@babel/plugin-transform-exponentiation-operator@^7.26.3":
|
||||||
version "7.26.3"
|
version "7.26.3"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.26.3.tgz#e29f01b6de302c7c2c794277a48f04a9ca7f03bc"
|
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.26.3.tgz#e29f01b6de302c7c2c794277a48f04a9ca7f03bc"
|
||||||
@@ -1082,15 +1019,6 @@
|
|||||||
"@babel/parser" "^7.26.9"
|
"@babel/parser" "^7.26.9"
|
||||||
"@babel/types" "^7.26.9"
|
"@babel/types" "^7.26.9"
|
||||||
|
|
||||||
"@babel/template@^7.27.2":
|
|
||||||
version "7.27.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.27.2.tgz#fa78ceed3c4e7b63ebf6cb39e5852fca45f6809d"
|
|
||||||
integrity sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==
|
|
||||||
dependencies:
|
|
||||||
"@babel/code-frame" "^7.27.1"
|
|
||||||
"@babel/parser" "^7.27.2"
|
|
||||||
"@babel/types" "^7.27.1"
|
|
||||||
|
|
||||||
"@babel/traverse@^7.25.9", "@babel/traverse@^7.26.5", "@babel/traverse@^7.26.8", "@babel/traverse@^7.26.9":
|
"@babel/traverse@^7.25.9", "@babel/traverse@^7.26.5", "@babel/traverse@^7.26.8", "@babel/traverse@^7.26.9":
|
||||||
version "7.26.9"
|
version "7.26.9"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.26.9.tgz#4398f2394ba66d05d988b2ad13c219a2c857461a"
|
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.26.9.tgz#4398f2394ba66d05d988b2ad13c219a2c857461a"
|
||||||
@@ -1104,19 +1032,6 @@
|
|||||||
debug "^4.3.1"
|
debug "^4.3.1"
|
||||||
globals "^11.1.0"
|
globals "^11.1.0"
|
||||||
|
|
||||||
"@babel/traverse@^7.28.0":
|
|
||||||
version "7.28.3"
|
|
||||||
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.28.3.tgz#6911a10795d2cce43ec6a28cffc440cca2593434"
|
|
||||||
integrity sha512-7w4kZYHneL3A6NP2nxzHvT3HCZ7puDZZjFMqDpBPECub79sTtSO5CGXDkKrTQq8ksAwfD/XI2MRFX23njdDaIQ==
|
|
||||||
dependencies:
|
|
||||||
"@babel/code-frame" "^7.27.1"
|
|
||||||
"@babel/generator" "^7.28.3"
|
|
||||||
"@babel/helper-globals" "^7.28.0"
|
|
||||||
"@babel/parser" "^7.28.3"
|
|
||||||
"@babel/template" "^7.27.2"
|
|
||||||
"@babel/types" "^7.28.2"
|
|
||||||
debug "^4.3.1"
|
|
||||||
|
|
||||||
"@babel/types@^7.21.3", "@babel/types@^7.25.4", "@babel/types@^7.25.9", "@babel/types@^7.26.9", "@babel/types@^7.4.4":
|
"@babel/types@^7.21.3", "@babel/types@^7.25.4", "@babel/types@^7.25.9", "@babel/types@^7.26.9", "@babel/types@^7.4.4":
|
||||||
version "7.26.9"
|
version "7.26.9"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.26.9.tgz#08b43dec79ee8e682c2ac631c010bdcac54a21ce"
|
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.26.9.tgz#08b43dec79ee8e682c2ac631c010bdcac54a21ce"
|
||||||
@@ -1125,14 +1040,6 @@
|
|||||||
"@babel/helper-string-parser" "^7.25.9"
|
"@babel/helper-string-parser" "^7.25.9"
|
||||||
"@babel/helper-validator-identifier" "^7.25.9"
|
"@babel/helper-validator-identifier" "^7.25.9"
|
||||||
|
|
||||||
"@babel/types@^7.27.1", "@babel/types@^7.28.2":
|
|
||||||
version "7.28.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.28.2.tgz#da9db0856a9a88e0a13b019881d7513588cf712b"
|
|
||||||
integrity sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==
|
|
||||||
dependencies:
|
|
||||||
"@babel/helper-string-parser" "^7.27.1"
|
|
||||||
"@babel/helper-validator-identifier" "^7.27.1"
|
|
||||||
|
|
||||||
"@bcoe/v8-coverage@^1.0.2":
|
"@bcoe/v8-coverage@^1.0.2":
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz#bbe12dca5b4ef983a0d0af4b07b9bc90ea0ababa"
|
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz#bbe12dca5b4ef983a0d0af4b07b9bc90ea0ababa"
|
||||||
@@ -2043,14 +1950,6 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@sinclair/typebox" "^0.27.8"
|
"@sinclair/typebox" "^0.27.8"
|
||||||
|
|
||||||
"@jridgewell/gen-mapping@^0.3.12":
|
|
||||||
version "0.3.13"
|
|
||||||
resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz#6342a19f44347518c93e43b1ac69deb3c4656a1f"
|
|
||||||
integrity sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==
|
|
||||||
dependencies:
|
|
||||||
"@jridgewell/sourcemap-codec" "^1.5.0"
|
|
||||||
"@jridgewell/trace-mapping" "^0.3.24"
|
|
||||||
|
|
||||||
"@jridgewell/gen-mapping@^0.3.5":
|
"@jridgewell/gen-mapping@^0.3.5":
|
||||||
version "0.3.8"
|
version "0.3.8"
|
||||||
resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz#4f0e06362e01362f823d348f1872b08f666d8142"
|
resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz#4f0e06362e01362f823d348f1872b08f666d8142"
|
||||||
@@ -2091,14 +1990,6 @@
|
|||||||
"@jridgewell/resolve-uri" "^3.1.0"
|
"@jridgewell/resolve-uri" "^3.1.0"
|
||||||
"@jridgewell/sourcemap-codec" "^1.4.14"
|
"@jridgewell/sourcemap-codec" "^1.4.14"
|
||||||
|
|
||||||
"@jridgewell/trace-mapping@^0.3.28":
|
|
||||||
version "0.3.30"
|
|
||||||
resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz#4a76c4daeee5df09f5d3940e087442fb36ce2b99"
|
|
||||||
integrity sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==
|
|
||||||
dependencies:
|
|
||||||
"@jridgewell/resolve-uri" "^3.1.0"
|
|
||||||
"@jridgewell/sourcemap-codec" "^1.4.14"
|
|
||||||
|
|
||||||
"@next/env@14.1.4":
|
"@next/env@14.1.4":
|
||||||
version "14.1.4"
|
version "14.1.4"
|
||||||
resolved "https://registry.yarnpkg.com/@next/env/-/env-14.1.4.tgz#432e80651733fbd67230bf262aee28be65252674"
|
resolved "https://registry.yarnpkg.com/@next/env/-/env-14.1.4.tgz#432e80651733fbd67230bf262aee28be65252674"
|
||||||
@@ -5374,9 +5265,6 @@ eslint-module-utils@^2.12.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
debug "^3.2.7"
|
debug "^3.2.7"
|
||||||
|
|
||||||
"eslint-plugin-eslint@file:packages/eslint":
|
|
||||||
version "0.1.0"
|
|
||||||
|
|
||||||
eslint-plugin-flowtype@^8.0.3:
|
eslint-plugin-flowtype@^8.0.3:
|
||||||
version "8.0.3"
|
version "8.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/eslint-plugin-flowtype/-/eslint-plugin-flowtype-8.0.3.tgz#e1557e37118f24734aa3122e7536a038d34a4912"
|
resolved "https://registry.yarnpkg.com/eslint-plugin-flowtype/-/eslint-plugin-flowtype-8.0.3.tgz#e1557e37118f24734aa3122e7536a038d34a4912"
|
||||||
|
|||||||
Reference in New Issue
Block a user