import { Dictionary, last } from 'lodash'; import { SilkeIcons } from '../silke-icon'; import React from 'react'; export type RenderFunction = (item: Item, index: number) => React.ReactElement; export type Item = T & { key: string; /** Not selectable and no hover interactions */ disabled?: boolean; children?: string[]; render?: RenderFunction; className?: string; /** Override the default behavior with checking for children length */ showExpandIcon?: boolean; style?: React.CSSProperties; disableCollapse?: boolean; /** Overwrite icon */ replaceIcon?: | { icon: SilkeIcons; color: string; } | SilkeSortableIconComponent; }; export type SilkeSortableIconProps = { collapsed: boolean; selected: boolean; hasChildren: boolean; item: Item; onMouseDown?: (e: React.MouseEvent) => void; }; export type SilkeSortableIconComponent = React.FC>; const INDENT_BITS = 6; const MAX_INDENT_MASK = 0b111111; export enum ItemState { indentBit1 = 1 << 0, indentBit2 = 1 << 1, indentBit3 = 1 << 2, indentBit4 = 1 << 3, indentBit5 = 1 << 4, indentBit6 = 1 << 5, collapsed = 1 << 6, canHaveChildren = 1 << 7, selected = 1 << 8, childOfSelected = 1 << 9, roundTop = 1 << 10, roundBottom = 1 << 11, placeAfter = 1 << 12, placeBefore = 1 << 13, placeInside = 1 << 14, placementInvalid = 1 << 15, } function getComputedState( prevState = 0, selected?: boolean, childOfSelected?: boolean, collapsed?: boolean, canHaveChildren?: boolean, indent = 0, ): number { let state = indent; const prevSelected = hasState(prevState, ItemState.selected | ItemState.childOfSelected); if (selected) { state |= ItemState.selected; if (!prevSelected && !childOfSelected) { state |= ItemState.roundTop; } } if (!selected && !childOfSelected) { state |= ItemState.roundTop | ItemState.roundBottom; } if (childOfSelected) state |= ItemState.childOfSelected; if (collapsed) state |= ItemState.collapsed; if (canHaveChildren) state |= ItemState.canHaveChildren; return state; } export function hasState(state: number, hasState: number): boolean { return Boolean(state & hasState); } export function getIndentLevel(state: number): number { return state & MAX_INDENT_MASK; } export function generatedComputedStates( itemMap: Dictionary> = {}, itemKeys: string[], selected?: string[], collapsed?: (item: Item) => boolean, canHaveChildren?: (item: Item) => boolean, isChildOfSelected?: boolean, indent = 0, computedStates: [number, Item][] = [], ): [number, Item][] { for (const itemKey of itemKeys) { const item = itemMap[itemKey]; if (!item) continue; const prevState = computedStates[computedStates.length - 1]?.[0] || 0; const isSelected = selected?.includes(item.key); const isCollapsed = collapsed?.(item) || false; const canHaveChildrenValue = canHaveChildren?.(item) || !!item.children?.length; const state = getComputedState( prevState, isSelected, isChildOfSelected, isCollapsed, canHaveChildrenValue, indent, ); if ( prevState && !isSelected && !hasState(state, ItemState.childOfSelected) && hasState(prevState, ItemState.selected | ItemState.childOfSelected) ) { computedStates[computedStates.length - 1][0] |= ItemState.roundBottom; } computedStates.push([state, item]); if (item.children && !hasState(state, ItemState.collapsed)) { generatedComputedStates( itemMap, item.children, selected, collapsed, canHaveChildren, isSelected || isChildOfSelected, indent + 1, computedStates, ); } } const prevState = computedStates[computedStates.length - 1]?.[0] || 0; if (!indent && hasState(prevState, ItemState.childOfSelected | ItemState.selected)) { computedStates[computedStates.length - 1][0] |= ItemState.roundBottom; } return computedStates; } export function getParents(itemKey: string, parentMap: { [key: string]: string }): string[] { const parents = []; let parentKey = parentMap[itemKey]; while (parentKey) { parents.push(parentKey); parentKey = parentMap[parentKey]; } return parents; } export function isParentOfSelf( targetKey: string, itemKey: string, parentMap: { [key: string]: string }, ): boolean { if (itemKey === targetKey) return true; const parentKey = parentMap[targetKey]; return !parentKey ? false : isParentOfSelf(parentKey, itemKey, parentMap); } export function debugLogState(message: string, state: number) { console.group(message); for (const key in ItemState) { const v = ItemState[key]; if (typeof v === 'number' && state & v) console.log(ItemState[v]); } console.groupEnd(); } /** * If the users hovers the end of a group the might be trying to move it to the end of the parent of the group * This function checks based on the mouse indent level which parent is the best target */ export function getMouseHorizontalIndentTarget( target: Item, deltaMouseIndent: number, itemMap: { [key: string]: Item }, parentMap: { [key: string]: string }, ): Item { // Can only indent negative direction so if greater than -1 then don't try to move target if (deltaMouseIndent > -1) return target; const parentKey = parentMap[target.key]; const parent = itemMap[parentKey]; // target needs to be the last in the parent if (last(parent?.children) === target.key) { return getMouseHorizontalIndentTarget(parent, deltaMouseIndent + 1, itemMap, parentMap); } return target; }