menu-utils.js 4.3 KB

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