import { isColumnGroup } from '../../type-predicates' import type { Column } from '../../types' import type { ColumnState, HeaderHierarchy, HeaderHierarchyGroup, } from '../types' export const getLowestLeafs = ( parentId: ColumnState['id'], entities: Record, hiddenIds: string[] ): ColumnState[] => { const parent = entities[parentId] const children: ColumnState[] = (parent?.childIds ?? []).reduce( (prev, curr) => { const currentEntity: ColumnState = entities[curr] if (currentEntity?.isParent) { return [ ...prev, ...getLowestLeafs(currentEntity.id, entities, hiddenIds), ] } if (!hiddenIds.includes(curr)) { prev.push(currentEntity) } return prev }, [] as ColumnState[] ) return children } export const getChildGroups = ( parentId: ColumnState['id'], entities: Record ): ColumnState[] => { const parent = entities[parentId] const children: ColumnState[] = (parent?.childIds || []).reduce( (prev, curr) => { const currentEntity: ColumnState = entities[curr] if (currentEntity?.isParent) { prev.push( currentEntity, ...getChildGroups(currentEntity.id, entities) ) } return prev }, [] as ColumnState[] ) return children } export const getParentGroups = ( columnId: ColumnState['id'], entities: Record ): ColumnState[] => { let current = entities[columnId] const ret = [] if (!current) { return [] } while (current.parentId) { const parent = entities[current.parentId] if (parent) { ret.push(parent) current = parent } else { break } } return ret } const DEFAULT_MIN_WIDTH = 100 const DEFAULT_MAX_WIDTH = Infinity export function constrainColumnWidth(width: number, column: Column) { const minWidth = Math.min( column.minWidth ?? DEFAULT_MIN_WIDTH, column.width ?? Infinity ) const maxWidth = Math.max( column.maxWidth ?? DEFAULT_MAX_WIDTH, column.width ?? 0 ) return Math.min(Math.max(width, minWidth), maxWidth) } export const constrainParentWidth = ( width: number, children: ColumnState[] ): number => { const maxWidth = children.some((child) => !child.data.maxWidth) ? Infinity : children.reduce((acc, child) => acc + (child.data.maxWidth ?? 0), 0) const minWidth = children.reduce( (acc, child) => acc + (child.data.minWidth ?? DEFAULT_MIN_WIDTH), 0 ) return Math.min(Math.max(width, minWidth), maxWidth) } export const getGroupAccumulatedWidth = ( columnGroupId: ColumnState['id'], entities: Record, hiddenIds: string[], changingItem?: { id: string; width: number } ): number => getLowestLeafs(entities[columnGroupId]?.id, entities, hiddenIds).reduce( (prev, curr: ColumnState) => { if (isColumnGroup(curr.data)) { prev = prev + getGroupAccumulatedWidth(curr.id, entities, hiddenIds) } if (curr.id === changingItem?.id) { return prev + changingItem.width } if (hiddenIds.includes(curr.id)) { return prev } return prev + curr.actualWidth }, 0 ) export const getAdjustedColumnWidths = ( children: ColumnState[], newTotalWidth: number ): { id: string; width: number }[] => { const childMap = new Map(children.map((child) => [child.id, child])) const currentTotalWidth = children.reduce( (sum, child) => sum + child.actualWidth, 0 ) const widthDifference = newTotalWidth - currentTotalWidth if (widthDifference === 0) { return [] // No adjustment needed } const adjustmentPerChild = Math.floor(widthDifference / children.length) const adjustedWidths = children.map((child) => { let newWidth = child.actualWidth + adjustmentPerChild const childMinWidth = child.data.minWidth ?? DEFAULT_MIN_WIDTH const childMaxWidth = child.data.maxWidth ?? DEFAULT_MAX_WIDTH // Enforce minWidth and maxWidth if (newWidth < childMinWidth) { newWidth = childMinWidth } else if (newWidth > childMaxWidth) { newWidth = childMaxWidth } return { id: child.id, width: newWidth, } }) // Adjust iteratively to ensure the total width matches const adjustedTotalWidth = adjustedWidths.reduce( (sum, child) => sum + child.width, 0 ) let remainingDifference = newTotalWidth - adjustedTotalWidth while (Math.abs(remainingDifference) > 0) { const sign = Math.sign(remainingDifference) for (const record of adjustedWidths) { if (remainingDifference === 0) break const child = childMap.get(record.id) if (!child) { break } const childMinWidth = child.data.minWidth ?? DEFAULT_MIN_WIDTH const childMaxWidth = child.data.maxWidth ?? DEFAULT_MAX_WIDTH // Check if we can still adjust the width of this child const maxAdjust = sign > 0 ? childMaxWidth - record.width : record.width - childMinWidth if (maxAdjust > 0) { const adjustment = Math.min(maxAdjust, Math.abs(remainingDifference)) * sign record.width += adjustment remainingDifference -= adjustment } } } return adjustedWidths } const groupHasChildGroup = ( groupId: ColumnState['id'], entities: Record ) => !!entities[groupId]?.childIds?.some((id) => entities[id].isParent) export const getGroupIndex = (childIds: string[], sortedIds: string[]) => { const sortedChildren = [...childIds].sort( (a, b) => sortedIds.indexOf(a) - sortedIds.indexOf(b) ) return sortedIds.indexOf(sortedChildren[0]) } export const buildHeaderHierarchy = ( entities: Record, hiddenIds: string[] ): HeaderHierarchy => { const ret: HeaderHierarchyGroup[] = [] let levels = 0 Object.values(entities).forEach((entity) => { if (entity.isParent) { const entityLevel = groupHasChildGroup(entity.id, entities) ? 2 : 1 const columnIds = getLowestLeafs(entity.id, entities, hiddenIds) .map((c) => c.id) .filter((id) => !hiddenIds.includes(id)) const parentIds = getParentGroups(entity.id, entities).map( (g) => g.id ) levels = Math.max(levels, entityLevel) const found = ret.find((group) => group.childIds.some((id) => entity.childIds?.includes(id)) ) if (found) { console.warn( 'Duplicate child reference found in columnGroups! Columns or groups can only be children of a single parent.' ) } ret.push({ id: entity.id, level: entityLevel, width: columnIds.length, childIds: entity.childIds?.filter((id) => !hiddenIds.includes(id)) ?? [], columnIds, parentIds, }) } }) /** * Drop any groups that have child groups that are already parents of groups, as that would exceed the allowed depth. */ const groupsSanitized = ret.filter( (group) => !group.childIds.some((id) => groupHasChildGroup(id, entities)) ) return { groups: groupsSanitized, levels } } export const getInferredSticky = ( id: ColumnState['id'], entities: Record, { groups }: HeaderHierarchy ): ColumnState['data']['sticky'] => { const entity = entities[id] if (!entity) { return undefined } const sortedParents = getParentGroups(id, entities).sort((a, b) => { const aLevel = groups.find((g) => g.id === a.id)?.level || 0 const bLevel = groups.find((g) => g.id === b.id)?.level || 0 return bLevel - aLevel }) if (sortedParents.length) { return sortedParents[0].data.sticky } return entity.data.sticky } export const getInferredLockedLocation = ( id: ColumnState['id'], entities: Record, { groups }: HeaderHierarchy ): ColumnState['data']['lockedLocation'] => { const entity = entities[id] if (!entity) { return undefined } const sortedParents = getParentGroups(id, entities).sort((a, b) => { const aLevel = groups.find((g) => g.id === a.id)?.level || 0 const bLevel = groups.find((g) => g.id === b.id)?.level || 0 return bLevel - aLevel }) if (sortedParents.length) { return sortedParents[0].data.lockedLocation } return entity.data.lockedLocation } export const healPreferencesSortedIds = ( newIds: string[], currentIds: string[] ): string[] | null => { const itemsAdded = newIds.filter((id) => !currentIds.includes(id)) /** * If any ids have been added to the current, return null and destroy saved preferences. * If none have been added but any ids have been removed from current, filter it out and return the result */ return itemsAdded.length ? null : currentIds.filter((id) => newIds.includes(id)) } export const healPreferencesSplitGroups = ( newIds: string[] | null, groups: HeaderHierarchyGroup[] ): string[] | null => { if (!newIds) { return newIds } const anySplitGroup = groups.some((group) => { const range = group.columnIds.reduce( (acc, id) => { const index = newIds.indexOf(id) return { min: Math.min(acc.min, index), max: Math.max(acc.max, index), } }, { min: Infinity, max: 0 } ) const newRange = range.max - range.min const expectedRange = group.columnIds.length - 1 return expectedRange !== newRange }) return anySplitGroup ? null : newIds }