feat: Arrow binding is a preference (#10839)

Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com>
This commit is contained in:
Márk Tolmács
2026-03-03 22:55:40 +01:00
committed by GitHub
parent 60b275880d
commit 437595fa65
24 changed files with 1164 additions and 146 deletions
+36 -19
View File
@@ -119,7 +119,6 @@ import {
fixBindingsAfterDeletion,
getHoveredElementForBinding,
isBindingEnabled,
shouldEnableBindingForPointerEvent,
updateBoundElements,
LinearElementEditor,
newElementWith,
@@ -319,6 +318,8 @@ import {
actionToggleElementLock,
actionToggleLinearEditor,
actionToggleObjectsSnapMode,
actionToggleArrowBinding,
actionToggleMidpointSnapping,
actionToggleCropEditor,
} from "../actions";
import { actionWrapTextInContainer } from "../actions/actionBoundText";
@@ -2734,7 +2735,9 @@ class App extends React.Component<AppProps, AppState> {
private onBlur = withBatchedUpdates(() => {
isHoldingSpace = false;
this.setState({ isBindingEnabled: true });
this.setState({
isBindingEnabled: this.state.bindingPreference === "enabled",
});
});
private onUnload = () => {
@@ -4937,13 +4940,15 @@ class App extends React.Component<AppProps, AppState> {
return;
}
if (event[KEYS.CTRL_OR_CMD] && this.state.isBindingEnabled) {
if (event[KEYS.CTRL_OR_CMD] && !event.repeat) {
if (getFeatureFlag("COMPLEX_BINDINGS")) {
this.resetDelayedBindMode();
}
flushSync(() => {
this.setState({ isBindingEnabled: false });
this.setState({
isBindingEnabled: this.state.bindingPreference !== "enabled",
});
});
maybeHandleArrowPointlikeDrag({ app: this, event });
@@ -5217,10 +5222,13 @@ class App extends React.Component<AppProps, AppState> {
}
}
}
if (!event[KEYS.CTRL_OR_CMD] && !this.state.isBindingEnabled) {
flushSync(() => {
this.setState({ isBindingEnabled: true });
});
if (!event[KEYS.CTRL_OR_CMD]) {
const preferenceEnabled = this.state.bindingPreference === "enabled";
if (this.state.isBindingEnabled !== preferenceEnabled) {
flushSync(() => {
this.setState({ isBindingEnabled: preferenceEnabled });
});
}
maybeHandleArrowPointlikeDrag({ app: this, event });
}
@@ -7138,6 +7146,14 @@ class App extends React.Component<AppProps, AppState> {
private handleCanvasPointerDown = (
event: React.PointerEvent<HTMLElement>,
) => {
// If Ctrl is not held, ensure isBindingEnabled reflects the user preference.
if (!event.ctrlKey) {
const preferenceEnabled = this.state.bindingPreference === "enabled";
if (this.state.isBindingEnabled !== preferenceEnabled) {
this.setState({ isBindingEnabled: preferenceEnabled });
}
}
const scenePointer = viewportCoordsToSceneCoords(event, this.state);
const { x: scenePointerX, y: scenePointerY } = scenePointer;
this.lastPointerMoveCoords = {
@@ -7358,7 +7374,6 @@ class App extends React.Component<AppProps, AppState> {
}
this.clearSelectionIfNotUsingSelection();
this.updateBindingEnabledOnPointerMove(event);
if (this.handleSelectionOnPointerDown(event, pointerDownState)) {
return;
@@ -7581,6 +7596,13 @@ class App extends React.Component<AppProps, AppState> {
this.removePointer(event);
this.lastPointerUpEvent = event;
if (!event.ctrlKey) {
const preferenceEnabled = this.state.bindingPreference === "enabled";
if (this.state.isBindingEnabled !== preferenceEnabled) {
this.setState({ isBindingEnabled: preferenceEnabled });
}
}
const scenePointer = viewportCoordsToSceneCoords(
{ clientX: event.clientX, clientY: event.clientY },
this.state,
@@ -8636,7 +8658,9 @@ class App extends React.Component<AppProps, AppState> {
): void => {
if (event.ctrlKey) {
flushSync(() => {
this.setState({ isBindingEnabled: false });
this.setState({
isBindingEnabled: this.state.bindingPreference !== "enabled",
});
});
}
@@ -11330,15 +11354,6 @@ class App extends React.Component<AppProps, AppState> {
this.addNewImagesToImageCache();
}, IMAGE_RENDER_TIMEOUT);
private updateBindingEnabledOnPointerMove = (
event: React.PointerEvent<HTMLElement>,
) => {
const shouldEnableBinding = shouldEnableBindingForPointerEvent(event);
if (this.state.isBindingEnabled !== shouldEnableBinding) {
this.setState({ isBindingEnabled: shouldEnableBinding });
}
};
private clearSelection(hitElement: ExcalidrawElement | null): void {
this.setState((prevState) => ({
selectedElementIds: makeNextSelectedElementIds({}, prevState),
@@ -12100,6 +12115,8 @@ class App extends React.Component<AppProps, AppState> {
CONTEXT_MENU_SEPARATOR,
actionToggleGridMode,
actionToggleObjectsSnapMode,
actionToggleArrowBinding,
actionToggleMidpointSnapping,
actionToggleZenMode,
actionToggleViewMode,
actionToggleStats,
@@ -249,6 +249,7 @@ const getRelevantAppStateProps = (
multiElement: appState.multiElement,
newElement: appState.newElement,
isBindingEnabled: appState.isBindingEnabled,
isMidpointSnappingEnabled: appState.isMidpointSnappingEnabled,
suggestedBinding: appState.suggestedBinding,
isRotating: appState.isRotating,
elementsToHighlight: appState.elementsToHighlight,
@@ -9,7 +9,9 @@ import {
actionLoadScene,
actionSaveToActiveFile,
actionShortcuts,
actionToggleArrowBinding,
actionToggleGridMode,
actionToggleMidpointSnapping,
actionToggleObjectsSnapMode,
actionToggleSearchMenu,
actionToggleStats,
@@ -443,6 +445,40 @@ const PreferencesToggleSnapModeItem = () => {
);
};
const PreferencesToggleArrowBindingItem = () => {
const { t } = useI18n();
const actionManager = useExcalidrawActionManager();
const appState = useUIAppState();
return (
<DropdownMenuItemCheckbox
checked={appState.bindingPreference === "enabled"}
onSelect={(event) => {
actionManager.executeAction(actionToggleArrowBinding);
event.preventDefault();
}}
>
{t("labels.arrowBinding")}
</DropdownMenuItemCheckbox>
);
};
const PreferencesToggleMidpointSnappingItem = () => {
const { t } = useI18n();
const actionManager = useExcalidrawActionManager();
const appState = useUIAppState();
return (
<DropdownMenuItemCheckbox
checked={appState.isMidpointSnappingEnabled}
onSelect={(event) => {
actionManager.executeAction(actionToggleMidpointSnapping);
event.preventDefault();
}}
>
{t("labels.midpointSnapping")}
</DropdownMenuItemCheckbox>
);
};
export const PreferencesToggleGridModeItem = () => {
const { t } = useI18n();
const actionManager = useExcalidrawActionManager();
@@ -538,6 +574,8 @@ export const Preferences = ({
<PreferencesToggleZenModeItem />
<PreferencesToggleViewModeItem />
<PreferencesToggleElementPropertiesItem />
<PreferencesToggleArrowBindingItem />
<PreferencesToggleMidpointSnappingItem />
</>
)}
{additionalItems}
@@ -548,6 +586,8 @@ export const Preferences = ({
Preferences.ToggleToolLock = PreferencesToggleToolLockItem;
Preferences.ToggleSnapMode = PreferencesToggleSnapModeItem;
Preferences.ToggleArrowBinding = PreferencesToggleArrowBindingItem;
Preferences.ToggleMidpointSnapping = PreferencesToggleMidpointSnappingItem;
Preferences.ToggleGridMode = PreferencesToggleGridModeItem;
Preferences.ToggleZenMode = PreferencesToggleZenModeItem;
Preferences.ToggleViewMode = PreferencesToggleViewModeItem;