mirror of
https://github.com/excalidraw/excalidraw.git
synced 2026-10-19 16:15:56 +00:00
feat(editor): dblclick to toggle arrowhead (#11615)
This commit is contained in:
@@ -38,6 +38,7 @@ import {
|
||||
} from "../../excalidraw/tests/queries/dom";
|
||||
|
||||
import type {
|
||||
ExcalidrawArrowElement,
|
||||
ExcalidrawElement,
|
||||
ExcalidrawLinearElement,
|
||||
ExcalidrawTextElementWithContainer,
|
||||
@@ -328,11 +329,69 @@ describe("Test Linear Elements", () => {
|
||||
createTwoPointerLinearElement("arrow");
|
||||
expect(h.state.selectedLinearElement?.isEditing).toBe(false);
|
||||
|
||||
mouse.doubleClick();
|
||||
mouse.doubleClickAt(midpoint[0], midpoint[1]);
|
||||
expect(h.state.selectedLinearElement?.isEditing).toBe(false);
|
||||
await getTextEditor();
|
||||
});
|
||||
|
||||
describe("Arrowhead toggle on endpoint dblclick", () => {
|
||||
// the toggle replaces the element (immutable update), so always re-read
|
||||
// the arrow from the scene instead of holding on to a stale reference
|
||||
const getArrow = () => h.elements[0] as ExcalidrawArrowElement;
|
||||
|
||||
it("should toggle end arrowhead between default and none", () => {
|
||||
createTwoPointerLinearElement("arrow");
|
||||
expect(getArrow().endArrowhead).toBe(null);
|
||||
|
||||
mouse.doubleClickAt(p2[0], p2[1]);
|
||||
expect(getArrow().endArrowhead).toBe("arrow");
|
||||
expect(document.querySelector(TEXT_EDITOR_SELECTOR)).toBe(null);
|
||||
|
||||
mouse.doubleClickAt(p2[0], p2[1]);
|
||||
expect(getArrow().endArrowhead).toBe(null);
|
||||
});
|
||||
|
||||
it("should toggle start arrowhead between default and none", () => {
|
||||
createTwoPointerLinearElement("arrow");
|
||||
expect(getArrow().startArrowhead).toBe(null);
|
||||
|
||||
mouse.doubleClickAt(p1[0], p1[1]);
|
||||
expect(getArrow().startArrowhead).toBe("arrow");
|
||||
expect(document.querySelector(TEXT_EDITOR_SELECTOR)).toBe(null);
|
||||
|
||||
mouse.doubleClickAt(p1[0], p1[1]);
|
||||
expect(getArrow().startArrowhead).toBe(null);
|
||||
});
|
||||
|
||||
it("should restore the arrowhead removed by the previous toggle", () => {
|
||||
const arrow = API.createElement({
|
||||
type: "arrow",
|
||||
x: p1[0],
|
||||
y: p1[1],
|
||||
width: p2[0] - p1[0],
|
||||
height: 0,
|
||||
points: [pointFrom(0, 0), pointFrom(p2[0] - p1[0], p2[1] - p1[1])],
|
||||
endArrowhead: "triangle",
|
||||
});
|
||||
API.setElements([arrow]);
|
||||
mouse.clickAt(midpoint[0], midpoint[1]);
|
||||
|
||||
mouse.doubleClickAt(p2[0], p2[1]);
|
||||
expect(getArrow().endArrowhead).toBe(null);
|
||||
|
||||
mouse.doubleClickAt(p2[0], p2[1]);
|
||||
expect(getArrow().endArrowhead).toBe("triangle");
|
||||
});
|
||||
|
||||
it("should not toggle arrowheads on dblclick outside endpoints", () => {
|
||||
createTwoPointerLinearElement("arrow");
|
||||
|
||||
mouse.doubleClickAt(midpoint[0], midpoint[1]);
|
||||
expect(getArrow().startArrowhead).toBe(null);
|
||||
expect(getArrow().endArrowhead).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
it("shouldn't create text element on double click in line editor (arrow)", async () => {
|
||||
createTwoPointerLinearElement("arrow");
|
||||
const arrow = h.elements[0] as ExcalidrawLinearElement;
|
||||
@@ -1091,7 +1150,7 @@ describe("Test Linear Elements", () => {
|
||||
|
||||
expect(h.elements.length).toBe(1);
|
||||
expect(h.elements[0].id).toBe(arrow.id);
|
||||
mouse.doubleClickAt(arrow.x, arrow.y);
|
||||
mouse.doubleClickAt(midpoint[0], midpoint[1]);
|
||||
expect(h.elements.length).toBe(2);
|
||||
|
||||
const text = h.elements[1] as ExcalidrawTextElementWithContainer;
|
||||
|
||||
@@ -286,6 +286,7 @@ import type {
|
||||
ExcalidrawEmbeddableElement,
|
||||
Ordered,
|
||||
MagicGenerationData,
|
||||
Arrowhead,
|
||||
ExcalidrawArrowElement,
|
||||
ExcalidrawElbowArrowElement,
|
||||
SceneElementsMap,
|
||||
@@ -741,6 +742,9 @@ class App extends React.Component<AppProps, AppState> {
|
||||
/** current frame pointer cords */
|
||||
lastPointerMoveCoords: { x: number; y: number } | null = null;
|
||||
private lastCompletedCanvasClicks: { x: number; y: number }[] = [];
|
||||
/** arrowheads removed via endpoint double-click toggle, so a subsequent
|
||||
* toggle can restore the original arrowhead (keyed by `elementId:side`) */
|
||||
private removedArrowheads = new Map<string, Arrowhead>();
|
||||
/** previous frame pointer coords */
|
||||
previousPointerMoveCoords: { x: number; y: number } | null = null;
|
||||
lastViewportPosition = { x: 0, y: 0 };
|
||||
@@ -6648,6 +6652,49 @@ class App extends React.Component<AppProps, AppState> {
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Toggles the arrowhead at the given endpoint between no arrowhead and the
|
||||
* arrowhead it had before the last toggle (falling back to the current
|
||||
* default arrowhead).
|
||||
*/
|
||||
private toggleArrowheadAtEndpoint = (
|
||||
element: ExcalidrawArrowElement,
|
||||
side: "start" | "end",
|
||||
) => {
|
||||
const currentArrowhead =
|
||||
side === "start" ? element.startArrowhead : element.endArrowhead;
|
||||
|
||||
this.store.scheduleCapture();
|
||||
|
||||
let arrowheadUpdate:
|
||||
| { startArrowhead: Arrowhead | null }
|
||||
| { endArrowhead: Arrowhead | null };
|
||||
|
||||
if (currentArrowhead) {
|
||||
this.removedArrowheads.set(`${element.id}:${side}`, currentArrowhead);
|
||||
arrowheadUpdate =
|
||||
side === "start" ? { startArrowhead: null } : { endArrowhead: null };
|
||||
} else {
|
||||
const arrowhead =
|
||||
this.removedArrowheads.get(`${element.id}:${side}`) ??
|
||||
(side === "start"
|
||||
? this.state.currentItemStartArrowhead
|
||||
: this.state.currentItemEndArrowhead) ??
|
||||
"arrow";
|
||||
arrowheadUpdate =
|
||||
side === "start"
|
||||
? { startArrowhead: arrowhead }
|
||||
: { endArrowhead: arrowhead };
|
||||
}
|
||||
|
||||
this.scene.mapElements((_element) => {
|
||||
if (_element.id === element.id && isArrowElement(_element)) {
|
||||
return newElementWith(_element, arrowheadUpdate);
|
||||
}
|
||||
return _element;
|
||||
});
|
||||
};
|
||||
|
||||
private handleCanvasDoubleClick = (
|
||||
event: Pick<
|
||||
React.MouseEvent<HTMLCanvasElement>,
|
||||
@@ -6686,6 +6733,31 @@ class App extends React.Component<AppProps, AppState> {
|
||||
if (selectedElements.length === 1 && isLinearElement(selectedElements[0])) {
|
||||
const selectedLinearElement: ExcalidrawLinearElement =
|
||||
selectedElements[0];
|
||||
|
||||
if (
|
||||
!event[KEYS.CTRL_OR_CMD] &&
|
||||
isArrowElement(selectedLinearElement) &&
|
||||
this.state.selectedLinearElement?.elementId === selectedLinearElement.id
|
||||
) {
|
||||
const clickedPointIndex = LinearElementEditor.getPointIndexUnderCursor(
|
||||
selectedLinearElement,
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
this.state.zoom,
|
||||
sceneX,
|
||||
sceneY,
|
||||
);
|
||||
if (
|
||||
clickedPointIndex === 0 ||
|
||||
clickedPointIndex === selectedLinearElement.points.length - 1
|
||||
) {
|
||||
this.toggleArrowheadAtEndpoint(
|
||||
selectedLinearElement,
|
||||
clickedPointIndex === 0 ? "start" : "end",
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
((event[KEYS.CTRL_OR_CMD] && isSimpleArrow(selectedLinearElement)) ||
|
||||
isLineElement(selectedLinearElement)) &&
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { CANVAS_SEARCH_TAB, DEFAULT_SIDEBAR } from "@excalidraw/common";
|
||||
|
||||
import {
|
||||
isArrowElement,
|
||||
isFlowchartNodeElement,
|
||||
isImageElement,
|
||||
isLinearElement,
|
||||
@@ -184,6 +185,16 @@ const getHints = ({
|
||||
|
||||
if (selectedElements.length === 1) {
|
||||
if (isLinearElement(selectedElements[0])) {
|
||||
if (
|
||||
isArrowElement(selectedElements[0]) &&
|
||||
appState.selectedLinearElement?.elementId ===
|
||||
selectedElements[0].id &&
|
||||
(appState.selectedLinearElement.hoverPointIndex === 0 ||
|
||||
appState.selectedLinearElement.hoverPointIndex ===
|
||||
selectedElements[0].points.length - 1)
|
||||
) {
|
||||
return t("hints.toggleArrowhead");
|
||||
}
|
||||
if (appState.selectedLinearElement?.isEditing) {
|
||||
return appState.selectedLinearElement.selectedPointsIndices
|
||||
? t("hints.lineEditor_pointSelected", {
|
||||
|
||||
@@ -373,6 +373,7 @@
|
||||
"resize": "You can constrain proportions by holding {{shortcut_1}} while resizing,\nhold {{shortcut_2}} to resize from the center",
|
||||
"resizeImage": "You can resize freely by holding {{shortcut_1}},\nhold {{shortcut_2}} to resize from the center",
|
||||
"rotate": "You can constrain angles by holding {{shortcut}} while rotating",
|
||||
"toggleArrowhead": "Double-click to toggle the arrowhead",
|
||||
"lineEditor_info": "Hold {{shortcut_1}} and Double-click or press {{shortcut_2}} to edit points",
|
||||
"lineEditor_line_info": "Double-click or press {{shortcut}} to edit points",
|
||||
"lineEditor_pointSelected": "Press {{shortcut_1}} to remove point(s),\n{{shortcut_2}} to duplicate, or drag to move",
|
||||
|
||||
Reference in New Issue
Block a user