import { ExcalidrawElement, NonDeletedExcalidrawElement, } from "../element/types"; import { getElementAbsoluteCoords, getElementBounds } from "../element"; import { AppState } from "../types"; import { isBoundToContainer } from "../element/typeChecks"; import { elementOverlapsWithFrame, getContainingFrame, getFrameElements, } from "../frame"; /** * Frames and their containing elements are not to be selected at the same time. * Given an array of selected elements, if there are frames and their containing elements * we only keep the frames. * @param selectedElements */ export const excludeElementsInFramesFromSelection = < T extends ExcalidrawElement, >( selectedElements: readonly T[], ) => { const framesInSelection = new Set(); selectedElements.forEach((element) => { if (element.type === "frame") { framesInSelection.add(element.id); } }); return selectedElements.filter((element) => { if (element.frameId && framesInSelection.has(element.frameId)) { return false; } return true; }); }; export const getElementsWithinSelection = ( elements: readonly NonDeletedExcalidrawElement[], selection: NonDeletedExcalidrawElement, excludeElementsInFrames: boolean = true, ) => { const [selectionX1, selectionY1, selectionX2, selectionY2] = getElementAbsoluteCoords(selection); let elementsInSelection = elements.filter((element) => { let [elementX1, elementY1, elementX2, elementY2] = getElementBounds(element); const containingFrame = getContainingFrame(element); if (containingFrame) { const [fx1, fy1, fx2, fy2] = getElementBounds(containingFrame); elementX1 = Math.max(fx1, elementX1); elementY1 = Math.max(fy1, elementY1); elementX2 = Math.min(fx2, elementX2); elementY2 = Math.min(fy2, elementY2); } return ( element.locked === false && element.type !== "selection" && !isBoundToContainer(element) && selectionX1 <= elementX1 && selectionY1 <= elementY1 && selectionX2 >= elementX2 && selectionY2 >= elementY2 ); }); elementsInSelection = excludeElementsInFrames ? excludeElementsInFramesFromSelection(elementsInSelection) : elementsInSelection; elementsInSelection = elementsInSelection.filter((element) => { const containingFrame = getContainingFrame(element); if (containingFrame) { return elementOverlapsWithFrame(element, containingFrame); } return true; }); return elementsInSelection; }; export const isSomeElementSelected = ( elements: readonly NonDeletedExcalidrawElement[], appState: Pick, ): boolean => elements.some((element) => appState.selectedElementIds[element.id]); /** * Returns common attribute (picked by `getAttribute` callback) of selected * elements. If elements don't share the same value, returns `null`. */ export const getCommonAttributeOfSelectedElements = ( elements: readonly NonDeletedExcalidrawElement[], appState: Pick, getAttribute: (element: ExcalidrawElement) => T, ): T | null => { const attributes = Array.from( new Set( getSelectedElements(elements, appState).map((element) => getAttribute(element), ), ), ); return attributes.length === 1 ? attributes[0] : null; }; export const getSelectedElements = ( elements: readonly NonDeletedExcalidrawElement[], appState: Pick, opts?: { includeBoundTextElement?: boolean; includeElementsInFrames?: boolean; }, ) => { const selectedElements = elements.filter((element) => { if (appState.selectedElementIds[element.id]) { return element; } if ( opts?.includeBoundTextElement && isBoundToContainer(element) && appState.selectedElementIds[element?.containerId] ) { return element; } return null; }); if (opts?.includeElementsInFrames) { const elementsToInclude: ExcalidrawElement[] = []; selectedElements.forEach((element) => { if (element.type === "frame") { getFrameElements(elements, element.id).forEach((e) => elementsToInclude.push(e), ); } elementsToInclude.push(element); }); return elementsToInclude; } return selectedElements; }; export const getTargetElements = ( elements: readonly NonDeletedExcalidrawElement[], appState: Pick, ) => appState.editingElement ? [appState.editingElement] : getSelectedElements(elements, appState, { includeBoundTextElement: true, });