From 0d5272720f7af309f0065cf2302496c77ea31253 Mon Sep 17 00:00:00 2001 From: Christopher Chedeau Date: Fri, 3 Jan 2020 21:03:25 -0800 Subject: [PATCH] Send to back (#98) --- src/index.tsx | 33 +++++++++++++++- src/zindex.test.ts | 51 ++++++++++++++++++++++++ src/zindex.ts | 97 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 180 insertions(+), 1 deletion(-) create mode 100644 src/zindex.test.ts create mode 100644 src/zindex.ts diff --git a/src/index.tsx b/src/index.tsx index 4ad3a7f7..0e4aadc2 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -11,6 +11,8 @@ import { faFont } from "@fortawesome/free-solid-svg-icons"; +import { moveOneLeft, moveAllLeft } from "./zindex"; + import "./styles.css"; type ExcalidrawElement = ReturnType; @@ -647,6 +649,16 @@ function isArrowKey(keyCode: string) { ); } +function getSelectedIndices() { + const selectedIndices: number[] = []; + elements.forEach((element, index) => { + if (element.isSelected) { + selectedIndices.push(index); + } + }); + return selectedIndices; +} + const ELEMENT_SHIFT_TRANSLATE_AMOUNT = 5; const ELEMENT_TRANSLATE_AMOUNT = 1; @@ -710,7 +722,26 @@ class App extends React.Component<{}, AppState> { }); this.forceUpdate(); event.preventDefault(); - } else if (event.key === "a" && event.metaKey) { + + // Send backwards: Cmd-Shift-Alt-B + } else if ( + event.metaKey && + event.shiftKey && + event.altKey && + event.code === "KeyB" + ) { + moveOneLeft(elements, getSelectedIndices()); + this.forceUpdate(); + event.preventDefault(); + + // Send to back: Cmd-Shift-B + } else if (event.metaKey && event.shiftKey && event.code === "KeyB") { + moveAllLeft(elements, getSelectedIndices()); + this.forceUpdate(); + event.preventDefault(); + + // Select all: Cmd-A + } else if (event.metaKey && event.code === "KeyA") { elements.forEach(element => { element.isSelected = true; }); diff --git a/src/zindex.test.ts b/src/zindex.test.ts new file mode 100644 index 00000000..dbc66e47 --- /dev/null +++ b/src/zindex.test.ts @@ -0,0 +1,51 @@ +import { moveOneLeft, moveAllLeft } from "./zindex"; + +function expectMove(fn, elems, indices, equal) { + 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 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"] + ); +}); diff --git a/src/zindex.ts b/src/zindex.ts new file mode 100644 index 00000000..a3ed35df --- /dev/null +++ b/src/zindex.ts @@ -0,0 +1,97 @@ +function swap(elements: T[], indexA: number, indexB: number) { + const element = elements[indexA]; + elements[indexA] = elements[indexB]; + elements[indexB] = element; +} + +export function moveOneLeft(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; + } + swap(elements, index - 1, index); + }); +} + +// 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 beggining +// +// |1 |0 +// [c, f, a, b, d, e, g] +// +// And we are done! +export function moveAllLeft(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) { + 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; + }); +}