1
0
mirror of https://gitlab.com/bramw/baserow.git synced 2024-11-21 23:37:55 +00:00
bramw_baserow/web-frontend/modules/database/utils/virtualScrolling.js
2022-07-29 09:12:43 +00:00

160 lines
4.8 KiB
JavaScript

/**
* This helper method will create an array of slots, where every slot contains one of
* the provided `items`. Every slot has a unique `id` and when the items change, the
* slots are recycled to avoid re-render. Every item that persists will it's own
* slot id.
*
* This is useful so virtual scroll. The `id` of the slot can be used to as
* `:key="slot.id"` value in the template. Because the id will always match the item
* id, it will never update or re-render the component. New items will get a
* recycled slot id so that they don't have to be re-rendered.
*
* It is a requirement that every item has an `id` property to make sure the id of
* the slot is properly recycled.
*
* Optionally a `getPosition` can be provided to set the `position` property in each
* slot. This is typically used by virtual scrolling change the absolute position of
* the dom element. This could be needed because the elements before are not rendered
* and are not pushing it into the right position.
*
* Example:
*
* const slots = recycleSlots(
* [],
* [
* { id: 1, name: "Item 1" },
* { id: 2, name: "Item 2" }
* ]
* ) == [
* { id: 0, position: {}, item: { id: 1, name: "Item 1" } },
* { id: 1, position: {}, item: { id: 2, name: "Item 2" } }
* ]
*
* recycleSlots(
* slots,
* [
* { id: 2, name: "Item 2" },
* { id: 3, name: "Item 3" }
* ]
* ) == [
* { id: 0, position: {}, item: { id: 3, name: "Item 3" } },
* { id: 1, position: {}, item: { id: 2, name: "Item 2" } }
* ]
*/
export const recycleSlots = (slots, items, getPosition, min = items.length) => {
// If there are more items than the minimum that must be rendered, we want to
// increase the minimum to ensure all items are visible.
if (min < items.length) {
min = items.length
}
for (let i = slots.length; i < min; i++) {
slots.push({
id: i,
position: {},
item: undefined,
})
}
// Remove slots that aren't needed anymore.
if (slots.length > min) {
slots.splice(items.length, min)
}
const emptySlots = []
const itemSlotIndexMap = {}
// Loop over the slots and clear the items that must not be rendered anymore.
slots.forEach((slot, index) => {
if (slot.item === undefined || slot.item === null) {
emptySlots.push(index)
return
}
const itemIndex = items.findIndex(
(item) => item !== null && item.id === slot.item.id
)
if (itemIndex < 0) {
// Slot item is not visible anymore.
slot.item = undefined
slot.position = {}
emptySlots.push(index)
} else {
// Item is found in slot array.
itemSlotIndexMap[items[itemIndex].id] = index
}
})
// Loop over the items and assign them to a slot if they don't yet exist.
items.forEach((item, position) => {
let index = itemSlotIndexMap[item?.id]
// If item isn't in a slot yet we use the first empty slot index.
if (index === undefined) {
index = emptySlots.shift()
}
// Only update the item and position if it has changed in the slot to avoid
// re-renders.
if (slots[index].item !== item) {
slots[index].item = item
}
const slotPosition = getPosition(item, position)
if (
JSON.stringify(slotPosition) !== JSON.stringify(slots[index].position)
) {
slots[index].position = slotPosition
}
})
// The remaining empty slots must be cleared because they could contain old items.
emptySlots.forEach((slotIndex) => {
slots[slotIndex].item = undefined
slots[slotIndex].position = {}
})
}
/**
* This function will order the slots based on the item position in items array. The
* slots will be moved without recreating the array to prevent re-rendering.
*/
export const orderSlots = (slots, items) => {
let i = 0
while (i < items.length) {
const item = items[i]
// Items can be null until they're fetched from server.
if (item === null) {
i++
continue
}
const existingIndex = slots.findIndex(
(slot) =>
slot.item !== null &&
slot.item !== undefined &&
slot.item.id === item.id
)
if (existingIndex > -1 && existingIndex !== i) {
// If the item already exists in the slots array, but the position match yet,
// we need to move it.
if (existingIndex < i) {
// In this case, the existing index is lower than the new index, so in order
// avoid conflicts with already moved items we just swap them.
slots.splice(i, 0, slots.splice(existingIndex, 1)[0])
slots.splice(existingIndex, 0, slots.splice(i - 1, 1)[0])
i++
} else if (existingIndex > i) {
// If the existing index is higher than the expected index, we need to move
// it one by one to avoid conflicts with already moved items.
slots.splice(existingIndex - 1, 0, slots.splice(existingIndex, 1)[0])
}
} else {
i++
}
}
}