feat: better unlock (#9546)

* change lock label

* feat: add unlock logic for single units on pointer up

* feat: add unlock popup

* fix: linting errors

* style: padding tweaks

* style: remove redundant line

* feat: lock multiple units together

* style: tweak color & position

* feat: add highlight for locked elements

* feat: select groups correctly after unlocking

* test: update snapshots

* fix: lasso from selecting locked elements

* fix: should prevent selecting unlocked elements and setting locked id at the same time

* fix: reset hit locked id immediately when appropriate

* feat: capture locked units in delta

* test: update locking test

* feat: show lock highlight when locking (including undo/redo)

* feat: make locked highlighting consistent

* feat: show correct cursor type when moving over locked elements

* fix history

* remove `lockedUnits.singleUnits`

* tweak button

* do not render UnlockPopup if not locked element selected

* tweak actions

* refactor: simplify type

* refactor: rename type

* refactor: simplify hit element setting & checking

* fix: prefer locked over link

* rename to `activeLockedId`

* refactor: getElementAtPosition takes an optional hitelments array

* fix: avoid setting active locked id after resizing

---------

Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com>
This commit is contained in:
Ryan Di
2025-05-21 21:57:12 +10:00
committed by GitHub
parent 41a7613dff
commit 712f267519
19 changed files with 843 additions and 109 deletions

View File

@@ -187,10 +187,12 @@ export class Delta<T> {
return;
}
if (
typeof deleted[property] === "object" ||
typeof inserted[property] === "object"
) {
const isDeletedObject =
deleted[property] !== null && typeof deleted[property] === "object";
const isInsertedObject =
inserted[property] !== null && typeof inserted[property] === "object";
if (isDeletedObject || isInsertedObject) {
type RecordLike = Record<string, V | undefined>;
const deletedObject: RecordLike = deleted[property] ?? {};
@@ -222,6 +224,9 @@ export class Delta<T> {
Reflect.deleteProperty(deleted, property);
Reflect.deleteProperty(inserted, property);
}
} else if (deleted[property] === inserted[property]) {
Reflect.deleteProperty(deleted, property);
Reflect.deleteProperty(inserted, property);
}
}
@@ -658,6 +663,24 @@ export class AppStateDelta implements DeltaContainer<AppState> {
}
break;
case "lockedMultiSelections": {
const prevLockedUnits = prevAppState[key] || {};
const nextLockedUnits = nextAppState[key] || {};
if (!isShallowEqual(prevLockedUnits, nextLockedUnits)) {
visibleDifferenceFlag.value = true;
}
break;
}
case "activeLockedId": {
const prevHitLockedId = prevAppState[key] || null;
const nextHitLockedId = nextAppState[key] || null;
if (prevHitLockedId !== nextHitLockedId) {
visibleDifferenceFlag.value = true;
}
break;
}
default: {
assertNever(
key,
@@ -753,6 +776,8 @@ export class AppStateDelta implements DeltaContainer<AppState> {
editingLinearElementId,
selectedLinearElementId,
croppingElementId,
lockedMultiSelections,
activeLockedId,
...standaloneProps
} = delta as ObservedAppState;
@@ -797,6 +822,18 @@ export class AppStateDelta implements DeltaContainer<AppState> {
"selectedGroupIds",
(prevValue) => (prevValue ?? false) as ValueOf<T["selectedGroupIds"]>,
);
Delta.diffObjects(
deleted,
inserted,
"lockedMultiSelections",
(prevValue) => (prevValue ?? {}) as ValueOf<T["lockedMultiSelections"]>,
);
Delta.diffObjects(
deleted,
inserted,
"activeLockedId",
(prevValue) => (prevValue ?? null) as ValueOf<T["activeLockedId"]>,
);
} catch (e) {
// 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.`);