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
co-authored by dwelle
parent 41a7613dff
commit 712f267519
19 changed files with 843 additions and 109 deletions
@@ -377,7 +377,9 @@ const renderElementsBoxHighlight = (
context: CanvasRenderingContext2D,
appState: InteractiveCanvasAppState,
elements: NonDeleted<ExcalidrawElement>[],
config?: { colors?: string[]; dashed?: boolean },
) => {
const { colors = ["rgb(0,118,255)"], dashed = false } = config || {};
const individualElements = elements.filter(
(element) => element.groupIds.length === 0,
);
@@ -394,8 +396,8 @@ const renderElementsBoxHighlight = (
x2,
y1,
y2,
selectionColors: ["rgb(0,118,255)"],
dashed: false,
selectionColors: colors,
dashed,
cx: x1 + (x2 - x1) / 2,
cy: y1 + (y2 - y1) / 2,
activeEmbeddable: false,
@@ -787,6 +789,17 @@ const _renderInteractiveScene = ({
renderElementsBoxHighlight(context, appState, appState.elementsToHighlight);
}
if (appState.activeLockedId) {
const element = allElementsMap.get(appState.activeLockedId);
const elements = element
? [element]
: getElementsInGroup(allElementsMap, appState.activeLockedId);
renderElementsBoxHighlight(context, appState, elements, {
colors: ["#ced4da"],
dashed: true,
});
}
const isFrameSelected = selectedElements.some((element) =>
isFrameLikeElement(element),
);
@@ -901,8 +914,8 @@ const _renderInteractiveScene = ({
y1,
x2,
y2,
selectionColors,
dashed: !!remoteClients,
selectionColors: element.locked ? ["#ced4da"] : selectionColors,
dashed: !!remoteClients || element.locked,
cx,
cy,
activeEmbeddable:
@@ -926,7 +939,9 @@ const _renderInteractiveScene = ({
x2,
y1,
y2,
selectionColors: [oc.black],
selectionColors: groupElements.some((el) => el.locked)
? ["#ced4da"]
: [oc.black],
dashed: true,
cx: x1 + (x2 - x1) / 2,
cy: y1 + (y2 - y1) / 2,
@@ -990,7 +1005,11 @@ const _renderInteractiveScene = ({
);
}
}
} else if (selectedElements.length > 1 && !appState.isRotating) {
} else if (
selectedElements.length > 1 &&
!appState.isRotating &&
!selectedElements.some((el) => el.locked)
) {
const dashedLinePadding =
(DEFAULT_TRANSFORM_HANDLE_SPACING * 2) / appState.zoom.value;
context.fillStyle = oc.white;