2019-08-27 19:10:29 +08:00
|
|
|
import cloneDeep from 'lodash/cloneDeep'
|
|
|
|
|
|
|
|
function isLeaf (option) {
|
|
|
|
if (option.isLeaf === true) {
|
|
|
|
return true
|
|
|
|
} else if (option.isLeaf === false) {
|
|
|
|
return false
|
|
|
|
} else if (hasChildren(option)) {
|
|
|
|
/**
|
|
|
|
* I don't take length into consideration because it may cause some problem
|
|
|
|
* when lazy load data
|
|
|
|
*/
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
function processedOption (option, activeIds, trackId) {
|
|
|
|
return {
|
|
|
|
...option,
|
|
|
|
active: activeIds.has(option.id),
|
|
|
|
hasChildren: hasChildren(option),
|
|
|
|
checkboxChecked: checkboxChecked(option),
|
|
|
|
checkboxIndeterminate: checkboxIndeterminate(option),
|
|
|
|
isLeaf: isLeaf(option),
|
2019-08-28 17:02:40 +08:00
|
|
|
tracked: tracked(option, trackId),
|
|
|
|
determined: !Number.isNaN(option.leafCount)
|
2019-08-27 19:10:29 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function tracked (option, trackId) {
|
|
|
|
return option.id === trackId
|
|
|
|
}
|
|
|
|
|
|
|
|
function checkboxChecked (option) {
|
|
|
|
if (option.type === 'multiple') {
|
2019-08-28 17:02:40 +08:00
|
|
|
if (option.isLeaf) {
|
2019-08-27 19:10:29 +08:00
|
|
|
return option.checked
|
2019-08-28 17:02:40 +08:00
|
|
|
} else {
|
|
|
|
if (Number.isNaN(option.leafCount)) {
|
|
|
|
return false
|
|
|
|
} else {
|
|
|
|
return option.leafCount === option.checkedLeafCount
|
|
|
|
}
|
2019-08-27 19:10:29 +08:00
|
|
|
}
|
|
|
|
} else {
|
|
|
|
return option.checked
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function checkboxIndeterminate (option) {
|
|
|
|
if (option.type === 'multiple') {
|
2019-08-28 17:02:40 +08:00
|
|
|
if (!option.isLeaf) {
|
|
|
|
return option.hasCheckedLeaf && !checkboxChecked(option)
|
|
|
|
}
|
2019-08-27 19:10:29 +08:00
|
|
|
} return false
|
|
|
|
}
|
|
|
|
|
|
|
|
function hasChildren (option) {
|
|
|
|
return Array.isArray(option.children)
|
|
|
|
}
|
|
|
|
|
|
|
|
function loaded (option) {
|
|
|
|
if (!isLeaf(option) && !hasChildren(option)) {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
function markAvailableSiblingIds (options) {
|
|
|
|
const length = options.length
|
2019-08-28 17:02:40 +08:00
|
|
|
if (length === 0) return
|
2019-08-27 19:10:29 +08:00
|
|
|
let lastAvailableOption = null
|
|
|
|
for (let i = 0; i < length; ++i) {
|
|
|
|
const option = options[i]
|
|
|
|
option.nextAvailableSiblingId = null
|
|
|
|
option.prevAvailableSiblingId = null
|
|
|
|
}
|
|
|
|
for (let i = 0; i <= length * 2; ++i) {
|
|
|
|
const option = options[i % length]
|
|
|
|
if (lastAvailableOption) {
|
|
|
|
option.prevAvailableSiblingId = lastAvailableOption.id
|
|
|
|
}
|
|
|
|
if (!option.disabled) {
|
|
|
|
lastAvailableOption = option
|
|
|
|
}
|
|
|
|
}
|
|
|
|
lastAvailableOption = null
|
|
|
|
for (let i = length * 2; i >= 0; --i) {
|
|
|
|
const option = options[i % length]
|
|
|
|
if (lastAvailableOption) {
|
|
|
|
option.nextAvailableSiblingId = lastAvailableOption.id
|
|
|
|
}
|
|
|
|
if (!option.disabled) {
|
|
|
|
lastAvailableOption = option
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function markFirstAvailableChildId (options) {
|
|
|
|
for (const option of options) {
|
|
|
|
if (option.isLeaf) {
|
|
|
|
option.firstAvailableChildId = null
|
|
|
|
} else {
|
|
|
|
if (hasChildren(option)) {
|
|
|
|
const firstChildOption = option.children[0]
|
|
|
|
if (firstChildOption) {
|
|
|
|
if (firstChildOption.disabled) {
|
|
|
|
option.firstAvailableChildId = firstChildOption.nextAvailableSiblingId
|
|
|
|
} else {
|
|
|
|
option.firstAvailableChildId = firstChildOption.id
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
option.firstAvailableChildId = null
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function availableParentId (parent, depth) {
|
|
|
|
if (!parent || parent.disabled || depth === 1) {
|
|
|
|
return null
|
|
|
|
}
|
|
|
|
return parent.id
|
|
|
|
}
|
|
|
|
|
|
|
|
function rootedOptions (options) {
|
|
|
|
return cloneDeep([{
|
2019-08-28 17:02:40 +08:00
|
|
|
isLeaf: false,
|
|
|
|
children: options || null
|
2019-08-27 19:10:29 +08:00
|
|
|
}])
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
*
|
|
|
|
* @param {*} options
|
|
|
|
* @param {Map} patches
|
|
|
|
*/
|
|
|
|
function patchedOptions (options, patches) {
|
2019-08-28 18:04:46 +08:00
|
|
|
// console.log('patchedOptions input', options, patches)
|
|
|
|
function traverse (options, depth = 0, parentId = 0) {
|
2019-08-27 19:10:29 +08:00
|
|
|
if (!Array.isArray(options)) return
|
2019-08-28 17:02:40 +08:00
|
|
|
for (let i = 0; i < options.length; ++i) {
|
|
|
|
const option = options[i]
|
2019-08-28 18:04:46 +08:00
|
|
|
const id = `${parentId}_${i + 1}`
|
2019-08-28 17:02:40 +08:00
|
|
|
// console.log('iterate on option', id)
|
2019-08-27 19:10:29 +08:00
|
|
|
if (!hasChildren(option)) {
|
2019-08-28 17:02:40 +08:00
|
|
|
if (patches.has(id)) {
|
2019-08-28 18:04:46 +08:00
|
|
|
// console.log('patched on option', id)
|
2019-08-28 17:02:40 +08:00
|
|
|
option.children = patches.get(id)
|
|
|
|
option.loaded = true
|
2019-08-27 19:10:29 +08:00
|
|
|
}
|
|
|
|
}
|
2019-08-28 18:04:46 +08:00
|
|
|
traverse(option.children, depth + 1, id)
|
2019-08-27 19:10:29 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
traverse(options)
|
2019-08-28 18:04:46 +08:00
|
|
|
// console.log('patchedOptions output', options)
|
2019-08-28 17:02:40 +08:00
|
|
|
return cloneDeep(options)
|
2019-08-27 19:10:29 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
function linkedCascaderOptions (options, type) {
|
|
|
|
const linkedCascaderOptions = options
|
|
|
|
const path = []
|
2019-08-28 18:04:46 +08:00
|
|
|
function traverse (options, parent = null, depth = 0, parentId = 0) {
|
2019-08-27 19:10:29 +08:00
|
|
|
if (!Array.isArray(options)) return
|
|
|
|
const length = options.length
|
|
|
|
for (let i = 0; i < length; ++i) {
|
|
|
|
const option = options[i]
|
|
|
|
if (depth > 0) path.push(option.label)
|
|
|
|
/**
|
|
|
|
* option.type determine option ui status
|
|
|
|
*/
|
|
|
|
option.type = type
|
|
|
|
/**
|
|
|
|
* options.availableParentId to support keyup left
|
|
|
|
*/
|
|
|
|
option.availableParentId = availableParentId(parent, depth)
|
|
|
|
/**
|
|
|
|
* option.depth to support find submenu
|
|
|
|
*/
|
|
|
|
option.depth = depth
|
|
|
|
/**
|
|
|
|
* options.id to suport track option
|
|
|
|
*/
|
2019-08-28 18:04:46 +08:00
|
|
|
option.id = `${parentId}_${i + 1}`
|
2019-08-27 19:10:29 +08:00
|
|
|
/**
|
|
|
|
* options.path to support ui status
|
|
|
|
*/
|
|
|
|
option.path = cloneDeep(path)
|
|
|
|
/**
|
|
|
|
* options.isLeaf to support ui status and lazy load
|
|
|
|
*/
|
|
|
|
option.isLeaf = isLeaf(option)
|
|
|
|
/**
|
|
|
|
* option.loaded to support lazy load
|
|
|
|
*/
|
|
|
|
option.loaded = loaded(option)
|
|
|
|
/**
|
|
|
|
* option.availableLeafCount to support ui status
|
|
|
|
* option.leafCount to support ui status
|
|
|
|
*/
|
|
|
|
if (!option.isLeaf) {
|
|
|
|
if (option.loaded) {
|
2019-08-28 18:04:46 +08:00
|
|
|
traverse(option.children, option, depth + 1, option.id)
|
2019-08-27 19:10:29 +08:00
|
|
|
option.leafCount = 0
|
|
|
|
option.availableLeafCount = 0
|
|
|
|
option.children.forEach(child => {
|
|
|
|
if (!child.disabled) {
|
|
|
|
option.availableLeafCount += child.availableLeafCount
|
|
|
|
}
|
|
|
|
option.leafCount += child.leafCount
|
|
|
|
})
|
|
|
|
} else {
|
|
|
|
option.availableLeafCount = NaN
|
|
|
|
option.leafCount = NaN
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
if (option.disabled) {
|
|
|
|
option.availableLeafCount = 0
|
|
|
|
} else {
|
|
|
|
option.availableLeafCount = 1
|
|
|
|
}
|
|
|
|
option.leafCount = 1
|
|
|
|
}
|
|
|
|
if (depth > 0) path.pop()
|
|
|
|
}
|
|
|
|
markAvailableSiblingIds(options)
|
|
|
|
markFirstAvailableChildId(options)
|
|
|
|
}
|
|
|
|
traverse(linkedCascaderOptions)
|
|
|
|
return linkedCascaderOptions
|
|
|
|
}
|
|
|
|
|
|
|
|
function menuOptions (linkedCascaderOptions, value, type) {
|
|
|
|
const valueSet = new Set(value)
|
|
|
|
const checkedOptions = []
|
|
|
|
function traverse (options, depth = 0) {
|
|
|
|
if (!Array.isArray(options)) return
|
|
|
|
const length = options.length
|
|
|
|
for (let i = 0; i < length; ++i) {
|
|
|
|
const option = options[i]
|
|
|
|
option.checkedLeafCount = 0
|
2019-08-28 17:02:40 +08:00
|
|
|
option.checkedAvailableLeafCount = 0
|
2019-08-27 19:10:29 +08:00
|
|
|
option.hasCheckedLeaf = false
|
|
|
|
if (type === 'multiple') {
|
|
|
|
if (option.loaded) {
|
|
|
|
if (!option.isLeaf) {
|
|
|
|
traverse(option.children, depth + 1)
|
|
|
|
option.children.forEach(child => {
|
|
|
|
option.checkedLeafCount += child.checkedLeafCount
|
2019-08-28 17:02:40 +08:00
|
|
|
option.checkedAvailableLeafCount += child.checkedAvailableLeafCount
|
2019-08-27 19:10:29 +08:00
|
|
|
option.hasCheckedLeaf = !!(option.hasCheckedLeaf || child.hasCheckedLeaf)
|
|
|
|
})
|
|
|
|
} else {
|
|
|
|
option.checked = valueSet.has(option.value)
|
|
|
|
if (option.checked) {
|
|
|
|
checkedOptions.push(option)
|
|
|
|
option.checkedLeafCount = 1
|
2019-08-28 17:02:40 +08:00
|
|
|
if (option.disabled) {
|
|
|
|
option.availableLeafCount = 0
|
|
|
|
} else {
|
|
|
|
option.availableLeafCount = 1
|
|
|
|
}
|
2019-08-27 19:10:29 +08:00
|
|
|
option.hasCheckedLeaf = true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
option.checkedLeafCount = NaN
|
2019-08-28 17:02:40 +08:00
|
|
|
option.checkedAvailableLeafCount = NaN
|
2019-08-27 19:10:29 +08:00
|
|
|
}
|
|
|
|
} else if (type === 'multiple-all-options') {
|
|
|
|
if (option.loaded && !option.isLeaf) {
|
|
|
|
traverse(option.children, option, depth + 1)
|
|
|
|
} else {
|
|
|
|
option.checkedLeafCount = NaN
|
|
|
|
}
|
|
|
|
option.checked = valueSet.has(option.value)
|
|
|
|
checkedOptions.push(option)
|
|
|
|
} else if (type === 'single' || type === 'single-all-options') {
|
|
|
|
if (hasChildren(option)) {
|
|
|
|
traverse(option.children, option, depth + 1)
|
|
|
|
}
|
|
|
|
option.checked = (option.value === value)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
traverse(linkedCascaderOptions)
|
2019-08-28 18:04:46 +08:00
|
|
|
// console.log('menuOptions', linkedCascaderOptions)
|
2019-08-27 19:10:29 +08:00
|
|
|
return linkedCascaderOptions
|
|
|
|
}
|
|
|
|
|
|
|
|
function optionPath (options, optionId) {
|
|
|
|
const path = []
|
|
|
|
if (optionId === null) return path
|
|
|
|
let done = false
|
|
|
|
function traverseOptions (options) {
|
|
|
|
if (!Array.isArray(options) || !options.length) return
|
|
|
|
for (const option of options) {
|
|
|
|
if (done) return
|
|
|
|
path.push(option)
|
|
|
|
if (option.id === optionId) {
|
|
|
|
done = true
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if (option.children) {
|
|
|
|
traverseOptions(option.children)
|
|
|
|
}
|
|
|
|
if (done) return
|
|
|
|
path.pop(option)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
traverseOptions(options)
|
|
|
|
return path
|
|
|
|
}
|
|
|
|
|
|
|
|
function menuModel (options, activeId, trackId) {
|
|
|
|
const activeOptionPath = optionPath(options, activeId)
|
|
|
|
const activeIds = new Set(activeOptionPath.map(option => option.id))
|
|
|
|
const firstSubmenu = options[0].children
|
2019-08-28 18:04:46 +08:00
|
|
|
// console.log('firstSubmenu', firstSubmenu)
|
|
|
|
console.log('menuModel', options, activeId, trackId, activeOptionPath)
|
2019-08-28 17:02:40 +08:00
|
|
|
const model = []
|
|
|
|
if (firstSubmenu !== null) {
|
|
|
|
model.push(firstSubmenu.map(option => {
|
|
|
|
return processedOption(option, activeIds, trackId)
|
|
|
|
}))
|
|
|
|
} else {
|
|
|
|
model.push([])
|
|
|
|
}
|
2019-08-27 19:10:29 +08:00
|
|
|
for (const option of activeOptionPath) {
|
|
|
|
/**
|
|
|
|
* pass root option
|
|
|
|
*/
|
|
|
|
if (option.depth === 0) continue
|
|
|
|
if (hasChildren(option)) {
|
|
|
|
model.push(option.children.map(option => {
|
|
|
|
return processedOption(option, activeIds, trackId)
|
|
|
|
}))
|
|
|
|
}
|
|
|
|
}
|
2019-08-28 18:04:46 +08:00
|
|
|
console.log('menuModel model', model)
|
2019-08-27 19:10:29 +08:00
|
|
|
return model
|
|
|
|
}
|
|
|
|
|
|
|
|
function firstOptionId (options) {
|
2019-08-28 18:04:46 +08:00
|
|
|
// console.log(options)
|
2019-08-27 19:10:29 +08:00
|
|
|
return options[0].firstAvailableChildId
|
|
|
|
}
|
|
|
|
|
|
|
|
export {
|
|
|
|
firstOptionId,
|
|
|
|
rootedOptions,
|
|
|
|
patchedOptions,
|
|
|
|
linkedCascaderOptions,
|
|
|
|
menuOptions,
|
|
|
|
menuModel
|
|
|
|
}
|