1
0
mirror of https://github.com/excalidraw/excalidraw.git synced 2024-11-02 03:25:53 +01:00

fix: frame add/remove/z-index ordering changes (#7194)

This commit is contained in:
David Luzar 2023-10-25 23:16:02 +02:00 committed by GitHub
parent f098789d16
commit 0f81c30276
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 512 additions and 212 deletions

@ -123,7 +123,7 @@ describe("adding elements to frames", () => {
const commonTestCases = async (
func: typeof resizeFrameOverElement | typeof dragElementIntoFrame,
) => {
describe("when frame is in a layer below", async () => {
describe.skip("when frame is in a layer below", async () => {
it("should add an element", async () => {
h.elements = [frame, rect2];
@ -167,7 +167,7 @@ describe("adding elements to frames", () => {
});
});
describe("when frame is in a layer above", async () => {
describe.skip("when frame is in a layer above", async () => {
it("should add an element", async () => {
h.elements = [rect2, frame];
@ -212,7 +212,7 @@ describe("adding elements to frames", () => {
});
describe("when frame is in an inner layer", async () => {
it("should add elements", async () => {
it.skip("should add elements", async () => {
h.elements = [rect2, frame, rect3];
func(frame, rect2);
@ -223,7 +223,7 @@ describe("adding elements to frames", () => {
expectEqualIds([rect2, rect3, frame]);
});
it("should add elements when there are other other elements in between", async () => {
it.skip("should add elements when there are other other elements in between", async () => {
h.elements = [rect2, rect1, frame, rect4, rect3];
func(frame, rect2);
@ -234,7 +234,7 @@ describe("adding elements to frames", () => {
expectEqualIds([rect1, rect2, rect3, frame, rect4]);
});
it("should add elements when there are other elements in between and the order is reversed", async () => {
it.skip("should add elements when there are other elements in between and the order is reversed", async () => {
h.elements = [rect3, rect4, frame, rect2, rect1];
func(frame, rect2);
@ -289,7 +289,7 @@ describe("adding elements to frames", () => {
describe("resizing frame over elements", async () => {
await commonTestCases(resizeFrameOverElement);
it("resizing over text containers and labelled arrows", async () => {
it.skip("resizing over text containers and labelled arrows", async () => {
await resizingTest(
"rectangle",
["frame", "rectangle", "text"],
@ -339,7 +339,7 @@ describe("adding elements to frames", () => {
// );
});
it("should add arrow bound with text when frame is in a layer below", async () => {
it.skip("should add arrow bound with text when frame is in a layer below", async () => {
h.elements = [frame, arrow, text];
resizeFrameOverElement(frame, arrow);
@ -359,7 +359,7 @@ describe("adding elements to frames", () => {
expectEqualIds([arrow, text, frame]);
});
it("should add arrow bound with text when frame is in an inner layer", async () => {
it.skip("should add arrow bound with text when frame is in an inner layer", async () => {
h.elements = [arrow, frame, text];
resizeFrameOverElement(frame, arrow);
@ -371,7 +371,7 @@ describe("adding elements to frames", () => {
});
describe("resizing frame over elements but downwards", async () => {
it("should add elements when frame is in a layer below", async () => {
it.skip("should add elements when frame is in a layer below", async () => {
h.elements = [frame, rect1, rect2, rect3, rect4];
resizeFrameOverElement(frame, rect4);
@ -382,7 +382,7 @@ describe("adding elements to frames", () => {
expectEqualIds([rect2, rect3, frame, rect4, rect1]);
});
it("should add elements when frame is in a layer above", async () => {
it.skip("should add elements when frame is in a layer above", async () => {
h.elements = [rect1, rect2, rect3, rect4, frame];
resizeFrameOverElement(frame, rect4);
@ -393,7 +393,7 @@ describe("adding elements to frames", () => {
expectEqualIds([rect1, rect2, rect3, frame, rect4]);
});
it("should add elements when frame is in an inner layer", async () => {
it.skip("should add elements when frame is in an inner layer", async () => {
h.elements = [rect1, rect2, frame, rect3, rect4];
resizeFrameOverElement(frame, rect4);
@ -408,7 +408,7 @@ describe("adding elements to frames", () => {
describe("dragging elements into the frame", async () => {
await commonTestCases(dragElementIntoFrame);
it("should drag element inside, duplicate it and keep it in frame", () => {
it.skip("should drag element inside, duplicate it and keep it in frame", () => {
h.elements = [frame, rect2];
dragElementIntoFrame(frame, rect2);
@ -422,7 +422,7 @@ describe("adding elements to frames", () => {
expectEqualIds([rect2_copy, rect2, frame]);
});
it("should drag element inside, duplicate it and remove it from frame", () => {
it.skip("should drag element inside, duplicate it and remove it from frame", () => {
h.elements = [frame, rect2];
dragElementIntoFrame(frame, rect2);

@ -19,7 +19,6 @@ import { mutateElement } from "./element/mutateElement";
import { AppClassProperties, AppState, StaticCanvasAppState } from "./types";
import { getElementsWithinSelection, getSelectedElements } from "./scene";
import { isFrameElement } from "./element";
import { moveOneRight } from "./zindex";
import { getElementsInGroup, selectGroupsFromGivenElements } from "./groups";
import Scene, { ExcalidrawElementsIncludingDeleted } from "./scene/Scene";
import { getElementLineSegments } from "./element/bounds";
@ -463,20 +462,17 @@ export const addElementsToFrame = (
elementsToAdd: NonDeletedExcalidrawElement[],
frame: ExcalidrawFrameElement,
) => {
const { allElementsIndexMap, currTargetFrameChildrenMap } =
allElements.reduce(
(acc, element, index) => {
acc.allElementsIndexMap.set(element.id, index);
if (element.frameId === frame.id) {
acc.currTargetFrameChildrenMap.set(element.id, true);
}
return acc;
},
{
allElementsIndexMap: new Map<ExcalidrawElement["id"], number>(),
currTargetFrameChildrenMap: new Map<ExcalidrawElement["id"], true>(),
},
);
const { currTargetFrameChildrenMap } = allElements.reduce(
(acc, element, index) => {
if (element.frameId === frame.id) {
acc.currTargetFrameChildrenMap.set(element.id, true);
}
return acc;
},
{
currTargetFrameChildrenMap: new Map<ExcalidrawElement["id"], true>(),
},
);
const suppliedElementsToAddSet = new Set(elementsToAdd.map((el) => el.id));
@ -502,66 +498,6 @@ export const addElementsToFrame = (
}
}
const finalElementsToAddSet = new Set(finalElementsToAdd.map((el) => el.id));
const nextElements: ExcalidrawElement[] = [];
const processedElements = new Set<ExcalidrawElement["id"]>();
for (const element of allElements) {
if (processedElements.has(element.id)) {
continue;
}
processedElements.add(element.id);
if (
finalElementsToAddSet.has(element.id) ||
(element.frameId && element.frameId === frame.id)
) {
// will be added in bulk once we process target frame
continue;
}
// target frame
if (element.id === frame.id) {
const currFrameChildren = getFrameElements(allElements, frame.id);
currFrameChildren.forEach((child) => {
processedElements.add(child.id);
});
// if not found, add all children on top by assigning the lowest index
const targetFrameIndex = allElementsIndexMap.get(frame.id) ?? -1;
const { newChildren_left, newChildren_right } = finalElementsToAdd.reduce(
(acc, element) => {
// if index not found, add on top of current frame children
const elementIndex = allElementsIndexMap.get(element.id) ?? Infinity;
if (elementIndex < targetFrameIndex) {
acc.newChildren_left.push(element);
} else {
acc.newChildren_right.push(element);
}
return acc;
},
{
newChildren_left: [] as ExcalidrawElement[],
newChildren_right: [] as ExcalidrawElement[],
},
);
nextElements.push(
...newChildren_left,
...currFrameChildren,
...newChildren_right,
element,
);
continue;
}
nextElements.push(element);
}
for (const element of finalElementsToAdd) {
mutateElement(
element,
@ -571,8 +507,7 @@ export const addElementsToFrame = (
false,
);
}
return nextElements;
return allElements.slice();
};
export const removeElementsFromFrame = (
@ -580,20 +515,34 @@ export const removeElementsFromFrame = (
elementsToRemove: NonDeletedExcalidrawElement[],
appState: AppState,
) => {
const _elementsToRemove: ExcalidrawElement[] = [];
const _elementsToRemove = new Map<
ExcalidrawElement["id"],
ExcalidrawElement
>();
const toRemoveElementsByFrame = new Map<
ExcalidrawFrameElement["id"],
ExcalidrawElement[]
>();
for (const element of elementsToRemove) {
if (element.frameId) {
_elementsToRemove.push(element);
_elementsToRemove.set(element.id, element);
const arr = toRemoveElementsByFrame.get(element.frameId) || [];
arr.push(element);
const boundTextElement = getBoundTextElement(element);
if (boundTextElement) {
_elementsToRemove.push(boundTextElement);
_elementsToRemove.set(boundTextElement.id, boundTextElement);
arr.push(boundTextElement);
}
toRemoveElementsByFrame.set(element.frameId, arr);
}
}
for (const element of _elementsToRemove) {
for (const [, element] of _elementsToRemove) {
mutateElement(
element,
{
@ -603,13 +552,7 @@ export const removeElementsFromFrame = (
);
}
const nextElements = moveOneRight(
allElements,
appState,
Array.from(_elementsToRemove),
);
return nextElements;
return allElements.slice();
};
export const removeAllElementsFromFrame = (

@ -94,7 +94,7 @@ export class API {
angle?: number;
id?: string;
isDeleted?: boolean;
frameId?: ExcalidrawElement["id"];
frameId?: ExcalidrawElement["id"] | null;
groupIds?: string[];
// generic element props
strokeColor?: ExcalidrawGenericElement["strokeColor"];

@ -12,6 +12,11 @@ import {
import { AppState } from "../types";
import { API } from "./helpers/api";
import { selectGroupsForSelectedElements } from "../groups";
import {
ExcalidrawElement,
ExcalidrawFrameElement,
ExcalidrawSelectionElement,
} from "../element/types";
// Unmount ReactDOM from root
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
@ -23,9 +28,15 @@ beforeEach(() => {
const { h } = window;
type ExcalidrawElementType = Exclude<
ExcalidrawElement,
ExcalidrawSelectionElement
>["type"];
const populateElements = (
elements: {
id: string;
type?: ExcalidrawElementType;
isDeleted?: boolean;
isSelected?: boolean;
groupIds?: string[];
@ -34,6 +45,7 @@ const populateElements = (
width?: number;
height?: number;
containerId?: string;
frameId?: ExcalidrawFrameElement["id"];
}[],
appState?: Partial<AppState>,
) => {
@ -50,9 +62,11 @@ const populateElements = (
width = 100,
height = 100,
containerId = null,
frameId = null,
type,
}) => {
const element = API.createElement({
type: containerId ? "text" : "rectangle",
type: type ?? (containerId ? "text" : "rectangle"),
id,
isDeleted,
x,
@ -61,6 +75,7 @@ const populateElements = (
height,
groupIds,
containerId,
frameId: frameId || null,
});
if (isSelected) {
selectedElementIds[element.id] = true;
@ -116,6 +131,8 @@ const assertZindex = ({
isSelected?: true;
groupIds?: string[];
containerId?: string;
frameId?: ExcalidrawFrameElement["id"];
type?: ExcalidrawElementType;
}[];
appState?: Partial<AppState>;
operations: [Actions, string[]][];
@ -1183,3 +1200,285 @@ describe("z-index manipulation", () => {
});
});
});
describe("z-indexing with frames", () => {
beforeEach(async () => {
await render(<Excalidraw />);
});
// naming scheme:
// F# ... frame element
// F#_# ... frame child of F# (rectangle)
// R# ... unrelated element (rectangle)
it("moving whole frame by one (normalized)", () => {
// normalized frame order
assertZindex({
elements: [
{ id: "F1_1", frameId: "F1" },
{ id: "F1_2", frameId: "F1" },
{ id: "F1", type: "frame", isSelected: true },
{ id: "R1" },
{ id: "R2" },
],
operations: [
// +1
[actionBringForward, ["R1", "F1_1", "F1_2", "F1", "R2"]],
// +1
[actionBringForward, ["R1", "R2", "F1_1", "F1_2", "F1"]],
// noop
[actionBringForward, ["R1", "R2", "F1_1", "F1_2", "F1"]],
// -1
[actionSendBackward, ["R1", "F1_1", "F1_2", "F1", "R2"]],
// -1
[actionSendBackward, ["F1_1", "F1_2", "F1", "R1", "R2"]],
// noop
[actionSendBackward, ["F1_1", "F1_2", "F1", "R1", "R2"]],
],
});
});
it("moving whole frame by one (DENORMALIZED)", () => {
// DENORMALIZED FRAME ORDER
assertZindex({
elements: [
{ id: "F1_1", frameId: "F1" },
{ id: "F1", type: "frame", isSelected: true },
{ id: "F1_2", frameId: "F1" },
{ id: "R1" },
{ id: "R2" },
],
operations: [
// +1
[actionBringForward, ["R1", "F1_1", "F1", "F1_2", "R2"]],
// +1
[actionBringForward, ["R1", "R2", "F1_1", "F1", "F1_2"]],
// noop
[actionBringForward, ["R1", "R2", "F1_1", "F1", "F1_2"]],
],
});
// DENORMALIZED FRAME ORDER
assertZindex({
elements: [
{ id: "F1_1", frameId: "F1" },
{ id: "F1", type: "frame", isSelected: true },
{ id: "R1" },
{ id: "F1_2", frameId: "F1" },
{ id: "R2" },
],
operations: [
// +1
[actionBringForward, ["R1", "F1_1", "F1", "R2", "F1_2"]],
// +1
[actionBringForward, ["R1", "R2", "F1_1", "F1", "F1_2"]],
// noop
[actionBringForward, ["R1", "R2", "F1_1", "F1", "F1_2"]],
],
});
// DENORMALIZED FRAME ORDER
assertZindex({
elements: [
{ id: "F1_1", frameId: "F1" },
{ id: "R1" },
{ id: "F1", type: "frame", isSelected: true },
{ id: "R2" },
{ id: "F1_2", frameId: "F1" },
{ id: "R3" },
],
operations: [
// +1
[actionBringForward, ["R1", "F1_1", "R2", "F1", "R3", "F1_2"]],
// +1
// FIXME incorrect, should put F1_1 after R3
[actionBringForward, ["R1", "R2", "F1_1", "R3", "F1", "F1_2"]],
// +1
// FIXME should be noop from previous step after it's fixed
[actionBringForward, ["R1", "R2", "R3", "F1_1", "F1", "F1_2"]],
],
});
// DENORMALIZED FRAME ORDER
assertZindex({
elements: [
{ id: "F1_1", frameId: "F1" },
{ id: "R1" },
{ id: "F1", type: "frame", isSelected: true },
{ id: "R2" },
{ id: "F1_2", frameId: "F1" },
{ id: "R3" },
],
operations: [
// -1
[actionSendBackward, ["F1_1", "F1", "R1", "F1_2", "R2", "R3"]],
// -1
[actionSendBackward, ["F1_1", "F1", "F1_2", "R1", "R2", "R3"]],
],
});
});
it("moving selected frame children by one (normalized)", () => {
// normalized frame order
assertZindex({
elements: [
{ id: "F1_1", frameId: "F1", isSelected: true },
{ id: "F1_2", frameId: "F1" },
{ id: "F1", type: "frame" },
{ id: "R1" },
],
operations: [
// +1
[actionBringForward, ["F1_2", "F1_1", "F1", "R1"]],
// noop
[actionBringForward, ["F1_2", "F1_1", "F1", "R1"]],
],
});
// normalized frame order, multiple frames
assertZindex({
elements: [
{ id: "F1_1", frameId: "F1", isSelected: true },
{ id: "F1_2", frameId: "F1" },
{ id: "F1", type: "frame" },
{ id: "R1" },
{ id: "F2_1", frameId: "F2", isSelected: true },
{ id: "F2_2", frameId: "F2" },
{ id: "F2", type: "frame" },
{ id: "R2" },
],
operations: [
// +1
[
actionBringForward,
["F1_2", "F1_1", "F1", "R1", "F2_2", "F2_1", "F2", "R2"],
],
// noop
[
actionBringForward,
["F1_2", "F1_1", "F1", "R1", "F2_2", "F2_1", "F2", "R2"],
],
],
});
});
it("moving selected frame children by one (DENORMALIZED)", () => {
// DENORMALIZED FRAME ORDER
assertZindex({
elements: [
{ id: "F1_1", frameId: "F1", isSelected: true },
{ id: "F1", type: "frame" },
{ id: "F1_2", frameId: "F1" },
{ id: "R1" },
],
operations: [
// +1
// NOTE not sure what we wanna do here
[actionBringForward, ["F1", "F1_2", "F1_1", "R1"]],
// noop
[actionBringForward, ["F1", "F1_2", "F1_1", "R1"]],
// -1
[actionSendBackward, ["F1", "F1_1", "F1_2", "R1"]],
// noop
[actionSendBackward, ["F1", "F1_1", "F1_2", "R1"]],
],
});
// DENORMALIZED FRAME ORDER
assertZindex({
elements: [
{ id: "F1_1", frameId: "F1", isSelected: true },
{ id: "R1" },
{ id: "F1", type: "frame" },
{ id: "F1_2", frameId: "F1" },
{ id: "R2" },
],
operations: [
// +1
// NOTE not sure what we wanna do here
[actionBringForward, ["R1", "F1", "F1_2", "F1_1", "R2"]],
// noop
[actionBringForward, ["R1", "F1", "F1_2", "F1_1", "R2"]],
// -1
[actionSendBackward, ["R1", "F1", "F1_1", "F1_2", "R2"]],
// noop
[actionSendBackward, ["R1", "F1", "F1_1", "F1_2", "R2"]],
],
});
});
it("moving whole frame to front/end", () => {
// normalized frame order
assertZindex({
elements: [
{ id: "F1_1", frameId: "F1" },
{ id: "F1_2", frameId: "F1" },
{ id: "F1", type: "frame", isSelected: true },
{ id: "R1" },
{ id: "R2" },
],
operations: [
// +∞
[actionBringToFront, ["R1", "R2", "F1_1", "F1_2", "F1"]],
// noop
[actionBringToFront, ["R1", "R2", "F1_1", "F1_2", "F1"]],
// -∞
[actionSendToBack, ["F1_1", "F1_2", "F1", "R1", "R2"]],
// noop
[actionSendToBack, ["F1_1", "F1_2", "F1", "R1", "R2"]],
],
});
// DENORMALIZED FRAME ORDER
assertZindex({
elements: [
{ id: "F1_1", frameId: "F1" },
{ id: "F1", type: "frame", isSelected: true },
{ id: "F1_2", frameId: "F1" },
{ id: "R1" },
{ id: "R2" },
],
operations: [
// +∞
[actionBringToFront, ["R1", "R2", "F1_1", "F1", "F1_2"]],
// noop
[actionBringToFront, ["R1", "R2", "F1_1", "F1", "F1_2"]],
// -∞
[actionSendToBack, ["F1_1", "F1", "F1_2", "R1", "R2"]],
// noop
[actionSendToBack, ["F1_1", "F1", "F1_2", "R1", "R2"]],
],
});
// DENORMALIZED FRAME ORDER
assertZindex({
elements: [
{ id: "F1_1", frameId: "F1" },
{ id: "F1", type: "frame", isSelected: true },
{ id: "R1" },
{ id: "F1_2", frameId: "F1" },
{ id: "R2" },
],
operations: [
// +∞
[actionBringToFront, ["R1", "R2", "F1_1", "F1", "F1_2"]],
],
});
// DENORMALIZED FRAME ORDER
assertZindex({
elements: [
{ id: "F1_1", frameId: "F1" },
{ id: "R1" },
{ id: "F1", type: "frame", isSelected: true },
{ id: "R2" },
{ id: "F1_2", frameId: "F1" },
{ id: "R3" },
],
operations: [
// +1
[actionBringToFront, ["R1", "R2", "R3", "F1_1", "F1", "F1_2"]],
],
});
});
});

@ -1,16 +1,14 @@
import { bumpVersion } from "./element/mutateElement";
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 { getSelectedElements } from "./scene";
import Scene from "./scene/Scene";
import { AppState } from "./types";
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
: getSelectedElements(elements, appState, {
includeBoundTextElement: true,
includeElementsInFrames: true,
}),
);
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
* is a non-deleted element, and not inside a group (unless we're editing it).
@ -115,6 +134,11 @@ const getTargetIndex = (
elements: readonly ExcalidrawElement[],
boundaryIndex: number,
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];
@ -122,6 +146,9 @@ const getTargetIndex = (
if (element.isDeleted) {
return false;
}
if (containingFrame) {
return element.frameId === containingFrame;
}
// if we're editing group, find closest sibling irrespective of whether
// there's a different-group element between them (for legacy reasons)
if (appState.editingGroupId) {
@ -132,8 +159,12 @@ const getTargetIndex = (
const candidateIndex =
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];
@ -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) {
return (
getTargetIndexAccountingForBinding(nextElement, elements, direction) ??
@ -195,13 +239,12 @@ const getTargetElementsMap = <T extends ExcalidrawElement>(
}, {} as Record<string, ExcalidrawElement>);
};
const _shiftElements = (
const shiftElementsByOne = (
elements: readonly ExcalidrawElement[],
appState: AppState,
direction: "left" | "right",
elementsToBeMoved?: readonly ExcalidrawElement[],
) => {
const indicesToMove = getIndicesToMove(elements, appState, elementsToBeMoved);
const indicesToMove = getIndicesToMove(elements, appState);
const targetElementsMap = getTargetElementsMap(elements, indicesToMove);
let groupedIndices = toContiguousGroups(indicesToMove);
@ -209,16 +252,30 @@ const _shiftElements = (
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) => {
const leadingIndex = indices[0];
const trailingIndex = indices[indices.length - 1];
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(
appState,
elements,
boundaryIndex,
direction,
containingFrame,
);
if (targetIndex === -1 || boundaryIndex === targetIndex) {
@ -263,34 +320,25 @@ const _shiftElements = (
});
};
const shiftElements = (
appState: AppState,
const shiftElementsToEnd = (
elements: readonly ExcalidrawElement[],
appState: AppState,
direction: "left" | "right",
containingFrame: ExcalidrawFrameElement["id"] | null,
elementsToBeMoved?: readonly ExcalidrawElement[],
) => {
return shift(
elements,
appState,
direction,
_shiftElements,
elementsToBeMoved,
);
};
const _shiftElementsToEnd = (
elements: readonly ExcalidrawElement[],
appState: AppState,
direction: "left" | "right",
) => {
const indicesToMove = getIndicesToMove(elements, appState);
const indicesToMove = getIndicesToMove(elements, appState, elementsToBeMoved);
const targetElementsMap = getTargetElementsMap(elements, indicesToMove);
const displacedElements: ExcalidrawElement[] = [];
let leadingIndex: number;
let trailingIndex: number;
if (direction === "left") {
if (appState.editingGroupId) {
if (containingFrame) {
leadingIndex = findIndex(elements, (el) =>
isOfTargetFrame(el, containingFrame),
);
} else if (appState.editingGroupId) {
const groupElements = getElementsInGroup(
elements,
appState.editingGroupId,
@ -305,7 +353,11 @@ const _shiftElementsToEnd = (
trailingIndex = indicesToMove[indicesToMove.length - 1];
} else {
if (appState.editingGroupId) {
if (containingFrame) {
trailingIndex = findLastIndex(elements, (el) =>
isOfTargetFrame(el, containingFrame),
);
} else if (appState.editingGroupId) {
const groupElements = getElementsInGroup(
elements,
appState.editingGroupId,
@ -321,6 +373,10 @@ const _shiftElementsToEnd = (
leadingIndex = indicesToMove[0];
}
if (leadingIndex === -1) {
leadingIndex = 0;
}
for (let index = leadingIndex; index < trailingIndex + 1; index++) {
if (!indicesToMove.includes(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,
direction: "left" | "right",
shiftFunction: (
elements: ExcalidrawElement[],
elements: readonly ExcalidrawElement[],
appState: AppState,
direction: "left" | "right",
containingFrame: ExcalidrawFrameElement["id"] | null,
elementsToBeMoved?: 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,
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,
);
}
}
}
let nextElements = allElements;
const frameChildrenSets = Array.from(
frameAwareContiguousElementsToMove.frameChildren.entries(),
);
for (const [frameId, children] of frameChildrenSets) {
nextElements = shiftFunction(
allElements,
appState,
direction,
frameId,
children,
);
}
return shiftFunction(
nextElements,
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[],
);
}
});
// return the final elements
let finalElements: ExcalidrawElement[] = [];
rootElements.forEach((element) => {
if (isFrameElement(element)) {
finalElements = [
...finalElements,
...(frameElementsMap.get(element.id) ?? []),
element,
];
} else {
finalElements = [...finalElements, element];
}
});
return finalElements;
null,
frameAwareContiguousElementsToMove.regularElements,
);
}
// public API
// -----------------------------------------------------------------------------
export const moveOneLeft = (
elements: readonly ExcalidrawElement[],
allElements: readonly ExcalidrawElement[],
appState: AppState,
elementsToBeMoved?: readonly ExcalidrawElement[],
) => {
return shiftElements(appState, elements, "left", elementsToBeMoved);
return shiftElementsByOne(allElements, appState, "left");
};
export const moveOneRight = (
elements: readonly ExcalidrawElement[],
allElements: readonly ExcalidrawElement[],
appState: AppState,
elementsToBeMoved?: readonly ExcalidrawElement[],
) => {
return shiftElements(appState, elements, "right", elementsToBeMoved);
return shiftElementsByOne(allElements, appState, "right");
};
export const moveAllLeft = (
elements: readonly ExcalidrawElement[],
allElements: readonly ExcalidrawElement[],
appState: AppState,
elementsToBeMoved?: readonly ExcalidrawElement[],
) => {
return shiftElementsToEnd(elements, appState, "left", elementsToBeMoved);
return shiftElementsAccountingForFrames(
allElements,
appState,
"left",
shiftElementsToEnd,
);
};
export const moveAllRight = (
elements: readonly ExcalidrawElement[],
allElements: readonly ExcalidrawElement[],
appState: AppState,
elementsToBeMoved?: readonly ExcalidrawElement[],
) => {
return shiftElementsToEnd(elements, appState, "right", elementsToBeMoved);
return shiftElementsAccountingForFrames(
allElements,
appState,
"right",
shiftElementsToEnd,
);
};