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

View file

@ -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,
);
};