|
@@ -1,16 +1,14 @@
|
|
import { bumpVersion } from "./element/mutateElement";
|
|
import { bumpVersion } from "./element/mutateElement";
|
|
import { isFrameElement } from "./element/typeChecks";
|
|
import { isFrameElement } from "./element/typeChecks";
|
|
-import { ExcalidrawElement } from "./element/types";
|
|
|
|
-import { groupByFrames } from "./frame";
|
|
|
|
|
|
+import { ExcalidrawElement, ExcalidrawFrameElement } from "./element/types";
|
|
import { getElementsInGroup } from "./groups";
|
|
import { getElementsInGroup } from "./groups";
|
|
import { getSelectedElements } from "./scene";
|
|
import { getSelectedElements } from "./scene";
|
|
import Scene from "./scene/Scene";
|
|
import Scene from "./scene/Scene";
|
|
import { AppState } from "./types";
|
|
import { AppState } from "./types";
|
|
import { arrayToMap, findIndex, findLastIndex } from "./utils";
|
|
import { arrayToMap, findIndex, findLastIndex } from "./utils";
|
|
|
|
|
|
-// elements that do not belong to a frame are considered a root element
|
|
|
|
-const isRootElement = (element: ExcalidrawElement) => {
|
|
|
|
- return !element.frameId;
|
|
|
|
|
|
+const isOfTargetFrame = (element: ExcalidrawElement, frameId: string) => {
|
|
|
|
+ return element.frameId === frameId || element.id === frameId;
|
|
};
|
|
};
|
|
|
|
|
|
/**
|
|
/**
|
|
@@ -35,6 +33,7 @@ const getIndicesToMove = (
|
|
? elementsToBeMoved
|
|
? elementsToBeMoved
|
|
: getSelectedElements(elements, appState, {
|
|
: getSelectedElements(elements, appState, {
|
|
includeBoundTextElement: true,
|
|
includeBoundTextElement: true,
|
|
|
|
+ includeElementsInFrames: true,
|
|
}),
|
|
}),
|
|
);
|
|
);
|
|
while (++index < elements.length) {
|
|
while (++index < elements.length) {
|
|
@@ -106,6 +105,26 @@ const getTargetIndexAccountingForBinding = (
|
|
}
|
|
}
|
|
};
|
|
};
|
|
|
|
|
|
|
|
+const getContiguousFrameRangeElements = (
|
|
|
|
+ allElements: readonly ExcalidrawElement[],
|
|
|
|
+ frameId: ExcalidrawFrameElement["id"],
|
|
|
|
+) => {
|
|
|
|
+ let rangeStart = -1;
|
|
|
|
+ let rangeEnd = -1;
|
|
|
|
+ allElements.forEach((element, index) => {
|
|
|
|
+ if (isOfTargetFrame(element, frameId)) {
|
|
|
|
+ if (rangeStart === -1) {
|
|
|
|
+ rangeStart = index;
|
|
|
|
+ }
|
|
|
|
+ rangeEnd = index;
|
|
|
|
+ }
|
|
|
|
+ });
|
|
|
|
+ if (rangeStart === -1) {
|
|
|
|
+ return [];
|
|
|
|
+ }
|
|
|
|
+ return allElements.slice(rangeStart, rangeEnd + 1);
|
|
|
|
+};
|
|
|
|
+
|
|
/**
|
|
/**
|
|
* Returns next candidate index that's available to be moved to. Currently that
|
|
* Returns next candidate index that's available to be moved to. Currently that
|
|
* is a non-deleted element, and not inside a group (unless we're editing it).
|
|
* is a non-deleted element, and not inside a group (unless we're editing it).
|
|
@@ -115,6 +134,11 @@ const getTargetIndex = (
|
|
elements: readonly ExcalidrawElement[],
|
|
elements: readonly ExcalidrawElement[],
|
|
boundaryIndex: number,
|
|
boundaryIndex: number,
|
|
direction: "left" | "right",
|
|
direction: "left" | "right",
|
|
|
|
+ /**
|
|
|
|
+ * Frame id if moving frame children.
|
|
|
|
+ * If whole frame (including all children) is being moved, supply `null`.
|
|
|
|
+ */
|
|
|
|
+ containingFrame: ExcalidrawFrameElement["id"] | null,
|
|
) => {
|
|
) => {
|
|
const sourceElement = elements[boundaryIndex];
|
|
const sourceElement = elements[boundaryIndex];
|
|
|
|
|
|
@@ -122,6 +146,9 @@ const getTargetIndex = (
|
|
if (element.isDeleted) {
|
|
if (element.isDeleted) {
|
|
return false;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
+ if (containingFrame) {
|
|
|
|
+ return element.frameId === containingFrame;
|
|
|
|
+ }
|
|
// if we're editing group, find closest sibling irrespective of whether
|
|
// if we're editing group, find closest sibling irrespective of whether
|
|
// there's a different-group element between them (for legacy reasons)
|
|
// there's a different-group element between them (for legacy reasons)
|
|
if (appState.editingGroupId) {
|
|
if (appState.editingGroupId) {
|
|
@@ -132,8 +159,12 @@ const getTargetIndex = (
|
|
|
|
|
|
const candidateIndex =
|
|
const candidateIndex =
|
|
direction === "left"
|
|
direction === "left"
|
|
- ? findLastIndex(elements, indexFilter, Math.max(0, boundaryIndex - 1))
|
|
|
|
- : findIndex(elements, indexFilter, boundaryIndex + 1);
|
|
|
|
|
|
+ ? findLastIndex(
|
|
|
|
+ elements,
|
|
|
|
+ (el) => indexFilter(el),
|
|
|
|
+ Math.max(0, boundaryIndex - 1),
|
|
|
|
+ )
|
|
|
|
+ : findIndex(elements, (el) => indexFilter(el), boundaryIndex + 1);
|
|
|
|
|
|
const nextElement = elements[candidateIndex];
|
|
const nextElement = elements[candidateIndex];
|
|
|
|
|
|
@@ -156,6 +187,19 @@ const getTargetIndex = (
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
+ if (
|
|
|
|
+ !containingFrame &&
|
|
|
|
+ (nextElement.frameId || nextElement.type === "frame")
|
|
|
|
+ ) {
|
|
|
|
+ const frameElements = getContiguousFrameRangeElements(
|
|
|
|
+ elements,
|
|
|
|
+ nextElement.frameId || nextElement.id,
|
|
|
|
+ );
|
|
|
|
+ return direction === "left"
|
|
|
|
+ ? elements.indexOf(frameElements[0])
|
|
|
|
+ : elements.indexOf(frameElements[frameElements.length - 1]);
|
|
|
|
+ }
|
|
|
|
+
|
|
if (!nextElement.groupIds.length) {
|
|
if (!nextElement.groupIds.length) {
|
|
return (
|
|
return (
|
|
getTargetIndexAccountingForBinding(nextElement, elements, direction) ??
|
|
getTargetIndexAccountingForBinding(nextElement, elements, direction) ??
|
|
@@ -195,13 +239,12 @@ const getTargetElementsMap = <T extends ExcalidrawElement>(
|
|
}, {} as Record<string, ExcalidrawElement>);
|
|
}, {} as Record<string, ExcalidrawElement>);
|
|
};
|
|
};
|
|
|
|
|
|
-const _shiftElements = (
|
|
|
|
|
|
+const shiftElementsByOne = (
|
|
elements: readonly ExcalidrawElement[],
|
|
elements: readonly ExcalidrawElement[],
|
|
appState: AppState,
|
|
appState: AppState,
|
|
direction: "left" | "right",
|
|
direction: "left" | "right",
|
|
- elementsToBeMoved?: readonly ExcalidrawElement[],
|
|
|
|
) => {
|
|
) => {
|
|
- const indicesToMove = getIndicesToMove(elements, appState, elementsToBeMoved);
|
|
|
|
|
|
+ const indicesToMove = getIndicesToMove(elements, appState);
|
|
const targetElementsMap = getTargetElementsMap(elements, indicesToMove);
|
|
const targetElementsMap = getTargetElementsMap(elements, indicesToMove);
|
|
let groupedIndices = toContiguousGroups(indicesToMove);
|
|
let groupedIndices = toContiguousGroups(indicesToMove);
|
|
|
|
|
|
@@ -209,16 +252,30 @@ const _shiftElements = (
|
|
groupedIndices = groupedIndices.reverse();
|
|
groupedIndices = groupedIndices.reverse();
|
|
}
|
|
}
|
|
|
|
|
|
|
|
+ const selectedFrames = new Set<ExcalidrawFrameElement["id"]>(
|
|
|
|
+ indicesToMove
|
|
|
|
+ .filter((idx) => elements[idx].type === "frame")
|
|
|
|
+ .map((idx) => elements[idx].id),
|
|
|
|
+ );
|
|
|
|
+
|
|
groupedIndices.forEach((indices, i) => {
|
|
groupedIndices.forEach((indices, i) => {
|
|
const leadingIndex = indices[0];
|
|
const leadingIndex = indices[0];
|
|
const trailingIndex = indices[indices.length - 1];
|
|
const trailingIndex = indices[indices.length - 1];
|
|
const boundaryIndex = direction === "left" ? leadingIndex : trailingIndex;
|
|
const boundaryIndex = direction === "left" ? leadingIndex : trailingIndex;
|
|
|
|
|
|
|
|
+ const containingFrame = indices.some((idx) => {
|
|
|
|
+ const el = elements[idx];
|
|
|
|
+ return el.frameId && selectedFrames.has(el.frameId);
|
|
|
|
+ })
|
|
|
|
+ ? null
|
|
|
|
+ : elements[boundaryIndex]?.frameId;
|
|
|
|
+
|
|
const targetIndex = getTargetIndex(
|
|
const targetIndex = getTargetIndex(
|
|
appState,
|
|
appState,
|
|
elements,
|
|
elements,
|
|
boundaryIndex,
|
|
boundaryIndex,
|
|
direction,
|
|
direction,
|
|
|
|
+ containingFrame,
|
|
);
|
|
);
|
|
|
|
|
|
if (targetIndex === -1 || boundaryIndex === targetIndex) {
|
|
if (targetIndex === -1 || boundaryIndex === targetIndex) {
|
|
@@ -263,34 +320,25 @@ const _shiftElements = (
|
|
});
|
|
});
|
|
};
|
|
};
|
|
|
|
|
|
-const shiftElements = (
|
|
|
|
- appState: AppState,
|
|
|
|
- elements: readonly ExcalidrawElement[],
|
|
|
|
- direction: "left" | "right",
|
|
|
|
- elementsToBeMoved?: readonly ExcalidrawElement[],
|
|
|
|
-) => {
|
|
|
|
- return shift(
|
|
|
|
- elements,
|
|
|
|
- appState,
|
|
|
|
- direction,
|
|
|
|
- _shiftElements,
|
|
|
|
- elementsToBeMoved,
|
|
|
|
- );
|
|
|
|
-};
|
|
|
|
-
|
|
|
|
-const _shiftElementsToEnd = (
|
|
|
|
|
|
+const shiftElementsToEnd = (
|
|
elements: readonly ExcalidrawElement[],
|
|
elements: readonly ExcalidrawElement[],
|
|
appState: AppState,
|
|
appState: AppState,
|
|
direction: "left" | "right",
|
|
direction: "left" | "right",
|
|
|
|
+ containingFrame: ExcalidrawFrameElement["id"] | null,
|
|
|
|
+ elementsToBeMoved?: readonly ExcalidrawElement[],
|
|
) => {
|
|
) => {
|
|
- const indicesToMove = getIndicesToMove(elements, appState);
|
|
|
|
|
|
+ const indicesToMove = getIndicesToMove(elements, appState, elementsToBeMoved);
|
|
const targetElementsMap = getTargetElementsMap(elements, indicesToMove);
|
|
const targetElementsMap = getTargetElementsMap(elements, indicesToMove);
|
|
const displacedElements: ExcalidrawElement[] = [];
|
|
const displacedElements: ExcalidrawElement[] = [];
|
|
|
|
|
|
let leadingIndex: number;
|
|
let leadingIndex: number;
|
|
let trailingIndex: number;
|
|
let trailingIndex: number;
|
|
if (direction === "left") {
|
|
if (direction === "left") {
|
|
- if (appState.editingGroupId) {
|
|
|
|
|
|
+ if (containingFrame) {
|
|
|
|
+ leadingIndex = findIndex(elements, (el) =>
|
|
|
|
+ isOfTargetFrame(el, containingFrame),
|
|
|
|
+ );
|
|
|
|
+ } else if (appState.editingGroupId) {
|
|
const groupElements = getElementsInGroup(
|
|
const groupElements = getElementsInGroup(
|
|
elements,
|
|
elements,
|
|
appState.editingGroupId,
|
|
appState.editingGroupId,
|
|
@@ -305,7 +353,11 @@ const _shiftElementsToEnd = (
|
|
|
|
|
|
trailingIndex = indicesToMove[indicesToMove.length - 1];
|
|
trailingIndex = indicesToMove[indicesToMove.length - 1];
|
|
} else {
|
|
} else {
|
|
- if (appState.editingGroupId) {
|
|
|
|
|
|
+ if (containingFrame) {
|
|
|
|
+ trailingIndex = findLastIndex(elements, (el) =>
|
|
|
|
+ isOfTargetFrame(el, containingFrame),
|
|
|
|
+ );
|
|
|
|
+ } else if (appState.editingGroupId) {
|
|
const groupElements = getElementsInGroup(
|
|
const groupElements = getElementsInGroup(
|
|
elements,
|
|
elements,
|
|
appState.editingGroupId,
|
|
appState.editingGroupId,
|
|
@@ -321,6 +373,10 @@ const _shiftElementsToEnd = (
|
|
leadingIndex = indicesToMove[0];
|
|
leadingIndex = indicesToMove[0];
|
|
}
|
|
}
|
|
|
|
|
|
|
|
+ if (leadingIndex === -1) {
|
|
|
|
+ leadingIndex = 0;
|
|
|
|
+ }
|
|
|
|
+
|
|
for (let index = leadingIndex; index < trailingIndex + 1; index++) {
|
|
for (let index = leadingIndex; index < trailingIndex + 1; index++) {
|
|
if (!indicesToMove.includes(index)) {
|
|
if (!indicesToMove.includes(index)) {
|
|
displacedElements.push(elements[index]);
|
|
displacedElements.push(elements[index]);
|
|
@@ -349,121 +405,123 @@ const _shiftElementsToEnd = (
|
|
];
|
|
];
|
|
};
|
|
};
|
|
|
|
|
|
-const shiftElementsToEnd = (
|
|
|
|
- elements: readonly ExcalidrawElement[],
|
|
|
|
- appState: AppState,
|
|
|
|
- direction: "left" | "right",
|
|
|
|
- elementsToBeMoved?: readonly ExcalidrawElement[],
|
|
|
|
-) => {
|
|
|
|
- return shift(
|
|
|
|
- elements,
|
|
|
|
- appState,
|
|
|
|
- direction,
|
|
|
|
- _shiftElementsToEnd,
|
|
|
|
- elementsToBeMoved,
|
|
|
|
- );
|
|
|
|
-};
|
|
|
|
-
|
|
|
|
-function shift(
|
|
|
|
- elements: readonly ExcalidrawElement[],
|
|
|
|
|
|
+function shiftElementsAccountingForFrames(
|
|
|
|
+ allElements: readonly ExcalidrawElement[],
|
|
appState: AppState,
|
|
appState: AppState,
|
|
direction: "left" | "right",
|
|
direction: "left" | "right",
|
|
shiftFunction: (
|
|
shiftFunction: (
|
|
- elements: ExcalidrawElement[],
|
|
|
|
|
|
+ elements: readonly ExcalidrawElement[],
|
|
appState: AppState,
|
|
appState: AppState,
|
|
direction: "left" | "right",
|
|
direction: "left" | "right",
|
|
|
|
+ containingFrame: ExcalidrawFrameElement["id"] | null,
|
|
elementsToBeMoved?: readonly ExcalidrawElement[],
|
|
elementsToBeMoved?: readonly ExcalidrawElement[],
|
|
) => ExcalidrawElement[] | readonly ExcalidrawElement[],
|
|
) => ExcalidrawElement[] | readonly ExcalidrawElement[],
|
|
- elementsToBeMoved?: readonly ExcalidrawElement[],
|
|
|
|
) {
|
|
) {
|
|
- const elementsMap = arrayToMap(elements);
|
|
|
|
- const frameElementsMap = groupByFrames(elements);
|
|
|
|
-
|
|
|
|
- // in case root is non-existent, we promote children elements to root
|
|
|
|
- let rootElements = elements.filter(
|
|
|
|
- (element) =>
|
|
|
|
- isRootElement(element) ||
|
|
|
|
- (element.frameId && !elementsMap.has(element.frameId)),
|
|
|
|
|
|
+ const elementsToMove = arrayToMap(
|
|
|
|
+ getSelectedElements(allElements, appState, {
|
|
|
|
+ includeBoundTextElement: true,
|
|
|
|
+ includeElementsInFrames: true,
|
|
|
|
+ }),
|
|
);
|
|
);
|
|
- // and remove non-existet root
|
|
|
|
- for (const frameId of frameElementsMap.keys()) {
|
|
|
|
- if (!elementsMap.has(frameId)) {
|
|
|
|
- frameElementsMap.delete(frameId);
|
|
|
|
|
|
+
|
|
|
|
+ const frameAwareContiguousElementsToMove: {
|
|
|
|
+ regularElements: ExcalidrawElement[];
|
|
|
|
+ frameChildren: Map<ExcalidrawFrameElement["id"], ExcalidrawElement[]>;
|
|
|
|
+ } = { regularElements: [], frameChildren: new Map() };
|
|
|
|
+
|
|
|
|
+ const fullySelectedFrames = new Set<ExcalidrawFrameElement["id"]>();
|
|
|
|
+
|
|
|
|
+ for (const element of allElements) {
|
|
|
|
+ if (elementsToMove.has(element.id) && isFrameElement(element)) {
|
|
|
|
+ fullySelectedFrames.add(element.id);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
- // shift the root elements first
|
|
|
|
- rootElements = shiftFunction(
|
|
|
|
- rootElements,
|
|
|
|
- appState,
|
|
|
|
- direction,
|
|
|
|
- elementsToBeMoved,
|
|
|
|
- ) as ExcalidrawElement[];
|
|
|
|
-
|
|
|
|
- // shift the elements in frames if needed
|
|
|
|
- frameElementsMap.forEach((frameElements, frameId) => {
|
|
|
|
- if (!appState.selectedElementIds[frameId]) {
|
|
|
|
- frameElementsMap.set(
|
|
|
|
- frameId,
|
|
|
|
- shiftFunction(
|
|
|
|
- frameElements,
|
|
|
|
- appState,
|
|
|
|
- direction,
|
|
|
|
- elementsToBeMoved,
|
|
|
|
- ) as ExcalidrawElement[],
|
|
|
|
- );
|
|
|
|
|
|
+ for (const element of allElements) {
|
|
|
|
+ if (elementsToMove.has(element.id)) {
|
|
|
|
+ if (
|
|
|
|
+ isFrameElement(element) ||
|
|
|
|
+ (element.frameId && fullySelectedFrames.has(element.frameId))
|
|
|
|
+ ) {
|
|
|
|
+ frameAwareContiguousElementsToMove.regularElements.push(element);
|
|
|
|
+ } else if (!element.frameId) {
|
|
|
|
+ frameAwareContiguousElementsToMove.regularElements.push(element);
|
|
|
|
+ } else {
|
|
|
|
+ const frameChildren =
|
|
|
|
+ frameAwareContiguousElementsToMove.frameChildren.get(
|
|
|
|
+ element.frameId,
|
|
|
|
+ ) || [];
|
|
|
|
+ frameChildren.push(element);
|
|
|
|
+ frameAwareContiguousElementsToMove.frameChildren.set(
|
|
|
|
+ element.frameId,
|
|
|
|
+ frameChildren,
|
|
|
|
+ );
|
|
|
|
+ }
|
|
}
|
|
}
|
|
- });
|
|
|
|
|
|
+ }
|
|
|
|
|
|
- // return the final elements
|
|
|
|
- let finalElements: ExcalidrawElement[] = [];
|
|
|
|
|
|
+ let nextElements = allElements;
|
|
|
|
|
|
- rootElements.forEach((element) => {
|
|
|
|
- if (isFrameElement(element)) {
|
|
|
|
- finalElements = [
|
|
|
|
- ...finalElements,
|
|
|
|
- ...(frameElementsMap.get(element.id) ?? []),
|
|
|
|
- element,
|
|
|
|
- ];
|
|
|
|
- } else {
|
|
|
|
- finalElements = [...finalElements, element];
|
|
|
|
- }
|
|
|
|
- });
|
|
|
|
|
|
+ const frameChildrenSets = Array.from(
|
|
|
|
+ frameAwareContiguousElementsToMove.frameChildren.entries(),
|
|
|
|
+ );
|
|
|
|
+
|
|
|
|
+ for (const [frameId, children] of frameChildrenSets) {
|
|
|
|
+ nextElements = shiftFunction(
|
|
|
|
+ allElements,
|
|
|
|
+ appState,
|
|
|
|
+ direction,
|
|
|
|
+ frameId,
|
|
|
|
+ children,
|
|
|
|
+ );
|
|
|
|
+ }
|
|
|
|
|
|
- return finalElements;
|
|
|
|
|
|
+ return shiftFunction(
|
|
|
|
+ nextElements,
|
|
|
|
+ appState,
|
|
|
|
+ direction,
|
|
|
|
+ null,
|
|
|
|
+ frameAwareContiguousElementsToMove.regularElements,
|
|
|
|
+ );
|
|
}
|
|
}
|
|
|
|
|
|
// public API
|
|
// public API
|
|
// -----------------------------------------------------------------------------
|
|
// -----------------------------------------------------------------------------
|
|
|
|
|
|
export const moveOneLeft = (
|
|
export const moveOneLeft = (
|
|
- elements: readonly ExcalidrawElement[],
|
|
|
|
|
|
+ allElements: readonly ExcalidrawElement[],
|
|
appState: AppState,
|
|
appState: AppState,
|
|
- elementsToBeMoved?: readonly ExcalidrawElement[],
|
|
|
|
) => {
|
|
) => {
|
|
- return shiftElements(appState, elements, "left", elementsToBeMoved);
|
|
|
|
|
|
+ return shiftElementsByOne(allElements, appState, "left");
|
|
};
|
|
};
|
|
|
|
|
|
export const moveOneRight = (
|
|
export const moveOneRight = (
|
|
- elements: readonly ExcalidrawElement[],
|
|
|
|
|
|
+ allElements: readonly ExcalidrawElement[],
|
|
appState: AppState,
|
|
appState: AppState,
|
|
- elementsToBeMoved?: readonly ExcalidrawElement[],
|
|
|
|
) => {
|
|
) => {
|
|
- return shiftElements(appState, elements, "right", elementsToBeMoved);
|
|
|
|
|
|
+ return shiftElementsByOne(allElements, appState, "right");
|
|
};
|
|
};
|
|
|
|
|
|
export const moveAllLeft = (
|
|
export const moveAllLeft = (
|
|
- elements: readonly ExcalidrawElement[],
|
|
|
|
|
|
+ allElements: readonly ExcalidrawElement[],
|
|
appState: AppState,
|
|
appState: AppState,
|
|
- elementsToBeMoved?: readonly ExcalidrawElement[],
|
|
|
|
) => {
|
|
) => {
|
|
- return shiftElementsToEnd(elements, appState, "left", elementsToBeMoved);
|
|
|
|
|
|
+ return shiftElementsAccountingForFrames(
|
|
|
|
+ allElements,
|
|
|
|
+ appState,
|
|
|
|
+ "left",
|
|
|
|
+ shiftElementsToEnd,
|
|
|
|
+ );
|
|
};
|
|
};
|
|
|
|
|
|
export const moveAllRight = (
|
|
export const moveAllRight = (
|
|
- elements: readonly ExcalidrawElement[],
|
|
|
|
|
|
+ allElements: readonly ExcalidrawElement[],
|
|
appState: AppState,
|
|
appState: AppState,
|
|
- elementsToBeMoved?: readonly ExcalidrawElement[],
|
|
|
|
) => {
|
|
) => {
|
|
- return shiftElementsToEnd(elements, appState, "right", elementsToBeMoved);
|
|
|
|
|
|
+ return shiftElementsAccountingForFrames(
|
|
|
|
+ allElements,
|
|
|
|
+ appState,
|
|
|
|
+ "right",
|
|
|
|
+ shiftElementsToEnd,
|
|
|
|
+ );
|
|
};
|
|
};
|