import { getRegionDelta, Point } from "@noya-app/noya-geometry"; import { DEFAULT_MOVEMENT_THRESHOLD, isMoving } from "../utils/isMoving"; import { getMovingOptionsForEvent, MovingOptions } from "../utils/movingUtils"; import { createCanvasPlugin } from "./baseCanvas"; import { BaseContextProperties, ElementProperties, MovableProperties, } from "./contextProperties"; import { modeSwitch, NoneMode } from "./PluginSystem"; import { PointerData } from "./pointerPlugin"; export type MoveMode = | NoneMode | { type: "moving"; delta: Point; snapshot: StateSnapshot }; export type MoveData = { zoom: number; selectedElementIds: string[]; } & Pick; export type MoveElementParameters = { elementIds: string[]; delta: Point; snapshot: StateSnapshot; options: MovingOptions; }; export type DuplicateElementsResult = Pick< MoveElementParameters, "elementIds" | "snapshot" >; export interface MoveContextProperties extends BaseContextProperties, MoveData>, ElementProperties, MovableProperties { adjustMoveDelta?: ( event: React.PointerEvent, options: MoveElementParameters ) => Point; finalizeMove?: (options: MoveElementParameters) => void; duplicateElements?: ( event: React.PointerEvent, options: MoveElementParameters ) => DuplicateElementsResult | undefined; getStateSnapshot: () => StateSnapshot; moveElements: (parameters: MoveElementParameters) => void; } export type MovePluginOptions = { movementThreshold?: number; }; export function movePlugin( options: MovePluginOptions = {} ) { const { movementThreshold = DEFAULT_MOVEMENT_THRESHOLD } = options; return createCanvasPlugin({ name: "move", data: { zoom: 1, selectedElementIds: [], }, handlers: ({ getMode, setMode, getData, setData, adjustMoveDelta = (_, options) => options.delta, ...context }: MoveContextProperties) => ({ onPointerMove(event) { modeSwitch(getMode(), { none: () => { const data = getData(); if (!data.pointerDown) return; const elements = context.filterElements({ at: data.pointerDown.anchor, predicate: context.isMovable, }); if ( elements.length > 0 && isMoving( data.pointerDown.anchor, data.pointerDown.head, data.zoom, movementThreshold ) ) { const snapshot = context.getStateSnapshot(); const baseParameters: MoveElementParameters = { elementIds: data.selectedElementIds, delta: getRegionDelta(data.pointerDown), snapshot, options: getMovingOptionsForEvent(event), }; const duplicated = event.altKey ? context.duplicateElements?.(event, baseParameters) : undefined; const parameters = duplicated ? { ...baseParameters, ...duplicated } : baseParameters; if (duplicated) { setData((data) => ({ ...data, selectedElementIds: duplicated.elementIds, })); } const delta = adjustMoveDelta(event, parameters); setMode({ type: "moving", delta, snapshot: parameters.snapshot, }); event.preventDefault(); } }, moving: ({ snapshot }) => { const data = getData(); if (!data.pointerDown) return; const parameters: MoveElementParameters = { elementIds: data.selectedElementIds, delta: getRegionDelta(data.pointerDown), snapshot, options: getMovingOptionsForEvent(event), }; const delta = adjustMoveDelta(event, parameters); setMode({ type: "moving", delta, snapshot }); context.moveElements?.({ ...parameters, delta }); event.preventDefault(); }, }); }, onPointerUp(event) { const mode = getMode(); if (mode.type !== "moving") return; const data = getData(); const parameters: MoveElementParameters = { elementIds: data.selectedElementIds, delta: mode.delta, snapshot: mode.snapshot, options: getMovingOptionsForEvent(event), }; context.finalizeMove?.(parameters); setMode({ type: "none" }); event.preventDefault(); }, }), }); }