Reordering Part 1: Arrays
In an app that uses a zoom UI, canvas, or really any paradigm where things are "stacked" in order from back to front, the user interface will usually provide some commands that let a user move items in the stack:
- Send to Back
- Send Backward
- Bring Forward
- Bring to Front
Implementing these commands will depend on how your application structures its items. Are they in an array? Are they in a table? Is this a multiplayer application?
In this article, I'll cover the most straightforward implementation in an app that structures its items in an array. In a future post, I'll cover a more complex method in an application where items are stored in a hash table.
Mise en place
Let's say we have an application where we're storing our items in an array:
type Item = { id: string }
type Items = Item[]
const itemsExample: Items = [{ id: "a" }, { id: "b" }, { id: "c" }]
In this structure, each item's "order" is represented by that item's index in the array. In the example above, the item { id: "a" }
has the index of 0
, the item { id: "b" }
has the index of 1
, etc.
Now that we have our data worked out, let's look at how we would implement our four reordering commands.
🚀 You can view the code and tests for this post at this CodeSandbox.
Send to Back
function sendToBack(items: Item[], ids: string[]) {
const movingIds = new Set(ids)
const moving: Item[] = []
const notMoving: Item[] = []
for (const item of items) {
const arr = movingIds.has(item.id) ? moving : notMoving
arr.push(item)
}
return moving.concat(notMoving)
}
For sendToBack
, we would want the new items
array to be all of the moving items, sorted by their prior order within the items
array, followed by all of the static items sorted by their prior order in the items
array.
This works for single items as well as for multiple items:
let items = [{ id: "a" }, { id: "b" }, { id: "c" }, { id: "d" }]
items = sendToBack(items, ["c"]) // c, a, b, d
let items = [{ id: "a" }, { id: "b" }, { id: "c" }, { id: "d" }]
items = sendToBack(items, ["b", "d"]) // b, d, a, c
Bring to Front
function bringToFront(items: Item[], ids: string[]) {
const movingIds = new Set(ids)
const moving: Item[] = []
const notMoving: Item[] = []
for (const item of items) {
const arr = movingIds.has(item.id) ? moving : notMoving
arr.push(item)
}
return notMoving.concat(moving)
}
For bringToFront
, we perform the same work as sendToBack
, except this time adding the moving items to the end of the static items array. Again, both arrays preserve their items' order from the input array.
let items = [{ id: "a" }, { id: "b" }, { id: "c" }, { id: "d" }]
items = bringToFront(items, ["b"]) // a, c, d, b
let items = [{ id: "a" }, { id: "b" }, { id: "c" }, { id: "d" }]
items = bringToFront(items, ["a", "c"]) // b, d, a, c
Send Backward
function sendBackward(items: Item[], ids: string[]) {
const movingIds = new Set(ids)
const indices: number[] = []
items.forEach((item, i) => {
if (movingIds.has(item.id)) {
indices.push(i)
}
})
const result = items.slice()
indices.forEach((index) => {
const movingItem = result[index]
const neighborBelow = result[index - 1]
if (neighborBelow && !movingIds.has(neighborBelow.id)) {
result[index] = neighborBelow
result[index - 1] = movingItem
}
})
return result
}
Sending an item backward is a little more complex. Here we want to iterate through each moving item's original index and try to swap the item we find at that index in the results array with its neighbor at the index below. If there is no neighbor item, then this means we're trying to move the first item in the list; and if the neighbor is also moving, then this means we haven't yet been able to move any items down.
let items = [{ id: "a" }, { id: "b" }, { id: "c" }, { id: "d" }]
items = sendBackward(items, ["c"]) // a, c, b, d
let items = [{ id: "a" }, { id: "b" }, { id: "c" }, { id: "d" }]
items = sendBackward(items, ["b", "c"]) // b, c, a, c
Bring Forward
function bringForward(items: Item[], ids: string[]) {
const movingIds = new Set(ids)
const indices: number[] = []
items.forEach((item, i) => {
if (movingIds.has(item.id)) {
indices.push(i)
}
})
const result = items.slice()
indices.reverse().forEach((index) => {
const movingItem = result[index]
const neighborAbove = result[index + 1]
if (neighborAbove && !movingIds.has(neighborAbove.id)) {
result[index] = neighborAbove
result[index + 1] = movingItem
}
})
return result
}
The bringForward
method is implemented in a similar way, but reversing the indices array so that we iterate down from the highest index to the lowest, and swapping each item with the item above it in the results array.
let items = [{ id: "a" }, { id: "b" }, { id: "c" }, { id: "d" }]
items = bringForward(items, ["b"]) // a, c, b, d
let items = [{ id: "a" }, { id: "b" }, { id: "c" }, { id: "d" }]
items = bringForward(items, ["b", "c"]) // b, a, d, c
Wrapup
Moving items in an array has some upsides and some downsides. An advantage is that items may be placed in the document or painted in the correct order without any further sorting or manipulation.
for (const item of items) {
canvas.paintItem(item)
}
<div>
{items.map((item) => (
<Item key={item.id} item={item} />
))}
</div>
The main disadvantage comes from the difficulty of accessing a particular item within the array, which requires a search through the array.
const items = [{ id: "a" }, { id: "b" }, { id: "c" }]
function getItem(id: string) {
return items.find((item) => item.id === id)
}
This can be impractical for apps that need to store many items, or that require a more efficient way of accessing items. This is usually done with a hash table (such as a Map
, or plain object) or another from of associative structure.
const items = {
a: { id: "a", index: 1 },
b: { id: "b", index: 2 },
c: { id: "c", index: 3 },
}
function getItem(id: string) {
return items[id]
}
However, while hash tables make for fast lookup, they can't make guarantees about ordering in the same way that arrays do; and so we would be forced to keep track of indices ourselves.
This adds some new trickiness and I'll cover that in the next post.
🚀 You can view the code and tests from this post at this CodeSandbox.
Thanks for reading! For more like this, follow me on Twitter. To support my work and nudge me toward more blogging, sponsor me on Github.