Files
excalidraw/packages/excalidraw/components/UnlockPopup.tsx
Ryan Di 712f267519 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>
2025-05-21 21:57:12 +10:00

76 lines
1.8 KiB
TypeScript

import {
getCommonBounds,
getElementsInGroup,
selectGroupsFromGivenElements,
} from "@excalidraw/element";
import { sceneCoordsToViewportCoords } from "@excalidraw/common";
import { flushSync } from "react-dom";
import { actionToggleElementLock } from "../actions";
import { t } from "../i18n";
import "./UnlockPopup.scss";
import { LockedIconFilled } from "./icons";
import type App from "./App";
import type { AppState } from "../types";
const UnlockPopup = ({
app,
activeLockedId,
}: {
app: App;
activeLockedId: NonNullable<AppState["activeLockedId"]>;
}) => {
const element = app.scene.getElement(activeLockedId);
const elements = element
? [element]
: getElementsInGroup(app.scene.getNonDeletedElementsMap(), activeLockedId);
if (elements.length === 0) {
return null;
}
const [x, y] = getCommonBounds(elements);
const { x: viewX, y: viewY } = sceneCoordsToViewportCoords(
{ sceneX: x, sceneY: y },
app.state,
);
return (
<div
className="UnlockPopup"
style={{
bottom: `${app.state.height + 12 - viewY + app.state.offsetTop}px`,
left: `${viewX - app.state.offsetLeft}px`,
}}
onClick={() => {
flushSync(() => {
const groupIds = selectGroupsFromGivenElements(elements, app.state);
app.setState({
selectedElementIds: elements.reduce(
(acc, element) => ({
...acc,
[element.id]: true,
}),
{},
),
selectedGroupIds: groupIds,
activeLockedId: null,
});
});
app.actionManager.executeAction(actionToggleElementLock);
}}
title={t("labels.elementLock.unlock")}
>
{LockedIconFilled}
</div>
);
};
export default UnlockPopup;