fix zindex to account for group boundaries (#2065)
parent
ea020f2c50
commit
d07099aadd
File diff suppressed because it is too large
Load Diff
@ -1,101 +0,0 @@
|
||||
import { moveOneLeft, moveOneRight, moveAllLeft, moveAllRight } from "./zindex";
|
||||
|
||||
const expectMove = <T>(
|
||||
fn: (elements: T[], indicesToMove: number[]) => void,
|
||||
elems: T[],
|
||||
indices: number[],
|
||||
equal: T[],
|
||||
) => {
|
||||
fn(elems, indices);
|
||||
expect(elems).toEqual(equal);
|
||||
};
|
||||
|
||||
it("should moveOneLeft", () => {
|
||||
expectMove(moveOneLeft, ["a", "b", "c", "d"], [1, 2], ["b", "c", "a", "d"]);
|
||||
expectMove(moveOneLeft, ["a", "b", "c", "d"], [0], ["a", "b", "c", "d"]);
|
||||
expectMove(
|
||||
moveOneLeft,
|
||||
["a", "b", "c", "d"],
|
||||
[0, 1, 2, 3],
|
||||
["a", "b", "c", "d"],
|
||||
);
|
||||
expectMove(moveOneLeft, ["a", "b", "c", "d"], [1, 3], ["b", "a", "d", "c"]);
|
||||
});
|
||||
|
||||
it("should moveOneRight", () => {
|
||||
expectMove(moveOneRight, ["a", "b", "c", "d"], [1, 2], ["a", "d", "b", "c"]);
|
||||
expectMove(moveOneRight, ["a", "b", "c", "d"], [3], ["a", "b", "c", "d"]);
|
||||
expectMove(
|
||||
moveOneRight,
|
||||
["a", "b", "c", "d"],
|
||||
[0, 1, 2, 3],
|
||||
["a", "b", "c", "d"],
|
||||
);
|
||||
expectMove(moveOneRight, ["a", "b", "c", "d"], [0, 2], ["b", "a", "d", "c"]);
|
||||
});
|
||||
|
||||
it("should moveAllLeft", () => {
|
||||
expectMove(
|
||||
moveAllLeft,
|
||||
["a", "b", "c", "d", "e", "f", "g"],
|
||||
[2, 5],
|
||||
["c", "f", "a", "b", "d", "e", "g"],
|
||||
);
|
||||
expectMove(
|
||||
moveAllLeft,
|
||||
["a", "b", "c", "d", "e", "f", "g"],
|
||||
[5],
|
||||
["f", "a", "b", "c", "d", "e", "g"],
|
||||
);
|
||||
expectMove(
|
||||
moveAllLeft,
|
||||
["a", "b", "c", "d", "e", "f", "g"],
|
||||
[0, 1, 2, 3, 4, 5, 6],
|
||||
["a", "b", "c", "d", "e", "f", "g"],
|
||||
);
|
||||
expectMove(
|
||||
moveAllLeft,
|
||||
["a", "b", "c", "d", "e", "f", "g"],
|
||||
[0, 1, 2],
|
||||
["a", "b", "c", "d", "e", "f", "g"],
|
||||
);
|
||||
expectMove(
|
||||
moveAllLeft,
|
||||
["a", "b", "c", "d", "e", "f", "g"],
|
||||
[4, 5, 6],
|
||||
["e", "f", "g", "a", "b", "c", "d"],
|
||||
);
|
||||
});
|
||||
|
||||
it("should moveAllRight", () => {
|
||||
expectMove(
|
||||
moveAllRight,
|
||||
["a", "b", "c", "d", "e", "f", "g"],
|
||||
[2, 5],
|
||||
["a", "b", "d", "e", "g", "c", "f"],
|
||||
);
|
||||
expectMove(
|
||||
moveAllRight,
|
||||
["a", "b", "c", "d", "e", "f", "g"],
|
||||
[5],
|
||||
["a", "b", "c", "d", "e", "g", "f"],
|
||||
);
|
||||
expectMove(
|
||||
moveAllRight,
|
||||
["a", "b", "c", "d", "e", "f", "g"],
|
||||
[0, 1, 2, 3, 4, 5, 6],
|
||||
["a", "b", "c", "d", "e", "f", "g"],
|
||||
);
|
||||
expectMove(
|
||||
moveAllRight,
|
||||
["a", "b", "c", "d", "e", "f", "g"],
|
||||
[0, 1, 2],
|
||||
["d", "e", "f", "g", "a", "b", "c"],
|
||||
);
|
||||
expectMove(
|
||||
moveAllRight,
|
||||
["a", "b", "c", "d", "e", "f", "g"],
|
||||
[4, 5, 6],
|
||||
["a", "b", "c", "d", "e", "f", "g"],
|
||||
);
|
||||
});
|
@ -1,202 +1,278 @@
|
||||
const swap = <T>(elements: T[], indexA: number, indexB: number) => {
|
||||
const element = elements[indexA];
|
||||
elements[indexA] = elements[indexB];
|
||||
elements[indexB] = element;
|
||||
};
|
||||
import { AppState } from "./types";
|
||||
import { ExcalidrawElement } from "./element/types";
|
||||
import { getElementsInGroup } from "./groups";
|
||||
import { findLastIndex, findIndex } from "./utils";
|
||||
|
||||
export const moveOneLeft = <T>(elements: T[], indicesToMove: number[]) => {
|
||||
indicesToMove.sort((a: number, b: number) => a - b);
|
||||
let isSorted = true;
|
||||
// We go from left to right to avoid overriding the wrong elements
|
||||
indicesToMove.forEach((index, i) => {
|
||||
// We don't want to bubble the first elements that are sorted as they are
|
||||
// already in their correct position
|
||||
isSorted = isSorted && index === i;
|
||||
if (isSorted) {
|
||||
return;
|
||||
/**
|
||||
* Returns indices of elements to move based on selected elements.
|
||||
* Includes contiguous deleted elements that are between two selected elements,
|
||||
* e.g.: [0 (selected), 1 (deleted), 2 (deleted), 3 (selected)]
|
||||
*/
|
||||
const getIndicesToMove = (
|
||||
elements: readonly ExcalidrawElement[],
|
||||
appState: AppState,
|
||||
) => {
|
||||
let selectedIndices: number[] = [];
|
||||
let deletedIndices: number[] = [];
|
||||
let includeDeletedIndex = null;
|
||||
let i = -1;
|
||||
while (++i < elements.length) {
|
||||
if (appState.selectedElementIds[elements[i].id]) {
|
||||
if (deletedIndices.length) {
|
||||
selectedIndices = selectedIndices.concat(deletedIndices);
|
||||
deletedIndices = [];
|
||||
}
|
||||
selectedIndices.push(i);
|
||||
includeDeletedIndex = i + 1;
|
||||
} else if (elements[i].isDeleted && includeDeletedIndex === i) {
|
||||
includeDeletedIndex = i + 1;
|
||||
deletedIndices.push(i);
|
||||
} else {
|
||||
deletedIndices = [];
|
||||
}
|
||||
swap(elements, index - 1, index);
|
||||
});
|
||||
}
|
||||
return selectedIndices;
|
||||
};
|
||||
|
||||
return elements;
|
||||
const toContiguousGroups = (array: number[]) => {
|
||||
let cursor = 0;
|
||||
return array.reduce((acc, value, index) => {
|
||||
if (index > 0 && array[index - 1] !== value - 1) {
|
||||
cursor = ++cursor;
|
||||
}
|
||||
(acc[cursor] || (acc[cursor] = [])).push(value);
|
||||
return acc;
|
||||
}, [] as number[][]);
|
||||
};
|
||||
|
||||
export const moveOneRight = <T>(elements: T[], indicesToMove: number[]) => {
|
||||
const reversedIndicesToMove = indicesToMove.sort(
|
||||
(a: number, b: number) => b - a,
|
||||
);
|
||||
let isSorted = true;
|
||||
|
||||
// We go from right to left to avoid overriding the wrong elements
|
||||
reversedIndicesToMove.forEach((index, i) => {
|
||||
// We don't want to bubble the first elements that are sorted as they are
|
||||
// already in their correct position
|
||||
isSorted = isSorted && index === elements.length - i - 1;
|
||||
if (isSorted) {
|
||||
return;
|
||||
/**
|
||||
* 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).
|
||||
*/
|
||||
const getTargetIndex = (
|
||||
appState: AppState,
|
||||
elements: ExcalidrawElement[],
|
||||
boundaryIndex: number,
|
||||
direction: "left" | "right",
|
||||
) => {
|
||||
const sourceElement = elements[boundaryIndex];
|
||||
|
||||
const indexFilter = (element: ExcalidrawElement) => {
|
||||
if (element.isDeleted) {
|
||||
return false;
|
||||
}
|
||||
swap(elements, index + 1, index);
|
||||
});
|
||||
return elements;
|
||||
// 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) {
|
||||
return element.groupIds.includes(appState.editingGroupId);
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const candidateIndex =
|
||||
direction === "left"
|
||||
? findLastIndex(elements, indexFilter, Math.max(0, boundaryIndex - 1))
|
||||
: findIndex(elements, indexFilter, boundaryIndex + 1);
|
||||
|
||||
const nextElement = elements[candidateIndex];
|
||||
|
||||
if (!nextElement) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (appState.editingGroupId) {
|
||||
if (
|
||||
// candidate element is a sibling in current editing group → return
|
||||
sourceElement?.groupIds.join("") === nextElement?.groupIds.join("")
|
||||
) {
|
||||
return candidateIndex;
|
||||
} else if (!nextElement?.groupIds.includes(appState.editingGroupId)) {
|
||||
// candidate element is outside current editing group → prevent
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
if (!nextElement.groupIds.length) {
|
||||
return candidateIndex;
|
||||
}
|
||||
|
||||
const siblingGroupId = appState.editingGroupId
|
||||
? nextElement.groupIds[
|
||||
nextElement.groupIds.indexOf(appState.editingGroupId) - 1
|
||||
]
|
||||
: nextElement.groupIds[nextElement.groupIds.length - 1];
|
||||
|
||||
const elementsInSiblingGroup = getElementsInGroup(elements, siblingGroupId);
|
||||
|
||||
if (elementsInSiblingGroup.length) {
|
||||
// assumes getElementsInGroup() returned elements are sorted
|
||||
// by zIndex (ascending)
|
||||
return direction === "left"
|
||||
? elements.indexOf(elementsInSiblingGroup[0])
|
||||
: elements.indexOf(
|
||||
elementsInSiblingGroup[elementsInSiblingGroup.length - 1],
|
||||
);
|
||||
}
|
||||
|
||||
return candidateIndex;
|
||||
};
|
||||
|
||||
// Let's go through an example
|
||||
// | |
|
||||
// [a, b, c, d, e, f, g]
|
||||
// -->
|
||||
// [c, f, a, b, d, e, g]
|
||||
//
|
||||
// We are going to override all the elements we want to move, so we keep them in an array
|
||||
// that we will restore at the end.
|
||||
// [c, f]
|
||||
//
|
||||
// From now on, we'll never read those values from the array anymore
|
||||
// |1 |0
|
||||
// [a, b, _, d, e, _, g]
|
||||
//
|
||||
// The idea is that we want to shift all the elements between the marker 0 and 1
|
||||
// by one slot to the right.
|
||||
//
|
||||
// |1 |0
|
||||
// [a, b, _, d, e, _, g]
|
||||
// -> ->
|
||||
//
|
||||
// which gives us
|
||||
//
|
||||
// |1 |0
|
||||
// [a, b, _, _, d, e, g]
|
||||
//
|
||||
// Now, we need to move all the elements from marker 1 to the beginning by two (not one)
|
||||
// slots to the right, which gives us
|
||||
//
|
||||
// |1 |0
|
||||
// [a, b, _, _, d, e, g]
|
||||
// ---|--^ ^
|
||||
// ------|
|
||||
//
|
||||
// which gives us
|
||||
//
|
||||
// |1 |0
|
||||
// [_, _, a, b, d, e, g]
|
||||
//
|
||||
// At this point, we can fill back the leftmost elements with the array we saved at
|
||||
// the beginning
|
||||
//
|
||||
// |1 |0
|
||||
// [c, f, a, b, d, e, g]
|
||||
//
|
||||
// And we are done!
|
||||
export const moveAllLeft = <T>(elements: T[], indicesToMove: number[]) => {
|
||||
indicesToMove.sort((a: number, b: number) => a - b);
|
||||
|
||||
// Copy the elements to move
|
||||
const leftMostElements = indicesToMove.map((index) => elements[index]);
|
||||
|
||||
const reversedIndicesToMove = indicesToMove
|
||||
// We go from right to left to avoid overriding elements.
|
||||
.reverse()
|
||||
// We add 0 for the final marker
|
||||
.concat([0]);
|
||||
|
||||
reversedIndicesToMove.forEach((index, i) => {
|
||||
// We skip the first one as it is not paired with anything else
|
||||
if (i === 0) {
|
||||
const shiftElements = (
|
||||
appState: AppState,
|
||||
elements: ExcalidrawElement[],
|
||||
direction: "left" | "right",
|
||||
) => {
|
||||
const indicesToMove = getIndicesToMove(elements, appState);
|
||||
let groupedIndices = toContiguousGroups(indicesToMove);
|
||||
|
||||
if (direction === "right") {
|
||||
groupedIndices = groupedIndices.reverse();
|
||||
}
|
||||
|
||||
groupedIndices.forEach((indices, i) => {
|
||||
const leadingIndex = indices[0];
|
||||
const trailingIndex = indices[indices.length - 1];
|
||||
const boundaryIndex = direction === "left" ? leadingIndex : trailingIndex;
|
||||
|
||||
const targetIndex = getTargetIndex(
|
||||
appState,
|
||||
elements,
|
||||
boundaryIndex,
|
||||
direction,
|
||||
);
|
||||
|
||||
if (targetIndex === -1 || boundaryIndex === targetIndex) {
|
||||
return;
|
||||
}
|
||||
|
||||
// We go from the next marker to the right (i - 1) to the current one (index)
|
||||
for (let pos = reversedIndicesToMove[i - 1] - 1; pos >= index; --pos) {
|
||||
// We move by 1 the first time, 2 the second... So we can use the index i in the array
|
||||
elements[pos + i] = elements[pos];
|
||||
}
|
||||
});
|
||||
const leadingElements =
|
||||
direction === "left"
|
||||
? elements.slice(0, targetIndex)
|
||||
: elements.slice(0, leadingIndex);
|
||||
const targetElements = elements.slice(leadingIndex, trailingIndex + 1);
|
||||
const displacedElements =
|
||||
direction === "left"
|
||||
? elements.slice(targetIndex, leadingIndex)
|
||||
: elements.slice(trailingIndex + 1, targetIndex + 1);
|
||||
const trailingElements =
|
||||
direction === "left"
|
||||
? elements.slice(trailingIndex + 1)
|
||||
: elements.slice(targetIndex + 1);
|
||||
|
||||
// The final step
|
||||
leftMostElements.forEach((element, i) => {
|
||||
elements[i] = element;
|
||||
elements =
|
||||
direction === "left"
|
||||
? [
|
||||
...leadingElements,
|
||||
...targetElements,
|
||||
...displacedElements,
|
||||
...trailingElements,
|
||||
]
|
||||
: [
|
||||
...leadingElements,
|
||||
...displacedElements,
|
||||
...targetElements,
|
||||
...trailingElements,
|
||||
];
|
||||
});
|
||||
|
||||
return elements;
|
||||
};
|
||||
|
||||
// Let's go through an example
|
||||
// | |
|
||||
// [a, b, c, d, e, f, g]
|
||||
// -->
|
||||
// [a, b, d, e, g, c, f]
|
||||
//
|
||||
// We are going to override all the elements we want to move, so we keep them in an array
|
||||
// that we will restore at the end.
|
||||
// [c, f]
|
||||
//
|
||||
// From now on, we'll never read those values from the array anymore
|
||||
// |0 |1
|
||||
// [a, b, _, d, e, _, g]
|
||||
//
|
||||
// The idea is that we want to shift all the elements between the marker 0 and 1
|
||||
// by one slot to the left.
|
||||
//
|
||||
// |0 |1
|
||||
// [a, b, _, d, e, _, g]
|
||||
// <- <-
|
||||
//
|
||||
// which gives us
|
||||
//
|
||||
// |0 |1
|
||||
// [a, b, d, e, _, _, g]
|
||||
//
|
||||
// Now, we need to move all the elements from marker 1 to the end by two (not one)
|
||||
// slots to the left, which gives us
|
||||
//
|
||||
// |0 |1
|
||||
// [a, b, d, e, _, _, g]
|
||||
// ^------
|
||||
//
|
||||
// which gives us
|
||||
//
|
||||
// |0 |1
|
||||
// [a, b, d, e, g, _, _]
|
||||
//
|
||||
// At this point, we can fill back the rightmost elements with the array we saved at
|
||||
// the beginning
|
||||
//
|
||||
// |0 |1
|
||||
// [a, b, d, e, g, c, f]
|
||||
//
|
||||
// And we are done!
|
||||
export const moveAllRight = <T>(elements: T[], indicesToMove: number[]) => {
|
||||
const reversedIndicesToMove = indicesToMove.sort(
|
||||
(a: number, b: number) => b - a,
|
||||
);
|
||||
|
||||
// Copy the elements to move
|
||||
const rightMostElements = reversedIndicesToMove.map(
|
||||
(index) => elements[index],
|
||||
);
|
||||
|
||||
indicesToMove = reversedIndicesToMove
|
||||
// We go from left to right to avoid overriding elements.
|
||||
.reverse()
|
||||
// We last element index for the final marker
|
||||
.concat([elements.length]);
|
||||
|
||||
indicesToMove.forEach((index, i) => {
|
||||
// We skip the first one as it is not paired with anything else
|
||||
if (i === 0) {
|
||||
return;
|
||||
const shiftElementsToEnd = (
|
||||
elements: readonly ExcalidrawElement[],
|
||||
appState: AppState,
|
||||
direction: "left" | "right",
|
||||
) => {
|
||||
const indicesToMove = getIndicesToMove(elements, appState);
|
||||
const targetElements: ExcalidrawElement[] = [];
|
||||
const displacedElements: ExcalidrawElement[] = [];
|
||||
|
||||
let leadingIndex, trailingIndex;
|
||||
if (direction === "left") {
|
||||
if (appState.editingGroupId) {
|
||||
const groupElements = getElementsInGroup(
|
||||
elements,
|
||||
appState.editingGroupId,
|
||||
);
|
||||
if (!groupElements.length) {
|
||||
return elements;
|
||||
}
|
||||
leadingIndex = elements.indexOf(groupElements[0]);
|
||||
} else {
|
||||
leadingIndex = 0;
|
||||
}
|
||||
|
||||
// We go from the next marker to the left (i - 1) to the current one (index)
|
||||
for (let pos = indicesToMove[i - 1] + 1; pos < index; ++pos) {
|
||||
// We move by 1 the first time, 2 the second... So we can use the index i in the array
|
||||
elements[pos - i] = elements[pos];
|
||||
trailingIndex = indicesToMove[indicesToMove.length - 1];
|
||||
} else {
|
||||
if (appState.editingGroupId) {
|
||||
const groupElements = getElementsInGroup(
|
||||
elements,
|
||||
appState.editingGroupId,
|
||||
);
|
||||
if (!groupElements.length) {
|
||||
return elements;
|
||||
}
|
||||
trailingIndex = elements.indexOf(groupElements[groupElements.length - 1]);
|
||||
} else {
|
||||
trailingIndex = elements.length - 1;
|
||||
}
|
||||
});
|
||||
|
||||
// The final step
|
||||
rightMostElements.forEach((element, i) => {
|
||||
elements[elements.length - i - 1] = element;
|
||||
});
|
||||
leadingIndex = indicesToMove[0];
|
||||
}
|
||||
|
||||
return elements;
|
||||
for (let index = leadingIndex; index < trailingIndex + 1; index++) {
|
||||
if (indicesToMove.includes(index)) {
|
||||
targetElements.push(elements[index]);
|
||||
} else {
|
||||
displacedElements.push(elements[index]);
|
||||
}
|
||||
}
|
||||
|
||||
const leadingElements = elements.slice(0, leadingIndex);
|
||||
const trailingElements = elements.slice(trailingIndex + 1);
|
||||
|
||||
return direction === "left"
|
||||
? [
|
||||
...leadingElements,
|
||||
...targetElements,
|
||||
...displacedElements,
|
||||
...trailingElements,
|
||||
]
|
||||
: [
|
||||
...leadingElements,
|
||||
...displacedElements,
|
||||
...targetElements,
|
||||
...trailingElements,
|
||||
];
|
||||
};
|
||||
|
||||
// public API
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
export const moveOneLeft = (
|
||||
elements: readonly ExcalidrawElement[],
|
||||
appState: AppState,
|
||||
) => {
|
||||
return shiftElements(appState, elements.slice(), "left");
|
||||
};
|
||||
|
||||
export const moveOneRight = (
|
||||
elements: readonly ExcalidrawElement[],
|
||||
appState: AppState,
|
||||
) => {
|
||||
return shiftElements(appState, elements.slice(), "right");
|
||||
};
|
||||
|
||||
export const moveAllLeft = (
|
||||
elements: readonly ExcalidrawElement[],
|
||||
appState: AppState,
|
||||
) => {
|
||||
return shiftElementsToEnd(elements, appState, "left");
|
||||
};
|
||||
|
||||
export const moveAllRight = (
|
||||
elements: readonly ExcalidrawElement[],
|
||||
appState: AppState,
|
||||
) => {
|
||||
return shiftElementsToEnd(elements, appState, "right");
|
||||
};
|
||||
|
Loading…
Reference in New Issue