From 85129823d06a613abb422746ad3a513d6b02ee0d Mon Sep 17 00:00:00 2001 From: miyanokomiya Date: Sun, 22 Sep 2024 16:16:54 +0900 Subject: [PATCH] refactor: Extract range selection funtionality as an utility --- .../shapeTreePanel/ShapeTreePanel.tsx | 21 +------ .../states/appCanvas/commons.spec.ts | 63 ++++++++++++++++++- src/composables/states/appCanvas/commons.ts | 30 +++++++++ 3 files changed, 94 insertions(+), 20 deletions(-) diff --git a/src/components/shapeTreePanel/ShapeTreePanel.tsx b/src/components/shapeTreePanel/ShapeTreePanel.tsx index 777297cc..1ecfc9b7 100644 --- a/src/components/shapeTreePanel/ShapeTreePanel.tsx +++ b/src/components/shapeTreePanel/ShapeTreePanel.tsx @@ -10,6 +10,7 @@ import { isGroupShape } from "../../shapes/group"; import { isAlignBoxShape } from "../../shapes/align/alignBox"; import { isCtrlOrMeta } from "../../utils/devices"; import { ToggleInput } from "../atoms/inputs/ToggleInput"; +import { selectShapesInRange } from "../../composables/states/appCanvas/commons"; type DropOperation = "group" | "above" | "below"; @@ -38,25 +39,7 @@ export const ShapeTreePanel: React.FC = () => { if (multi) { ctx.multiSelectShapes([id], true); } else if (range) { - const lastId = ctx.getLastSelectedShapeId(); - if (!lastId) return; - - const composite = ctx.getShapeComposite(); - const lastSelected = composite.shapeMap[lastId]; - const siblings = - composite.mergedShapeTreeMap[lastSelected.parentId ?? ""]?.children ?? composite.mergedShapeTree; - const siblingIds = siblings.map((s) => s.id); - const lastIndex = siblingIds.findIndex((sid) => sid === lastSelected.id); - const targetIndex = siblingIds.findIndex((sid) => sid === id); - if (lastIndex < targetIndex) { - const ids = siblingIds.slice(lastIndex, targetIndex); - ids.push(id); - ctx.multiSelectShapes(ids, true); - } else if (targetIndex < lastIndex) { - const ids = siblingIds.slice(targetIndex + 1, lastIndex + 1); - ids.push(id); - ctx.multiSelectShapes(ids, true); - } + selectShapesInRange(ctx, id); } else { ctx.selectShape(id); handleEvent({ diff --git a/src/composables/states/appCanvas/commons.spec.ts b/src/composables/states/appCanvas/commons.spec.ts index 2eb3cd91..895223d3 100644 --- a/src/composables/states/appCanvas/commons.spec.ts +++ b/src/composables/states/appCanvas/commons.spec.ts @@ -1,10 +1,17 @@ import { expect, test, describe, vi } from "vitest"; -import { getCommonAcceptableEvents, handleCommonWheel, handleHistoryEvent, handleStateEvent } from "./commons"; +import { + getCommonAcceptableEvents, + handleCommonWheel, + handleHistoryEvent, + handleStateEvent, + selectShapesInRange, +} from "./commons"; import { createShape, getCommonStruct } from "../../../shapes"; import { RectangleShape } from "../../../shapes/rectangle"; import { UserSetting } from "../../../models"; import { createInitialAppCanvasStateContext } from "../../../contexts/AppCanvasContext"; import { createStyleScheme } from "../../../models/factories"; +import { newShapeComposite } from "../../shapeComposite"; function getMockCtx() { return { @@ -96,3 +103,57 @@ describe("handleCommonWheel", () => { expect(ctx2.scrollView).not.toHaveBeenCalled(); }); }); + +describe("selectShapesInRange", () => { + function getCtx() { + return { + getLastSelectedShapeId: vi.fn(), + multiSelectShapes: vi.fn(), + getShapeComposite: () => + newShapeComposite({ + shapes: [ + createShape(getCommonStruct, "group", { id: "group" }), + createShape(getCommonStruct, "rectangle", { id: "child0", parentId: "group" }), + createShape(getCommonStruct, "rectangle", { id: "child1", parentId: "group" }), + createShape(getCommonStruct, "rectangle", { id: "child2", parentId: "group" }), + createShape(getCommonStruct, "rectangle", { id: "root1" }), + createShape(getCommonStruct, "rectangle", { id: "root2" }), + ], + getStruct: getCommonStruct, + }), + }; + } + + test("should select shapes in the range and set the target shape the latest", () => { + const ctx0 = getCtx(); + ctx0.getLastSelectedShapeId.mockReturnValue("child0"); + selectShapesInRange(ctx0, "child2"); + expect(ctx0.multiSelectShapes).toHaveBeenCalledWith(["child0", "child1", "child2"], true); + + const ctx1 = getCtx(); + ctx1.getLastSelectedShapeId.mockReturnValue("child2"); + selectShapesInRange(ctx1, "child0"); + expect(ctx1.multiSelectShapes).toHaveBeenCalledWith(["child1", "child2", "child0"], true); + }); + + test("should select shapes in the range: for root shapes", () => { + const ctx0 = getCtx(); + ctx0.getLastSelectedShapeId.mockReturnValue("group"); + selectShapesInRange(ctx0, "root2"); + expect(ctx0.multiSelectShapes).toHaveBeenCalledWith(["group", "root1", "root2"], true); + }); + + test("should not change selection when target shape isn't in the same scope of selected shapes", () => { + const ctx0 = getCtx(); + ctx0.getLastSelectedShapeId.mockReturnValue("child0"); + selectShapesInRange(ctx0, "root2"); + expect(ctx0.multiSelectShapes).not.toHaveBeenCalled(); + }); + + test("should select the target when no shape is selected", () => { + const ctx0 = getCtx(); + ctx0.getLastSelectedShapeId.mockReturnValue(undefined); + selectShapesInRange(ctx0, "child2"); + expect(ctx0.multiSelectShapes).toHaveBeenCalledWith(["child2"], true); + }); +}); diff --git a/src/composables/states/appCanvas/commons.ts b/src/composables/states/appCanvas/commons.ts index 8e2be2b7..4dc07a3b 100644 --- a/src/composables/states/appCanvas/commons.ts +++ b/src/composables/states/appCanvas/commons.ts @@ -703,3 +703,33 @@ export function panViewToShape( scale: ctx.getScale(), }); } + +export function selectShapesInRange( + ctx: Pick, + targetId: string, +) { + const lastId = ctx.getLastSelectedShapeId(); + if (!lastId) { + ctx.multiSelectShapes([targetId], true); + return; + } + + const shapeComposite = ctx.getShapeComposite(); + const lastSelected = shapeComposite.shapeMap[lastId]; + const siblings = + shapeComposite.mergedShapeTreeMap[lastSelected.parentId ?? ""]?.children ?? shapeComposite.mergedShapeTree; + const siblingIds = siblings.map((s) => s.id); + const lastIndex = siblingIds.findIndex((id) => id === lastSelected.id); + const targetIndex = siblingIds.findIndex((id) => id === targetId); + if (targetIndex === -1) return; + + if (lastIndex < targetIndex) { + const ids = siblingIds.slice(lastIndex, targetIndex); + ids.push(targetId); + ctx.multiSelectShapes(ids, true); + } else if (targetIndex < lastIndex) { + const ids = siblingIds.slice(targetIndex + 1, lastIndex + 1); + ids.push(targetId); + ctx.multiSelectShapes(ids, true); + } +}