menu-sort-helpers.js
1 // UTILS 2 3 function splitArray(arr, predicate) { 4 let lastArr = []; 5 const multiArr = [lastArr]; 6 arr.forEach(item => { 7 if (predicate(item)) { 8 if (lastArr.length > 0) { 9 lastArr = []; 10 multiArr.push(lastArr); 11 } 12 } else { 13 lastArr.push(item); 14 } 15 }); 16 return multiArr; 17 } 18 19 function joinArrays(arrays, joiner) { 20 const joinedArr = []; 21 arrays.forEach((arr, i) => { 22 if (i > 0 && arr.length > 0) { 23 joinedArr.push(joiner); 24 } 25 joinedArr.push(...arr); 26 }); 27 return joinedArr; 28 } 29 30 const pushOntoMultiMap = (map, key, value) => { 31 if (!map.has(key)) { 32 map.set(key, []); 33 } 34 map.get(key).push(value); 35 }; 36 37 function indexOfGroupContainingCommand(groups, command, ignoreGroup) { 38 return groups.findIndex( 39 candiateGroup => 40 candiateGroup !== ignoreGroup && 41 candiateGroup.some(candidateItem => candidateItem.command === command) 42 ); 43 } 44 45 // Sort nodes topologically using a depth-first approach. Encountered cycles 46 // are broken. 47 function sortTopologically(originalOrder, edgesById) { 48 const sorted = []; 49 const marked = new Set(); 50 51 function visit(id) { 52 if (marked.has(id)) { 53 // Either this node has already been placed, or we have encountered a 54 // cycle and need to exit. 55 return; 56 } 57 marked.add(id); 58 const edges = edgesById.get(id); 59 if (edges != null) { 60 edges.forEach(visit); 61 } 62 sorted.push(id); 63 } 64 65 originalOrder.forEach(visit); 66 return sorted; 67 } 68 69 function attemptToMergeAGroup(groups) { 70 for (let i = 0; i < groups.length; i++) { 71 const group = groups[i]; 72 for (const item of group) { 73 const toCommands = [...(item.before || []), ...(item.after || [])]; 74 for (const command of toCommands) { 75 const index = indexOfGroupContainingCommand(groups, command, group); 76 if (index === -1) { 77 // No valid edge for this command 78 continue; 79 } 80 const mergeTarget = groups[index]; 81 // Merge with group containing `command` 82 mergeTarget.push(...group); 83 groups.splice(i, 1); 84 return true; 85 } 86 } 87 } 88 return false; 89 } 90 91 // Merge groups based on before/after positions 92 // Mutates both the array of groups, and the individual group arrays. 93 function mergeGroups(groups) { 94 let mergedAGroup = true; 95 while (mergedAGroup) { 96 mergedAGroup = attemptToMergeAGroup(groups); 97 } 98 return groups; 99 } 100 101 function sortItemsInGroup(group) { 102 const originalOrder = group.map((node, i) => i); 103 const edges = new Map(); 104 const commandToIndex = new Map(group.map((item, i) => [item.command, i])); 105 106 group.forEach((item, i) => { 107 if (item.before) { 108 item.before.forEach(toCommand => { 109 const to = commandToIndex.get(toCommand); 110 if (to != null) { 111 pushOntoMultiMap(edges, to, i); 112 } 113 }); 114 } 115 if (item.after) { 116 item.after.forEach(toCommand => { 117 const to = commandToIndex.get(toCommand); 118 if (to != null) { 119 pushOntoMultiMap(edges, i, to); 120 } 121 }); 122 } 123 }); 124 125 const sortedNodes = sortTopologically(originalOrder, edges); 126 127 return sortedNodes.map(i => group[i]); 128 } 129 130 function findEdgesInGroup(groups, i, edges) { 131 const group = groups[i]; 132 for (const item of group) { 133 if (item.beforeGroupContaining) { 134 for (const command of item.beforeGroupContaining) { 135 const to = indexOfGroupContainingCommand(groups, command, group); 136 if (to !== -1) { 137 pushOntoMultiMap(edges, to, i); 138 return; 139 } 140 } 141 } 142 if (item.afterGroupContaining) { 143 for (const command of item.afterGroupContaining) { 144 const to = indexOfGroupContainingCommand(groups, command, group); 145 if (to !== -1) { 146 pushOntoMultiMap(edges, i, to); 147 return; 148 } 149 } 150 } 151 } 152 } 153 154 function sortGroups(groups) { 155 const originalOrder = groups.map((item, i) => i); 156 const edges = new Map(); 157 158 for (let i = 0; i < groups.length; i++) { 159 findEdgesInGroup(groups, i, edges); 160 } 161 162 const sortedGroupIndexes = sortTopologically(originalOrder, edges); 163 return sortedGroupIndexes.map(i => groups[i]); 164 } 165 166 function isSeparator(item) { 167 return item.type === 'separator'; 168 } 169 170 function sortMenuItems(menuItems) { 171 // Split the items into their implicit groups based upon separators. 172 const groups = splitArray(menuItems, isSeparator); 173 // Merge groups that contain before/after references to eachother. 174 const mergedGroups = mergeGroups(groups); 175 // Sort each individual group internally. 176 const mergedGroupsWithSortedItems = mergedGroups.map(sortItemsInGroup); 177 // Sort the groups based upon their beforeGroupContaining/afterGroupContaining 178 // references. 179 const sortedGroups = sortGroups(mergedGroupsWithSortedItems); 180 // Join the groups back 181 return joinArrays(sortedGroups, { type: 'separator' }); 182 } 183 184 module.exports = { sortMenuItems };