mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-05-03 10:00:07 -04:00
fix: frame add/remove/z-index ordering changes (#7194)
This commit is contained in:
parent
f098789d16
commit
0f81c30276
5 changed files with 512 additions and 212 deletions
276
src/zindex.ts
276
src/zindex.ts
|
@ -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,
|
||||
);
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue