177 lines
4.3 KiB
JavaScript
177 lines
4.3 KiB
JavaScript
|
'use strict'
|
||
|
|
||
|
function splitArray (arr, predicate) {
|
||
|
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
|
||
|
}, [[]])
|
||
|
|
||
|
if (result[result.length - 1].length === 0) {
|
||
|
return result.slice(0, result.length - 1)
|
||
|
}
|
||
|
return result
|
||
|
}
|
||
|
|
||
|
function joinArrays (arrays, joinIDs) {
|
||
|
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, key, value) {
|
||
|
if (!map.has(key)) {
|
||
|
map.set(key, [])
|
||
|
}
|
||
|
map.get(key).push(value)
|
||
|
}
|
||
|
|
||
|
function indexOfGroupContainingID (groups, id, ignoreGroup) {
|
||
|
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, edgesById) {
|
||
|
const sorted = []
|
||
|
const marked = new Set()
|
||
|
|
||
|
const visit = (mark) => {
|
||
|
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) {
|
||
|
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) {
|
||
|
let merged = true
|
||
|
while (merged) {
|
||
|
merged = attemptToMergeAGroup(groups)
|
||
|
}
|
||
|
return groups
|
||
|
}
|
||
|
|
||
|
function sortItemsInGroup (group) {
|
||
|
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, i, edges) {
|
||
|
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) {
|
||
|
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])
|
||
|
}
|
||
|
|
||
|
function sortMenuItems (menuItems) {
|
||
|
const isSeparator = (item) => 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
|
||
|
}
|
||
|
|
||
|
module.exports = { sortMenuItems }
|