diff --git a/packages/common/src/utility-types.ts b/packages/common/src/utility-types.ts index d4804d195..02b3a4e00 100644 --- a/packages/common/src/utility-types.ts +++ b/packages/common/src/utility-types.ts @@ -68,3 +68,28 @@ export type MaybePromise = T | Promise; // get union of all keys from the union of types export type AllPossibleKeys = T extends any ? keyof T : never; + +// utlity types for filter helper and related data structures +// ----------------------------------------------------------------------------- +export type ReadonlyArrayOrMap< + T, + K = T extends { id: string } ? T["id"] : string, +> = readonly T[] | ReadonlyMap; + +export type GenericAccumulator = Set | Map | Array; +export type ArrayAccumulator = Array; +export type MapAccumulator = Map; +export type SetAccumulator = Set; +export type OutputAccumulator< + Accumulator, + OutputType, + Attr extends keyof OutputType = never, +> = Accumulator extends SetAccumulator + ? Set<[Attr] extends [never] ? OutputType : Attr> + : Accumulator extends MapAccumulator + ? Map< + OutputType extends { id: string } ? OutputType["id"] : string, + OutputType + > + : Array; +// ----------------------------------------------------------------------------- diff --git a/packages/element/src/dragElements.ts b/packages/element/src/dragElements.ts index 4f9eb9ca4..a5d8d0c6a 100644 --- a/packages/element/src/dragElements.ts +++ b/packages/element/src/dragElements.ts @@ -26,6 +26,10 @@ import { isTextElement, } from "./typeChecks"; +import { filterElements } from "./utils"; + +import { getFrameChildren } from "./frame"; + import type Scene from "./Scene"; import type { Bounds } from "./bounds"; @@ -65,23 +69,13 @@ export const dragSelectedElements = ( return true; }); - // we do not want a frame and its elements to be selected at the same time - // but when it happens (due to some bug), we want to avoid updating element - // in the frame twice, hence the use of set - const elementsToUpdate = new Set( - selectedElements, + // update frames and their children (use a set to make sure we avoid + // duplicates in case the user already selected the frame's children) + const elementsToUpdate = getFrameChildren( + scene.getNonDeletedElements(), + filterElements(selectedElements, isFrameLikeElement, new Set(), "id"), + new Set(selectedElements), ); - const frames = selectedElements - .filter((e) => isFrameLikeElement(e)) - .map((f) => f.id); - - if (frames.length > 0) { - for (const element of scene.getNonDeletedElements()) { - if (element.frameId !== null && frames.includes(element.frameId)) { - elementsToUpdate.add(element); - } - } - } const origElements: ExcalidrawElement[] = []; diff --git a/packages/element/src/frame.ts b/packages/element/src/frame.ts index 74375a48d..b85d89b51 100644 --- a/packages/element/src/frame.ts +++ b/packages/element/src/frame.ts @@ -9,7 +9,13 @@ import type { StaticCanvasAppState, } from "@excalidraw/excalidraw/types"; -import type { ReadonlySetLike } from "@excalidraw/common/utility-types"; +import type { + ArrayAccumulator, + GenericAccumulator, + ReadonlyArrayOrMap, + ReadonlySetLike, + OutputAccumulator, +} from "@excalidraw/common/utility-types"; import { getElementsWithinSelection, getSelectedElements } from "./selection"; import { getElementsInGroup, selectGroupsFromGivenElements } from "./groups"; @@ -27,6 +33,8 @@ import { isTextElement, } from "./typeChecks"; +import { filterElements } from "./utils"; + import type { ExcalidrawElementsIncludingDeleted } from "./Scene"; import type { @@ -230,17 +238,30 @@ export const groupByFrameLikes = (elements: readonly ExcalidrawElement[]) => { return frameElementsMap; }; -export const getFrameChildren = ( - allElements: ElementsMapOrArray, - frameId: string, -) => { - const frameChildren: ExcalidrawElement[] = []; - for (const element of allElements.values()) { - if (element.frameId === frameId) { - frameChildren.push(element); - } +export const getFrameChildren = < + K extends ExcalidrawElement, + O extends GenericAccumulator = ArrayAccumulator, +>( + allElements: ReadonlyArrayOrMap, + frameId: K["id"] | Set, + output?: O, +): OutputAccumulator => { + if (frameId instanceof Set && frameId.size === 0) { + return (output || []) as any as OutputAccumulator; } - return frameChildren; + + return filterElements( + allElements, + (element): element is K => { + if (!element.frameId) { + return false; + } + return typeof frameId === "string" + ? element.frameId === frameId + : frameId.has(element.frameId); + }, + output || [], + ) as any as OutputAccumulator; }; export const getFrameLikeElements = ( diff --git a/packages/element/src/utils.ts b/packages/element/src/utils.ts index 57b1e4346..3192e2558 100644 --- a/packages/element/src/utils.ts +++ b/packages/element/src/utils.ts @@ -10,7 +10,7 @@ import { type GlobalPoint, } from "@excalidraw/math"; -import { elementCenterPoint } from "@excalidraw/common"; +import { elementCenterPoint, isReadonlyArray } from "@excalidraw/common"; import type { Curve, LineSegment } from "@excalidraw/math"; @@ -18,8 +18,15 @@ import { getCornerRadius } from "./shapes"; import { getDiamondPoints } from "./bounds"; +import type { + GenericAccumulator, + OutputAccumulator, + ReadonlyArrayOrMap, +} from "../../common/src/utility-types"; + import type { ExcalidrawDiamondElement, + ExcalidrawElement, ExcalidrawRectanguloidElement, } from "./types"; @@ -353,3 +360,35 @@ export function deconstructDiamondElement( return [sides, corners]; } + +export const filterElements = < + InputType extends ExcalidrawElement, + PredicateOutputType extends InputType, + AccumulatorType extends GenericAccumulator, + Attr extends keyof PredicateOutputType = never, +>( + elements: ReadonlyArrayOrMap, + predicate: (elem: InputType) => elem is PredicateOutputType, + accumulator: AccumulatorType, + attr?: Attr, +): OutputAccumulator => { + for (const element of isReadonlyArray(elements) + ? elements + : elements.values()) { + if (predicate(element)) { + if (accumulator instanceof Set) { + accumulator.add(attr ? element[attr] : element); + } else if (accumulator instanceof Map) { + accumulator.set(element.id, attr ? element[attr] : element); + } else { + accumulator.push(element); + } + } + } + + return accumulator as any as OutputAccumulator< + AccumulatorType, + PredicateOutputType, + Attr + >; +};