excalidraw/src/zindex.ts

279 lines
7.6 KiB
TypeScript

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;
};
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;
}
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;
};
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;
}
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,
];
};
// 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");
};