src/components/Tree/utilities.ts
import type { UniqueIdentifier } from '@dnd-kit/core'
import { arrayMove } from '@dnd-kit/sortable'
import type { FlattenedItem, TreeItem, TreeItems } from './types'
export const iOS = /iPad|iPhone|iPod/.test(navigator.platform)
function getDragDepth(offset: number, indentationWidth: number) {
return Math.round(offset / indentationWidth)
}
export function getProjection(
items: FlattenedItem[],
activeId: UniqueIdentifier,
overId: UniqueIdentifier,
dragOffset: number,
indentationWidth: number,
) {
const overItemIndex = items.findIndex(({ id }) => id === overId)
const activeItemIndex = items.findIndex(({ id }) => id === activeId)
const activeItem = items[activeItemIndex]
const newItems = arrayMove(items, activeItemIndex, overItemIndex)
const previousItem = newItems[overItemIndex - 1]
const nextItem = newItems[overItemIndex + 1]
const dragDepth = getDragDepth(dragOffset, indentationWidth)
const projectedDepth = activeItem.depth + dragDepth
const maxDepth = getMaxDepth({
previousItem,
})
const minDepth = getMinDepth({ nextItem })
let depth = projectedDepth
if (projectedDepth >= maxDepth) {
depth = maxDepth
} else if (projectedDepth < minDepth) {
depth = minDepth
}
return { depth, maxDepth, minDepth, parentId: getParentId() }
function getParentId() {
if (depth === 0 || !previousItem) {
return null
}
if (depth === previousItem.depth) {
return previousItem.parentId
}
if (depth > previousItem.depth) {
return previousItem.id
}
const newParent = newItems
.slice(0, overItemIndex)
.reverse()
.find((item) => item.depth === depth)?.parentId
return newParent ?? null
}
}
function getMaxDepth({ previousItem }: { previousItem: FlattenedItem }) {
if (previousItem) {
return previousItem.depth + 1
}
return 0
}
function getMinDepth({ nextItem }: { nextItem: FlattenedItem }) {
if (nextItem) {
return nextItem.depth
}
return 0
}
function flatten(
items: TreeItems,
parentId: UniqueIdentifier | null = null,
depth = 0,
): FlattenedItem[] {
return items.reduce<FlattenedItem[]>((acc, item, index) => {
return [
...acc,
{ ...item, parentId, depth, index },
...flatten(item.children, item.id, depth + 1),
]
}, [])
}
export function flattenTree(items: TreeItems): FlattenedItem[] {
return flatten(items)
}
export function buildTree(flattenedItems: FlattenedItem[]): TreeItems {
const root: TreeItem = { id: 'root', children: [] }
const nodes: Record<string, TreeItem> = { [root.id]: root }
const items = flattenedItems.map((item) => ({ ...item, children: [] }))
for (const item of items) {
const { id, children } = item
const parentId = item.parentId ?? root.id
const parent = nodes[parentId] ?? findItem(items, parentId)
nodes[id] = { id, children }
parent.children.push(item)
}
return root.children
}
export function findItem(items: TreeItem[], itemId: UniqueIdentifier) {
return items.find(({ id }) => id === itemId)
}
export function findItemDeep(
items: TreeItems,
itemId: UniqueIdentifier,
): TreeItem | undefined {
for (const item of items) {
const { id, children } = item
if (id === itemId) {
return item
}
if (children.length) {
const child = findItemDeep(children, itemId)
if (child) {
return child
}
}
}
return undefined
}
export function removeItem(items: TreeItems, id: UniqueIdentifier) {
const newItems = []
for (const item of items) {
if (item.id === id) {
continue
}
if (item.children.length) {
item.children = removeItem(item.children, id)
}
newItems.push(item)
}
return newItems
}
export function setProperty<T extends keyof TreeItem>(
items: TreeItems,
id: UniqueIdentifier,
property: T,
setter: (value: TreeItem[T]) => TreeItem[T],
) {
const newItems = items.map((item) => {
if (item.id === id) {
const newItem = item.clone()
newItem[property] = setter(newItem[property])
return newItem
}
if (item.children.length) {
const newItem = item.clone()
newItem.children = setProperty(newItem.children, id, property, setter)
return newItem
}
return item
})
return [...newItems]
}
function countChildren(items: TreeItem[], count = 0): number {
return items.reduce((acc, { children }) => {
if (children.length) {
return countChildren(children, acc + 1)
}
return acc + 1
}, count)
}
export function getChildCount(items: TreeItems, id: UniqueIdentifier) {
const item = findItemDeep(items, id)
return item ? countChildren(item.children) : 0
}
export function removeChildrenOf(
items: FlattenedItem[],
ids: UniqueIdentifier[],
) {
const excludeParentIds = [...ids]
return items.filter((item) => {
if (item.parentId && excludeParentIds.includes(item.parentId)) {
if (item.children.length) {
excludeParentIds.push(item.id)
}
return false
}
return true
})
}