menu-utils.ts 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180
  1. function splitArray<T> (arr: T[], predicate: (x: T) => boolean) {
  2. const result = arr.reduce((multi, item) => {
  3. const current = multi[multi.length - 1];
  4. if (predicate(item)) {
  5. if (current.length > 0) multi.push([]);
  6. } else {
  7. current.push(item);
  8. }
  9. return multi;
  10. }, [[]] as T[][]);
  11. if (result[result.length - 1].length === 0) {
  12. return result.slice(0, result.length - 1);
  13. }
  14. return result;
  15. }
  16. function joinArrays (arrays: any[][], joinIDs: any[]) {
  17. return arrays.reduce((joined, arr, i) => {
  18. if (i > 0 && arr.length) {
  19. if (joinIDs.length > 0) {
  20. joined.push(joinIDs[0]);
  21. joinIDs.splice(0, 1);
  22. } else {
  23. joined.push({ type: 'separator' });
  24. }
  25. }
  26. return joined.concat(arr);
  27. }, []);
  28. }
  29. function pushOntoMultiMap<K, V> (map: Map<K, V[]>, key: K, value: V) {
  30. if (!map.has(key)) {
  31. map.set(key, []);
  32. }
  33. map.get(key)!.push(value);
  34. }
  35. function indexOfGroupContainingID<T> (groups: {id?: T}[][], id: T, ignoreGroup: {id?: T}[]) {
  36. return groups.findIndex(
  37. candidateGroup =>
  38. candidateGroup !== ignoreGroup &&
  39. candidateGroup.some(
  40. candidateItem => candidateItem.id === id
  41. )
  42. );
  43. }
  44. // Sort nodes topologically using a depth-first approach. Encountered cycles
  45. // are broken.
  46. function sortTopologically<T> (originalOrder: T[], edgesById: Map<T, T[]>) {
  47. const sorted = [] as T[];
  48. const marked = new Set<T>();
  49. const visit = (mark: T) => {
  50. if (marked.has(mark)) return;
  51. marked.add(mark);
  52. const edges = edgesById.get(mark);
  53. if (edges != null) {
  54. for (const edge of edges) {
  55. visit(edge);
  56. }
  57. }
  58. sorted.push(mark);
  59. };
  60. for (const edge of originalOrder) {
  61. visit(edge);
  62. }
  63. return sorted;
  64. }
  65. function attemptToMergeAGroup<T> (groups: {before?: T[], after?: T[], id?: T}[][]) {
  66. for (let i = 0; i < groups.length; i++) {
  67. const group = groups[i];
  68. for (const item of group) {
  69. const toIDs = [...(item.before || []), ...(item.after || [])];
  70. for (const id of toIDs) {
  71. const index = indexOfGroupContainingID(groups, id, group);
  72. if (index === -1) continue;
  73. const mergeTarget = groups[index];
  74. mergeTarget.push(...group);
  75. groups.splice(i, 1);
  76. return true;
  77. }
  78. }
  79. }
  80. return false;
  81. }
  82. function mergeGroups<T> (groups: {before?: T[], after?: T[], id?: T}[][]) {
  83. let merged = true;
  84. while (merged) {
  85. merged = attemptToMergeAGroup(groups);
  86. }
  87. return groups;
  88. }
  89. function sortItemsInGroup<T> (group: {before?: T[], after?: T[], id?: T}[]) {
  90. const originalOrder = group.map((node, i) => i);
  91. const edges = new Map();
  92. const idToIndex = new Map(group.map((item, i) => [item.id, i]));
  93. for (const [i, item] of group.entries()) {
  94. if (item.before) {
  95. for (const toID of item.before) {
  96. const to = idToIndex.get(toID);
  97. if (to != null) {
  98. pushOntoMultiMap(edges, to, i);
  99. }
  100. }
  101. }
  102. if (item.after) {
  103. for (const toID of item.after) {
  104. const to = idToIndex.get(toID);
  105. if (to != null) {
  106. pushOntoMultiMap(edges, i, to);
  107. }
  108. }
  109. }
  110. }
  111. const sortedNodes = sortTopologically(originalOrder, edges);
  112. return sortedNodes.map(i => group[i]);
  113. }
  114. function findEdgesInGroup<T> (groups: {beforeGroupContaining?: T[], afterGroupContaining?: T[], id?: T}[][], i: number, edges: Map<any, any>) {
  115. const group = groups[i];
  116. for (const item of group) {
  117. if (item.beforeGroupContaining) {
  118. for (const id of item.beforeGroupContaining) {
  119. const to = indexOfGroupContainingID(groups, id, group);
  120. if (to !== -1) {
  121. pushOntoMultiMap(edges, to, i);
  122. return;
  123. }
  124. }
  125. }
  126. if (item.afterGroupContaining) {
  127. for (const id of item.afterGroupContaining) {
  128. const to = indexOfGroupContainingID(groups, id, group);
  129. if (to !== -1) {
  130. pushOntoMultiMap(edges, i, to);
  131. return;
  132. }
  133. }
  134. }
  135. }
  136. }
  137. function sortGroups<T> (groups: {id?: T}[][]) {
  138. const originalOrder = groups.map((item, i) => i);
  139. const edges = new Map();
  140. for (let i = 0; i < groups.length; i++) {
  141. findEdgesInGroup(groups, i, edges);
  142. }
  143. const sortedGroupIndexes = sortTopologically(originalOrder, edges);
  144. return sortedGroupIndexes.map(i => groups[i]);
  145. }
  146. export function sortMenuItems (menuItems: (Electron.MenuItemConstructorOptions | Electron.MenuItem)[]) {
  147. const isSeparator = (i: Electron.MenuItemConstructorOptions | Electron.MenuItem) => {
  148. const opts = i as Electron.MenuItemConstructorOptions;
  149. return i.type === 'separator' && !opts.before && !opts.after && !opts.beforeGroupContaining && !opts.afterGroupContaining;
  150. };
  151. const separators = menuItems.filter(isSeparator);
  152. // Split the items into their implicit groups based upon separators.
  153. const groups = splitArray(menuItems, isSeparator);
  154. const mergedGroups = mergeGroups(groups);
  155. const mergedGroupsWithSortedItems = mergedGroups.map(sortItemsInGroup);
  156. const sortedGroups = sortGroups(mergedGroupsWithSortedItems);
  157. const joined = joinArrays(sortedGroups, separators);
  158. return joined;
  159. }