interface IBaseMenuItem { label: React.ReactNode; value?: TValue; data: TMenuData | null; } export type IFlatMenuItem = IBaseMenuItem< TMenuData, TValue > & { id: string; parent: string | null; sort: number; }; export type IMenuItem = IBaseMenuItem< TMenuData, TValue > & { children: Array>; }; export type IMenu = Array< IMenuItem >; export type IFlatMenu = Array< IFlatMenuItem >; export function validateMenuOptions( menu: IMenu ): boolean { const values: TValue[] = toFlat(menu) .map(menuItem => menuItem.value) .filter(value => value !== undefined); const uniqueValues = Array.from(new Set(values)); return uniqueValues.length === values.length; } function _getMenuItemByPath( menuItem: IMenuItem, path: number[] ): IMenuItem { if (path.length === 0) { return menuItem; } return _getMenuItemByPath(menuItem.children[path[0]], path.slice(1)); } export function getMenuItemByPath( menu: IMenu, path: number[] ): IMenuItem { return _getMenuItemByPath(menu[path[0]], path.slice(1)); } export function getMenuItemByValue( menu: IMenu, value: TValue ): IMenuItem { const flatMenu = toFlat(menu); const flatMenuItem: IFlatMenuItem = flatMenu.find( menuItem => menuItem.value === value ); if (flatMenuItem === undefined) { throw new Error(`Value ${value} does not exist in menu`); } return _fromFlat(flatMenu, flatMenuItem); } function _walkToMenuItem( menuItem: IMenuItem, path: number[] ): IMenu { const node = menuItem.children[path[0]]; if (path.length === 1) { return [node]; } return [node, ..._walkToMenuItem(node, path.slice(1))]; } export function walkToMenuItem( menu: IMenu, path: number[] ): IMenu { const walkByNode = menu[path[0]]; return [walkByNode, ..._walkToMenuItem(walkByNode, path.slice(1))]; } function _walkToRoot( flatMenu: IFlatMenu, parent: string ): IFlatMenu { const menuItem = flatMenu.find(menuItem => menuItem.id === parent); if (menuItem.parent === null) { return [menuItem]; } return [menuItem, ..._walkToRoot(flatMenu, menuItem.parent)]; } export function walkToRoot( menu: IMenu, value: TValue ): IMenu { const flatMenu = toFlat(menu); const menuItem = flatMenu.find(menuItem => menuItem.value === value); return (menuItem.parent === null ? [menuItem] : [menuItem, ..._walkToRoot(flatMenu, menuItem.parent)] ).map(flatMenuItem => _fromFlat(flatMenu, flatMenuItem)); } function _toFlat( menuItem: IMenuItem, sort: number, parent: string ): IFlatMenu { const id = parent ? [parent, sort].join(":") : sort.toString(); const flatMenuItem: IFlatMenuItem = { data: menuItem.data, id, label: menuItem.label, parent, sort, value: menuItem.value }; return [ flatMenuItem, ...menuItem.children .map((child, childIndex) => _toFlat(child, childIndex, id)) .reduce((acc, curr) => [...acc, ...curr], [] as IFlatMenu< TMenuData, TValue >) ]; } export function toFlat( menu: IMenu ): IFlatMenu { return menu .map((menuItem, menuItemIndex) => _toFlat(menuItem, menuItemIndex, null)) .reduce((acc, curr) => [...acc, ...curr], [] as IFlatMenu< TMenuData, TValue >); } function _fromFlat( menu: IFlatMenu, flatMenuItem: IFlatMenuItem ): IMenuItem { const children: Array> = menu .filter(menuItem => menuItem.parent === flatMenuItem.id) .map(menuItem => _fromFlat(menu, menuItem)); return { children, data: flatMenuItem.data, label: flatMenuItem.label, value: flatMenuItem.value }; } export function fromFlat( menu: IFlatMenu ): IMenu { return menu .filter(menuItem => menuItem.parent === null) .map(menuItem => _fromFlat(menu, menuItem)); } export function isLeaf( menuItem: IMenuItem ): boolean { return menuItem.children.length === 0; }