import type { ColumnState, HeaderHierarchy } from '../../../state' /* Taken from (MIT licensed) https://github.com/pmndrs/use-gesture The comment on their code read: Based on @aholachek ;) https://twitter.com/chpwn/status/285540192096497664 iOS constant = 0.55 https://medium.com/@nathangitter/building-fluid-interfaces-ios-swift-9732bb934bf5 */ function clamp(v: number, min: number, max: number) { return Math.max(min, Math.min(v, max)) } function rubberBand(distance: number, dimension: number, constant: number) { if (dimension === 0 || Math.abs(dimension) === Infinity) return Math.pow(distance, constant * 5) return (distance * dimension * constant) / (dimension + constant * distance) } export function rubberBandIfOutOfBounds( position: number, min: number, max: number, constant = 0.1 ) { if (constant === 0) return clamp(position, min, max) if (position < min) return -rubberBand(min - position, max - min, constant) + min if (position > max) return +rubberBand(position - max, max - min, constant) + max return position } export type ColumnCompare = { location: number newIndex: number compare: 'less' | 'more' parentId: string | null } type Bounds = { initialLeft: number locations: ColumnCompare[] currentIndex: number maxIndex: number minIndex: number minLeft: number maxLeft: number } export const calculateColumnMoving = ( targetId: string, columns: ColumnState[], groups: HeaderHierarchy['groups'], getGroup: (id: string) => ColumnState, hiddenIds: string[] ): Bounds => { let left = 0 const parent = groups.find((g) => g.childIds.includes(targetId)) //If column has a parent, limit the span of columns to it's siblings const validColumnIds = ( parent ? columns.filter((c) => parent.columnIds.includes(c.id)) : columns ).map((c) => c.id) const currentIndex = columns.findIndex((col) => col.id === targetId) const targetColumn = columns[currentIndex] let minIndex = 0 let maxIndex = Infinity let minLeft = 0 let maxLeft = Infinity let initialLeft = 0 const locations = columns.reduce((m, col, index) => { if (col === targetColumn) { initialLeft = left return m } // If the column is hidden, do not include it in the calculation const columnWidth = hiddenIds.includes(col.id) ? 0 : col.actualWidth const columnLeft = left left += columnWidth if ( col.data.lockedLocation === 'left' || (!validColumnIds.includes(col.id) && columns.indexOf(col) < currentIndex) ) { minIndex = index + 1 minLeft = left maxLeft = left + columnWidth return m } if ( col.data.lockedLocation === 'right' || (!validColumnIds.includes(col.id) && columns.indexOf(col) > currentIndex) ) { if (maxIndex === Infinity) { maxIndex = index - 1 } /* Here is where we no longer update max left */ } else { maxLeft = left } if (columnWidth === 0) { // Return early if the column is hidden return m } if (!parent) { //Target column is not a child of a group. Find the suitable indexes between groups const topParent = groups .filter((g) => g.columnIds.includes(col.id)) .sort((a, b) => b.level - a.level)[0] const topParentEntity = topParent ? getGroup(topParent.id) : null const actualWidth = topParentEntity ? topParentEntity.actualWidth : columnWidth const nextIndex = columns.indexOf(col) + (topParent && columns.indexOf(col) > currentIndex ? topParent.columnIds.length - 1 : 0) const d: ColumnCompare = { location: columnLeft + actualWidth / 2, newIndex: nextIndex, compare: nextIndex < currentIndex ? 'less' : 'more', parentId: topParentEntity ? topParentEntity.id : null, } if (!(d.parentId && m.find((c) => c.parentId === d.parentId))) { m.push(d) } } else { const ancestorEntity = col.parentId ? getGroup(col.parentId) : null //Target column is a child of a group. Find the suitable indexes between siblings if (ancestorEntity?.id === parent?.id) { //Target column has same parent as location column const d: ColumnCompare = { location: columnLeft + columnWidth / 2, newIndex: index, compare: index < currentIndex ? 'less' : 'more', parentId: null, } m.push(d) } else { //Target column has same parent as location column const ancestorGroup = groups.find( (g) => g.id === ancestorEntity?.id ) const actualWidth = ancestorEntity ? ancestorEntity.actualWidth : columnWidth const nextIndex = columns.indexOf(col) + (ancestorEntity && ancestorGroup && columns.indexOf(col) > currentIndex ? ancestorGroup.columnIds.length - 1 : 0) const d: ColumnCompare = { location: columnLeft + actualWidth / 2, newIndex: nextIndex, compare: nextIndex < currentIndex ? 'less' : 'more', parentId: ancestorEntity ? ancestorEntity.id : null, } if (!(d.parentId && m.find((c) => c.parentId === d.parentId))) { //Only push once per group m.push(d) } } } return m }, []) return { locations, initialLeft, currentIndex, minIndex, maxIndex, minLeft: minLeft - initialLeft, maxLeft: maxLeft - initialLeft, } } const getGroupEdges = ( targetId: string, columns: ColumnState[], groups: HeaderHierarchy['groups'], getGroup: (id: string) => ColumnState, initialLeft: number, hiddenIds: string[] ): { minLeft: number maxLeft: number minIndex: number maxIndex: number } => { let left = 0 const groupEntity = getGroup(targetId) const group = groups.find((g) => g.id === targetId) if (groupEntity.parentId) { //If column has a parent, limit the span of columns to it's siblings const parent = groups.find((g) => g.id === groupEntity.parentId) const parentEntity = getGroup(groupEntity.parentId) const parentColumnIds = parent?.columnIds const edges = columns.reduce( (m, col) => { const columnWidth = hiddenIds.includes(col.id) ? 0 : col.actualWidth let newMinIndex = m.minIndex let newMaxIndex = m.maxIndex let newMinLeft = m.minLeft if (parentColumnIds?.includes(col.id)) { newMinIndex = Math.min(m.minIndex, columns.indexOf(col)) newMinLeft = Math.min(m.minLeft, left) newMaxIndex = Math.max(m.maxIndex, columns.indexOf(col)) } left = left + columnWidth return { ...m, minIndex: newMinIndex, minLeft: newMinLeft, maxIndex: newMaxIndex, } }, { minIndex: Infinity, maxIndex: 0, minLeft: Infinity, maxLeft: 0, } ) return { ...edges, minLeft: edges.minLeft - initialLeft, maxLeft: Math.max( 0, edges.minLeft + (parentEntity.actualWidth - groupEntity.actualWidth) - initialLeft ), } } else { const edges = columns.reduce( (m, col, index) => { const columnWidth = hiddenIds.includes(col.id) ? 0 : col.actualWidth let newMinIndex = m.minIndex let newMaxIndex = m.maxIndex let newMinLeft = m.minLeft let newMaxLeft = m.maxLeft if (col.data.lockedLocation === 'right') { newMaxIndex = Math.min(index - 1, m.maxIndex) if (m.maxLeft === Infinity) { newMaxLeft = left } } else if (!group?.columnIds.includes(col.id)) { newMaxLeft = left } if (col.data.lockedLocation === 'left') { newMinIndex = index + 1 newMinLeft = left + columnWidth } left = left + columnWidth return { ...m, minIndex: newMinIndex, minLeft: newMinLeft, maxLeft: newMaxLeft, maxIndex: newMaxIndex, } }, { minIndex: 0, maxIndex: columns.length - 1, minLeft: 0, maxLeft: Infinity, } ) return { ...edges, minLeft: edges.minLeft - initialLeft, maxLeft: Math.max( 0, Math.min(left - groupEntity.actualWidth, edges.maxLeft) - initialLeft ), } } } export const calculateGroupMoving = ( targetId: string, columns: ColumnState[], groups: HeaderHierarchy['groups'], getGroup: (id: string) => ColumnState, hiddenIds: string[] ): Bounds => { const columnIds = columns.map((c) => c.id) const targetGroup = groups.find((g) => g.id === targetId)! const targetGroupEntity = getGroup(targetId) const groupSortedColumns = columnIds.filter((id) => targetGroup.columnIds.includes(id) ) const currentIndex = columnIds.indexOf(groupSortedColumns[0]) let initialLeft = 0 let left = 0 const locations = columns.reduce((m, col, index) => { if (col.id === groupSortedColumns[0]) { initialLeft = left return m } const columnWidth = hiddenIds.includes(col.id) ? 0 : col.actualWidth left += columnWidth if (columnWidth === 0) { //Return early if the column is hidden return m } //Add column with the same group parent as the target group if (col.parentId === targetGroupEntity.parentId) { const d: ColumnCompare = { location: left - columnWidth / 2, newIndex: index, compare: index < currentIndex ? 'less' : 'more', parentId: targetGroupEntity.parentId ?? null, } m.push(d) } //Add groups that have the same parent as the target group let ancestorEntity = col.parentId ? getGroup(col.parentId) : null if ( ancestorEntity?.parentId && ancestorEntity?.parentId !== targetGroupEntity.parentId ) { //Move up the hierarchy if the group has a parent (to the top group since we only support 2 levels) //This would need to be refactored if we want to support more than 2 levels ancestorEntity = getGroup(ancestorEntity.parentId) } if ( ancestorEntity && ancestorEntity?.parentId === targetGroupEntity.parentId && ancestorEntity.id !== targetGroupEntity.id ) { const ancestorGroup = groups.find( (g) => g.id === ancestorEntity?.id ) const directAncestor = getGroup(col.parentId!) const actualWidth = directAncestor ? directAncestor.actualWidth : columnWidth const nextIndex = columns.indexOf(col) + (ancestorEntity && ancestorGroup && columns.indexOf(col) > currentIndex ? ancestorGroup.columnIds.length - 1 : 0) const d: ColumnCompare = { location: left - columnWidth + actualWidth / 2, newIndex: nextIndex, compare: nextIndex < currentIndex ? 'less' : 'more', parentId: ancestorEntity ? ancestorEntity.id : null, } if ( col.parentId !== targetId && !(d.parentId && m.find((c) => c.parentId === d.parentId)) ) { // Only push once per group m.push(d) } } return m }, []) return { locations, initialLeft, currentIndex, ...getGroupEdges( targetId, columns, groups, getGroup, initialLeft, hiddenIds ), } }