2019-08-28 22:37:43 +08:00
|
|
|
/**
|
|
|
|
* For Cascader Component to use
|
|
|
|
*/
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2019-08-28 19:19:31 +08:00
|
|
|
function processedOption (option, activeIds, trackId, loadingId) {
|
2019-08-27 19:10:29 +08:00
|
|
|
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),
|
2019-08-28 19:19:31 +08:00
|
|
|
determined: !Number.isNaN(option.leafCount),
|
|
|
|
loading: option.id === loadingId
|
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-09-15 10:45:34 +08:00
|
|
|
isRoot: true,
|
2019-08-28 17:02:40 +08:00
|
|
|
isLeaf: false,
|
2019-09-15 10:45:34 +08:00
|
|
|
key: Symbol('n-tree-root'),
|
2019-08-28 17:02:40 +08:00
|
|
|
children: options || null
|
2019-08-27 19:10:29 +08:00
|
|
|
}])
|
|
|
|
}
|
|
|
|
|
2019-09-15 10:45:34 +08:00
|
|
|
function patchedOptionsUsingProp (options, patches, prop = 'key') {
|
|
|
|
const patchesMap = new Map()
|
|
|
|
for (const [id, children] of patches) {
|
|
|
|
patchesMap.set(id, children)
|
|
|
|
}
|
|
|
|
function traverse (options) {
|
|
|
|
if (!Array.isArray(options)) return
|
|
|
|
for (let i = 0; i < options.length; ++i) {
|
|
|
|
const option = options[i]
|
|
|
|
const id = option[prop]
|
|
|
|
// console.log('iterate on option', id)
|
|
|
|
if (!hasChildren(option)) {
|
|
|
|
if (patches.has(id)) {
|
|
|
|
// console.log('patched on option', id)
|
|
|
|
option.children = patches.get(id)
|
|
|
|
option.loaded = true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
traverse(option.children)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
traverse(options)
|
|
|
|
// console.log('patchedOptions output', options)
|
|
|
|
return options
|
|
|
|
}
|
|
|
|
|
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
|
|
|
}
|
|
|
|
|
2019-09-15 10:45:34 +08:00
|
|
|
function dropIsValid ([sourceNode, targetNode, type]) {
|
|
|
|
if (sourceNode.key === targetNode.key) return false
|
|
|
|
if (type === 'append') {
|
|
|
|
if (targetNode.key === sourceNode.parent.key) return false
|
|
|
|
let parent = targetNode.parent
|
|
|
|
while (!parent.isRoot) {
|
|
|
|
if (parent.key === sourceNode.key) {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
parent = parent.parent
|
|
|
|
}
|
|
|
|
} else if (type === 'insertBefore' || type === 'insertAfter') {
|
|
|
|
let parent = targetNode.parent
|
|
|
|
while (!parent.isRoot) {
|
|
|
|
if (parent.key === sourceNode.key) {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
parent = parent.parent
|
|
|
|
}
|
|
|
|
} else return false
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
function treedOptions (options) {
|
|
|
|
const decoratedOptions = rootedOptions(options)
|
|
|
|
function traverse (root, parent = null) {
|
|
|
|
root.parent = parent
|
|
|
|
if (Array.isArray(root.children)) {
|
|
|
|
root.children.forEach(child => traverse(child, root))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
traverse(decoratedOptions[0])
|
|
|
|
return decoratedOptions
|
|
|
|
}
|
|
|
|
|
|
|
|
function applyDrop ([sourceNode, targetNode, type]) {
|
|
|
|
if (type === 'append') {
|
|
|
|
const parent = sourceNode.parent
|
|
|
|
const index = parent.children.findIndex(child => child.key === sourceNode.key)
|
|
|
|
if (~index) {
|
|
|
|
parent.children.splice(index, 1)
|
2019-09-15 13:31:26 +08:00
|
|
|
if (!parent.children.length) {
|
|
|
|
parent.children = null
|
|
|
|
parent.isLeaf = true
|
|
|
|
}
|
2019-09-15 10:45:34 +08:00
|
|
|
} else {
|
|
|
|
throw new Error('[n-tree]: switch error')
|
|
|
|
}
|
|
|
|
if (Array.isArray(targetNode.children)) {
|
|
|
|
targetNode.children.push(sourceNode)
|
|
|
|
} else {
|
2019-09-15 13:31:26 +08:00
|
|
|
targetNode.isLeaf = false
|
2019-09-15 10:45:34 +08:00
|
|
|
targetNode.children = [sourceNode]
|
|
|
|
}
|
|
|
|
sourceNode.parent = targetNode
|
|
|
|
} else if (type === 'insertBefore' || type === 'insertAfter') {
|
|
|
|
let parent = sourceNode.parent
|
|
|
|
const sourceIndex = parent.children.findIndex(child => child.key === sourceNode.key)
|
|
|
|
if (~sourceIndex) {
|
|
|
|
parent.children.splice(sourceIndex, 1)
|
2019-09-15 13:31:26 +08:00
|
|
|
if (!parent.children.length) {
|
|
|
|
parent.children = null
|
|
|
|
parent.isLeaf = true
|
|
|
|
}
|
2019-09-15 10:45:34 +08:00
|
|
|
} else {
|
|
|
|
throw new Error('[n-tree]: switch error')
|
|
|
|
}
|
|
|
|
parent = targetNode.parent
|
|
|
|
const targetIndex = parent.children.findIndex(child => child.key === targetNode.key)
|
|
|
|
if (~targetIndex) {
|
|
|
|
if (type === 'insertBefore') {
|
|
|
|
parent.children.splice(targetIndex, 0, sourceNode)
|
|
|
|
} else {
|
|
|
|
parent.children.splice(targetIndex + 1, 0, sourceNode)
|
|
|
|
}
|
2019-09-15 13:31:26 +08:00
|
|
|
if (!parent.children.length) {
|
|
|
|
parent.children = null
|
|
|
|
parent.isLeaf = true
|
|
|
|
}
|
2019-09-15 10:45:34 +08:00
|
|
|
} else {
|
|
|
|
throw new Error('[n-tree]: switch error')
|
|
|
|
}
|
|
|
|
sourceNode.parent = targetNode.parent
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
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 = []
|
2019-08-29 18:59:41 +08:00
|
|
|
function traverse (options) {
|
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]
|
|
|
|
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) {
|
2019-08-29 18:59:41 +08:00
|
|
|
traverse(option.children)
|
2019-08-27 19:10:29 +08:00
|
|
|
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) {
|
2019-08-29 18:59:41 +08:00
|
|
|
traverse(option.children)
|
2019-08-27 19:10:29 +08:00
|
|
|
} else {
|
|
|
|
option.checkedLeafCount = NaN
|
|
|
|
}
|
|
|
|
option.checked = valueSet.has(option.value)
|
|
|
|
checkedOptions.push(option)
|
|
|
|
} else if (type === 'single' || type === 'single-all-options') {
|
|
|
|
if (hasChildren(option)) {
|
2019-08-29 18:59:41 +08:00
|
|
|
traverse(option.children)
|
2019-08-27 19:10:29 +08:00
|
|
|
}
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2019-08-28 19:19:31 +08:00
|
|
|
function menuModel (options, activeId, trackId, loadingId) {
|
|
|
|
// console.log('menuModel params', options, activeId, trackId, loadingId)
|
2019-08-27 19:10:29 +08:00
|
|
|
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)
|
2019-08-28 19:19:31 +08:00
|
|
|
// 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 => {
|
2019-08-28 19:19:31 +08:00
|
|
|
return processedOption(option, activeIds, trackId, loadingId)
|
2019-08-28 17:02:40 +08:00
|
|
|
}))
|
|
|
|
} 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 => {
|
2019-08-28 19:19:31 +08:00
|
|
|
return processedOption(option, activeIds, trackId, loadingId)
|
2019-08-27 19:10:29 +08:00
|
|
|
}))
|
|
|
|
}
|
|
|
|
}
|
2019-08-28 19:19:31 +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,
|
2019-09-15 10:45:34 +08:00
|
|
|
patchedOptionsUsingProp,
|
|
|
|
dropIsValid,
|
|
|
|
applyDrop,
|
|
|
|
treedOptions,
|
2019-08-27 19:10:29 +08:00
|
|
|
linkedCascaderOptions,
|
|
|
|
menuOptions,
|
|
|
|
menuModel
|
|
|
|
}
|