diff --git a/src/data/restore.ts b/src/data/restore.ts index 06aed01136..3e865da182 100644 --- a/src/data/restore.ts +++ b/src/data/restore.ts @@ -43,6 +43,7 @@ import { measureBaseline, } from "../element/textElement"; import { normalizeLink } from "./url"; +import { normalizeFractionalIndicies } from "../fractionalIndex"; type RestoredAppState = Omit< AppState, @@ -460,7 +461,7 @@ export const restoreElements = ( } } - return restoredElements; + return normalizeFractionalIndicies(restoredElements) as ExcalidrawElement[]; }; const coalesceAppStateValue = < diff --git a/src/fractionalIndex.ts b/src/fractionalIndex.ts index 9320379944..e31806264d 100644 --- a/src/fractionalIndex.ts +++ b/src/fractionalIndex.ts @@ -1,6 +1,6 @@ import { mutateElement } from "./element/mutateElement"; import { ExcalidrawElement } from "./element/types"; -import { generateKeyBetween } from "fractional-indexing"; +import { generateKeyBetween, generateNKeysBetween } from "fractional-indexing"; type FractionalIndex = ExcalidrawElement["fractionalIndex"]; @@ -28,6 +28,80 @@ const isValidFractionalIndex = ( return false; }; +const getContiguousMovedIndices = ( + elements: readonly ExcalidrawElement[], + movedElementsMap: Record, +) => { + const result: number[][] = []; + const contiguous: number[] = []; + + for (let i = 0; i < elements.length; i++) { + const element = elements[i]; + if (movedElementsMap[element.id]) { + if (contiguous.length) { + if (contiguous[contiguous.length - 1] + 1 === i) { + contiguous.push(i); + } else { + result.push(contiguous.slice()); + contiguous.length = 0; + contiguous.push(i); + } + } else { + contiguous.push(i); + } + } + } + + if (contiguous.length > 0) { + result.push(contiguous.slice()); + } + + return result; +}; + +export const fixFractionalIndices = ( + elements: readonly ExcalidrawElement[], + movedElementsMap: Record, +) => { + const fixedElements = elements.slice(); + const contiguousMovedIndices = getContiguousMovedIndices( + fixedElements, + movedElementsMap, + ); + + for (const movedIndices of contiguousMovedIndices) { + try { + const predecessor = + fixedElements[movedIndices[0] - 1]?.fractionalIndex || null; + const successor = + fixedElements[movedIndices[movedIndices.length - 1] + 1] + ?.fractionalIndex || null; + + const newKeys = generateNKeysBetween( + predecessor, + successor, + movedIndices.length, + ); + + for (let i = 0; i < movedIndices.length; i++) { + const element = fixedElements[movedIndices[i]]; + + mutateElement( + element, + { + fractionalIndex: newKeys[i], + }, + false, + ); + } + } catch (e) { + console.error("error generating fractional indices", e); + } + } + + return fixedElements; +}; + const generateFractionalIndex = ( index: FractionalIndex, predecessor: FractionalIndex, diff --git a/src/scene/Scene.ts b/src/scene/Scene.ts index 9399a15c3d..66b60c9bfd 100644 --- a/src/scene/Scene.ts +++ b/src/scene/Scene.ts @@ -315,11 +315,11 @@ class Scene { "insertElementAtIndex can only be called with index >= 0", ); } - const nextElements = [ + const nextElements = normalizeFractionalIndicies([ ...this.elements.slice(0, index), ...elements, ...this.elements.slice(index), - ]; + ]); this.replaceAllElements(nextElements); } @@ -328,7 +328,9 @@ class Scene { if (element.frameId) { this.insertElementAtIndex(element, this.getElementIndex(element.frameId)); } else { - this.replaceAllElements([...this.elements, element]); + this.replaceAllElements( + normalizeFractionalIndicies([...this.elements, element]), + ); } }; diff --git a/src/zindex.ts b/src/zindex.ts index a46c8b9a7c..aec2e30f53 100644 --- a/src/zindex.ts +++ b/src/zindex.ts @@ -1,6 +1,7 @@ import { bumpVersion } from "./element/mutateElement"; import { isFrameLikeElement } from "./element/typeChecks"; import { ExcalidrawElement, ExcalidrawFrameLikeElement } from "./element/types"; +import { fixFractionalIndices } from "./fractionalIndex"; import { getElementsInGroup } from "./groups"; import { getSelectedElements } from "./scene"; import Scene from "./scene/Scene"; @@ -312,12 +313,7 @@ const shiftElementsByOne = ( ]; }); - return elements.map((element) => { - if (targetElementsMap[element.id]) { - return bumpVersion(element); - } - return element; - }); + return fixFractionalIndices(elements, targetElementsMap); }; const shiftElementsToEnd = ( @@ -390,19 +386,22 @@ const shiftElementsToEnd = ( const leadingElements = elements.slice(0, leadingIndex); const trailingElements = elements.slice(trailingIndex + 1); - return direction === "left" - ? [ - ...leadingElements, - ...targetElements, - ...displacedElements, - ...trailingElements, - ] - : [ - ...leadingElements, - ...displacedElements, - ...targetElements, - ...trailingElements, - ]; + return fixFractionalIndices( + direction === "left" + ? [ + ...leadingElements, + ...targetElements, + ...displacedElements, + ...trailingElements, + ] + : [ + ...leadingElements, + ...displacedElements, + ...targetElements, + ...trailingElements, + ], + targetElementsMap, + ); }; function shiftElementsAccountingForFrames(