function splitArray (arr: T[], predicate: (x: T) => boolean) { const result = arr.reduce((multi, item) => { const current = multi[multi.length - 1]; if (predicate(item)) { if (current.length > 0) multi.push([]); } else { current.push(item); } return multi; }, [[]] as T[][]); if (result[result.length - 1].length === 0) { return result.slice(0, result.length - 1); } return result; } function joinArrays (arrays: any[][], joinIDs: any[]) { return arrays.reduce((joined, arr, i) => { if (i > 0 && arr.length) { if (joinIDs.length > 0) { joined.push(joinIDs[0]); joinIDs.splice(0, 1); } else { joined.push({ type: 'separator' }); } } return joined.concat(arr); }, []); } function pushOntoMultiMap (map: Map, key: K, value: V) { if (!map.has(key)) { map.set(key, []); } map.get(key)!.push(value); } function indexOfGroupContainingID (groups: {id?: T}[][], id: T, ignoreGroup: {id?: T}[]) { return groups.findIndex( candidateGroup => candidateGroup !== ignoreGroup && candidateGroup.some( candidateItem => candidateItem.id === id ) ); } // Sort nodes topologically using a depth-first approach. Encountered cycles // are broken. function sortTopologically (originalOrder: T[], edgesById: Map) { const sorted = [] as T[]; const marked = new Set(); const visit = (mark: T) => { if (marked.has(mark)) return; marked.add(mark); const edges = edgesById.get(mark); if (edges != null) { edges.forEach(visit); } sorted.push(mark); }; originalOrder.forEach(visit); return sorted; } function attemptToMergeAGroup (groups: {before?: T[], after?: T[], id?: T}[][]) { for (let i = 0; i < groups.length; i++) { const group = groups[i]; for (const item of group) { const toIDs = [...(item.before || []), ...(item.after || [])]; for (const id of toIDs) { const index = indexOfGroupContainingID(groups, id, group); if (index === -1) continue; const mergeTarget = groups[index]; mergeTarget.push(...group); groups.splice(i, 1); return true; } } } return false; } function mergeGroups (groups: {before?: T[], after?: T[], id?: T}[][]) { let merged = true; while (merged) { merged = attemptToMergeAGroup(groups); } return groups; } function sortItemsInGroup (group: {before?: T[], after?: T[], id?: T}[]) { const originalOrder = group.map((node, i) => i); const edges = new Map(); const idToIndex = new Map(group.map((item, i) => [item.id, i])); group.forEach((item, i) => { if (item.before) { item.before.forEach(toID => { const to = idToIndex.get(toID); if (to != null) { pushOntoMultiMap(edges, to, i); } }); } if (item.after) { item.after.forEach(toID => { const to = idToIndex.get(toID); if (to != null) { pushOntoMultiMap(edges, i, to); } }); } }); const sortedNodes = sortTopologically(originalOrder, edges); return sortedNodes.map(i => group[i]); } function findEdgesInGroup (groups: {beforeGroupContaining?: T[], afterGroupContaining?: T[], id?: T}[][], i: number, edges: Map) { const group = groups[i]; for (const item of group) { if (item.beforeGroupContaining) { for (const id of item.beforeGroupContaining) { const to = indexOfGroupContainingID(groups, id, group); if (to !== -1) { pushOntoMultiMap(edges, to, i); return; } } } if (item.afterGroupContaining) { for (const id of item.afterGroupContaining) { const to = indexOfGroupContainingID(groups, id, group); if (to !== -1) { pushOntoMultiMap(edges, i, to); return; } } } } } function sortGroups (groups: {id?: T}[][]) { const originalOrder = groups.map((item, i) => i); const edges = new Map(); for (let i = 0; i < groups.length; i++) { findEdgesInGroup(groups, i, edges); } const sortedGroupIndexes = sortTopologically(originalOrder, edges); return sortedGroupIndexes.map(i => groups[i]); } export function sortMenuItems (menuItems: {type?: string, id?: string}[]) { const isSeparator = (item: {type?: string}) => item.type === 'separator'; const separators = menuItems.filter(i => i.type === 'separator'); // Split the items into their implicit groups based upon separators. const groups = splitArray(menuItems, isSeparator); const mergedGroups = mergeGroups(groups); const mergedGroupsWithSortedItems = mergedGroups.map(sortItemsInGroup); const sortedGroups = sortGroups(mergedGroupsWithSortedItems); const joined = joinArrays(sortedGroups, separators); return joined; }