fix zindex to account for group boundaries (#2065)

This commit is contained in:
David Luzar 2020-09-11 17:06:07 +02:00 committed by GitHub
parent ea020f2c50
commit d07099aadd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 1287 additions and 416 deletions

View file

@ -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";
/**
* 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 = [];
}
}
return selectedIndices;
};
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) {
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[][]);
};
/**
* 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;
}
// 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;
};
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;
}
swap(elements, index - 1, index);
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);
elements =
direction === "left"
? [
...leadingElements,
...targetElements,
...displacedElements,
...trailingElements,
]
: [
...leadingElements,
...displacedElements,
...targetElements,
...trailingElements,
];
});
return elements;
};
export const moveOneRight = <T>(elements: T[], indicesToMove: number[]) => {
const reversedIndicesToMove = indicesToMove.sort(
(a: number, b: number) => b - a,
);
let isSorted = true;
const shiftElementsToEnd = (
elements: readonly ExcalidrawElement[],
appState: AppState,
direction: "left" | "right",
) => {
const indicesToMove = getIndicesToMove(elements, appState);
const targetElements: ExcalidrawElement[] = [];
const displacedElements: ExcalidrawElement[] = [];
// 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;
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;
}
swap(elements, index + 1, index);
});
return elements;
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;
}
leadingIndex = indicesToMove[0];
}
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,
];
};
// 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);
// public API
// -----------------------------------------------------------------------------
// 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) {
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];
}
});
// The final step
leftMostElements.forEach((element, i) => {
elements[i] = element;
});
return elements;
export const moveOneLeft = (
elements: readonly ExcalidrawElement[],
appState: AppState,
) => {
return shiftElements(appState, elements.slice(), "left");
};
// 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;
}
// 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];
}
});
// The final step
rightMostElements.forEach((element, i) => {
elements[elements.length - i - 1] = element;
});
return elements;
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");
};