veui
Version:
Baidu Enterprise UI for Vue.js.
569 lines (528 loc) • 17.2 kB
JavaScript
import { includes, find, filter } from 'lodash'
import {
walk,
hasChildren,
getGroupDescendants,
getLeaves
} from '../utils/datasource'
const INDETERMINATE = 'include-indeterminate'
const call = (val, context) => (typeof val === 'function' ? val(context) : val)
const methods = {
/**
* 向参数 tree 加上 checked 信息
* @param {Array} tree 数据源
* @param {Array} checked 选中的 value 数组
* @return {Array} 返回原 tree,但标记了 checked 信息
*/
markChecked (tree, checked) {
let walker = {
enter: (item, context) => {
markAncestorInChecked(item, checked, context, this.treeChildrenKey)
},
exit: (item, context) => {
let { value, [this.treeChildrenKey]: children } = item
if (hasChildren(item, this.treeChildrenKey)) {
item.checked = children.every(({ checked }) => checked)
item.partialChecked =
!item.checked &&
children.some(
({ partialChecked, checked }) => !!partialChecked || checked
)
} else {
// 如果中间态不同步进 checked,那么祖先是选中,则下面的所有子孙节点都选中
let includeIndeterminate = this.strategy === INDETERMINATE
item.checked =
includes(checked, value) ||
(!includeIndeterminate && !!context.ancestorInChecked)
}
}
}
walk(tree, walker, this.treeChildrenKey)
return tree
},
/**
* toggle 数据源中某个项目(可能不是叶子节点)的选中状态
* @param {Array} prevChecked 当前的选中值,比如 [1,2,3], 然后需要 toggle value=3 的项目
* @param {object} item 被 toggle 的项目,如上面的 value=3 的项目
* @param {Array} parents 参数 item 的祖先数组,[topMost, 2ndTop]
* @param {Array} datasource 完整的数据源,用来normalize
*/
toggleItem (prevChecked, item, parents, datasource) {
let options = {
strategy: this.strategy,
childrenKey: this.treeChildrenKey
}
let newChecked = _toggleItem(prevChecked, item, parents, options)
return getNormalizedCheckedValues(
datasource,
newChecked,
prevChecked,
options
)
},
// 参见上面 toggle,这里是 toggle 中的选中操作,类似 transfer candidate 中选中(不能选中 hidden 的)
checkItem (prevChecked, item, parents, datasource) {
let options = {
strategy: this.strategy,
childrenKey: this.treeChildrenKey,
operation: 'check'
}
let newChecked = _toggleItem(prevChecked, item, parents, options)
return getNormalizedCheckedValues(
datasource,
newChecked,
prevChecked,
options
)
},
// 参见上面 toggle,这里是 toggle 中的取消操作,类似 transfer candidatePanel 中取消(不能取消 hidden 的)
uncheckItem (prevChecked, item, parents, datasource) {
let options = {
strategy: this.strategy,
childrenKey: this.treeChildrenKey,
operation: 'uncheck'
}
let newChecked = _toggleItem(prevChecked, item, parents, options)
return getNormalizedCheckedValues(
datasource,
newChecked,
prevChecked,
options
)
},
// 参见上面 uncheckItem,不同的是这里忽略 hidden 的影响,类似 transfer selectedPanel 中取消
clearItem (prevChecked, item, parents, datasource) {
let options = {
strategy: this.strategy,
childrenKey: this.treeChildrenKey,
operation: 'clear'
}
let newChecked = _toggleItem(prevChecked, item, parents, options)
return getNormalizedCheckedValues(
datasource,
newChecked,
prevChecked,
options
)
},
// transfer candidatePanel 中的全选(不能选中 hidden 的)
checkAll (prevChecked, wholeTreeWithMarkingHidden) {
let options = {
strategy: this.strategy,
childrenKey: this.treeChildrenKey,
operation: 'check'
}
return batch(
prevChecked,
wholeTreeWithMarkingHidden,
wholeTreeWithMarkingHidden,
options
)
},
clearAll (prevChecked, itemsToBatch, datasource) {
let options = {
strategy: this.strategy,
childrenKey: this.treeChildrenKey,
operation: 'clear'
}
return batch(prevChecked, itemsToBatch, datasource, options)
},
// 用原始 tree 和 checked 选中数据派生出选中的子树
getCheckedSubTree (tree, checked) {
const walker = {
enter: (item, context) => {
markAncestorInChecked(item, checked, context, this.treeChildrenKey)
},
exit: (item, context) => {
if (hasChildren(item, this.treeChildrenKey)) {
let children = context.childrenResult.filter((i) => !!i)
if (children.length) {
return {
...item,
[this.treeChildrenKey]: children
}
}
} else {
let includeIndeterminate = this.strategy === INDETERMINATE
let isChecked =
inChecked(checked, item) ||
(!includeIndeterminate && !!context.ancestorInChecked)
if (isChecked) {
return item
}
}
}
}
return walk(tree, walker, this.treeChildrenKey).filter((i) => !!i)
}
}
export default function treeFactory (options = {}) {
let {
supportIndeterminate,
childrenKey = 'children',
defaultMerge = 'keep-all'
} = options
// props
let props = {
mergeChecked: {
type: String,
default: defaultMerge,
validator (value) {
return includes(['keep-all', 'upwards', 'downwards'], value)
}
}
}
if (supportIndeterminate !== false) {
props.includeIndeterminate = Boolean
}
return {
props,
computed: {
strategy () {
if (this.mergeChecked === 'keep-all' && this.includeIndeterminate) {
return INDETERMINATE
}
return this.mergeChecked
},
treeChildrenKey () {
return call(childrenKey, this) // 直接应该是 children or options
}
},
methods
}
}
function getCheckedValues (
{ value, checked, children, partialChecked },
strategy,
childrenCheckedValues
) {
let includeSelf = checked && value != null
let checkedValues = []
switch (strategy) {
case 'keep-all':
if (includeSelf) {
checkedValues.push(value)
}
checkedValues.push(...childrenCheckedValues)
break
case INDETERMINATE:
if ((partialChecked || checked) && value != null) {
checkedValues.push(value)
}
checkedValues.push(...childrenCheckedValues)
break
case 'upwards':
checkedValues.push(...(includeSelf ? [value] : childrenCheckedValues))
break
case 'downwards':
includeSelf = includeSelf && (!children || !children.length)
checkedValues.push(...(includeSelf ? [value] : childrenCheckedValues))
break
}
return checkedValues
}
/**
* 重新生成一份符合当前 strategy 的选中数据
* @param {Array} datasource 全量数据
* @param {Array} checked 选中的数据
* @param {string} options.strategy 选中数据的合并策略
* @param {string} options.childrenKey 子项的字段名
* @return {Array} 符合当前 strategy 的选中数据
*/
function getNormalizedCheckedValues (
datasource,
checked,
prevChecked,
{ strategy, childrenKey }
) {
let walker = {
enter (item, context) {
markAncestorInChecked(item, checked, context, childrenKey)
},
exit (item, context) {
let { value, [childrenKey]: children } = item
let itemChecked = false
let partialChecked = false
let childrenCheckedValues = []
if (hasChildren(item, childrenKey)) {
itemChecked = context.childrenResult.every((i) => !!i.checked)
partialChecked =
!itemChecked &&
context.childrenResult.some(
({ checked, partialChecked }) => !!partialChecked || !!checked
)
childrenCheckedValues = context.childrenResult.reduce(
(res, { values }) => res.concat(values),
[]
)
} else {
// 如果中间态不同步进 checked,那么祖先是选中,则下面的所有子孙节点都选中
let includeIndeterminate = strategy === INDETERMINATE
itemChecked =
includes(checked, value) ||
(!includeIndeterminate && !!context.ancestorInChecked)
}
let values = getCheckedValues(
{ value, children, checked: itemChecked, partialChecked },
strategy,
childrenCheckedValues
)
return { checked: itemChecked, partialChecked, values }
}
}
let normalized = walk([{ [childrenKey]: datasource }], walker, childrenKey)[0]
.values
return respectSelectionOrder(normalized, prevChecked)
}
function batch (prevChecked, itemsToBatch, datasource, options) {
itemsToBatch = filter(
itemsToBatch,
({ hidden, disabled }) => !hidden && !disabled
)
let newChecked = itemsToBatch.reduce((prevChecked, item) => {
return _toggleItem(prevChecked, item, [], options)
}, prevChecked)
// 重新生成一份符合当前 strategy 的 checked
return getNormalizedCheckedValues(
datasource,
newChecked,
prevChecked,
options
)
}
/**
* 选中/取消一个 item 后得到 checked 数据(一般就是 emit 出去的 checked prop)
* @param {Array} prevChecked 之前选中的数据
* @param {object} item 当前被操作的项目
* @param {Array} parents 祖先链,从最高的开始
* @param {string} option.strategy 选中值合并策略
* @param {string} option.operation toggle/check/uncheck/clear
* 操作:check(选中参数中的item),uncheck(取消参数中的item),clear清空非disabled,默认是toggle
* @param {string} option.childrenKey 子项的字段名
*/
function _toggleItem (
prevChecked,
item,
parents,
{ strategy, operation, childrenKey }
) {
let checked
let isGroup = hasChildren(item, childrenKey)
prevChecked = prevChecked || []
let clear = operation === 'clear'
operation = clear ? 'uncheck' : operation
if (isGroup) {
// 先尝试全部取消(如果有可能取消的叶子节点的话)-> 否则再尝试全部选中
let leaves
if (!operation || isUncheck(operation)) {
leaves = clear
? getEnabledLeavesFrom(item, childrenKey)
: getEnabledCheckedLeavesFrom(item, childrenKey)
if (!operation && leaves.length) {
operation = 'uncheck'
}
}
if (!operation || !isUncheck(operation)) {
leaves = getEnabledUncheckedLeavesFrom(item, childrenKey)
operation = 'check'
}
checked = isUncheck(operation)
? uncheckGroup(prevChecked, item, parents, {
strategy,
leaves,
childrenKey
})
: checkGroup(prevChecked, leaves)
} else {
operation = operation || (item.checked ? 'uncheck' : 'check')
checked = isUncheck(operation)
? uncheckLeaf(prevChecked, item, parents, { strategy, childrenKey })
: checkLeaf(prevChecked, item)
}
return checked
}
function checkLeaf (prevChecked, item) {
return [...prevChecked, item.value]
}
function uncheckLeaf (prevChecked, item, parents, { strategy, childrenKey }) {
let includeIndeterminate = strategy === INDETERMINATE
let parentValues = parents.map((i) => i.value).filter((val) => val != null)
let willUncheck = [item.value, ...parentValues] // 可能多删掉中间态的 parent,会在后面的 getNormalizedCheckedValues 中重新推导出来
let mostTopAncestor = find(parents, ({ value }) =>
includes(prevChecked, value)
)
let realChecked = !includeIndeterminate
? [
...prevChecked,
...(mostTopAncestor ? getLeaves(mostTopAncestor, childrenKey) : [])
]
: prevChecked
return realChecked.filter((i) => !includes(willUncheck, i))
}
function checkGroup (prevChecked, leaves) {
return [...prevChecked, ...leaves]
}
function uncheckGroup (
prevChecked,
item,
parents,
{ strategy, leaves, childrenKey }
) {
let includeIndeterminate = strategy === INDETERMINATE
let parentValues = parents.map((i) => i.value).filter((val) => val != null)
let willUncheck = [
...leaves,
...getGroupDescendants(item, childrenKey),
...(item.value != null ? [item.value] : []),
...parentValues // 可能多删掉中间态的 parent,会在后面的 getNormalizedCheckedValues 中重新推导出来
]
// replaceCheckedGroupWithLeaves 的原因:
// 上面会把 item 祖先和后代中是 group 的都删掉了,会导致两个问题:
// 1. 删除祖先可能在 branch 策略下把所有的都删除了
// 2. 删除 group 后代了,但是该 group 下可能有 disabled 叶子,在 branch 策略也会导致多删除
// 所以,需要将所有 group 删掉,但是补上等价的叶子节点,
// 同时希望最大程度兼容数据缺失但逻辑完整的情况,所以在 all/leaf/branch 策略下 都做了这个补齐
// 数据缺失但逻辑完整的情况:
// 在 all/leaf/branch 策略下,父或子节点只要一方数据完整,另一方数据可以推导出来,所以逻辑是完整的
let realChecked = !includeIndeterminate
? [
...prevChecked,
...replaceCheckedGroupWithLeaves(
prevChecked,
parents,
item,
childrenKey
)
]
: prevChecked
return realChecked.filter((i) => !includes(willUncheck, i))
}
function replaceCheckedGroupWithLeaves (
prevChecked,
parents,
self,
childrenKey
) {
let mostTopAncestor = find(parents, (p) => inChecked(prevChecked, p))
// 祖先有选中的,那么直接替换最高祖先
if (mostTopAncestor) {
return getLeaves(mostTopAncestor, childrenKey)
}
// 自身是选中,直接替换自身
if (inChecked(prevChecked, self)) {
return getLeaves(self, childrenKey)
}
// 遍历替换后代选中的 group 节点
let result = []
walk(self, (item, { skip }) => {
if (hasChildren(item, childrenKey) && inChecked(prevChecked, item)) {
result.push(...getLeaves(item, childrenKey))
// 该节点以下已经替换过了
skip()
}
})
return result
}
function getLeavesByContext (item, initialContext, childrenKey) {
let result = []
walk(
item,
(child, { setContext, skip, checked, hidden }) => {
// 1. 只要 checked 叶子,disabled 或 disabled 父下的不要
// 2. hidden条件:不是 true 的叶子,或者 leafAncestor 下的叶子
let { disabled, value } = child
if (hasChildren(child, childrenKey)) {
skip(disabled)
if (!disabled && hidden != null && isLeafAncestor(child, childrenKey)) {
setContext({ hidden: null })
}
return
}
if (
!disabled &&
valueMatches(child.checked, checked) &&
valueMatches(child.hidden, hidden)
) {
result.push(value)
}
},
childrenKey,
initialContext
)
return result
}
// under: 自身不会包含, from 自身如果是叶子会包含在结果中
// 获取已选中的叶子,如果能获取到,那么可以做取消操作
function getEnabledCheckedLeavesFrom (item, childrenKey) {
return getLeavesByContext(
[item],
{ checked: true, hidden: false },
childrenKey
)
}
// 获取未选中的叶子,如果能获取到,那么可以做选中操作
function getEnabledUncheckedLeavesFrom (item, childrenKey) {
return getLeavesByContext(
[item],
{ checked: false, hidden: false },
childrenKey
)
}
function getEnabledLeavesFrom (item, childrenKey) {
return getLeavesByContext(
[item],
{ checked: null, hidden: null },
childrenKey
)
}
function markAncestorInChecked (
item,
checked,
{ ancestorInChecked, setContext },
childrenKey
) {
if (hasChildren(item, childrenKey) && !ancestorInChecked) {
if (inChecked(checked, item)) {
setContext({ ancestorInChecked: true })
}
}
}
function isUncheck (operation) {
return operation === 'uncheck'
}
function isLeafAncestor (item, childrenKey) {
return (
!item.hidden &&
hasChildren(item, childrenKey) &&
item[childrenKey].every((i) => !!i.hidden)
)
}
/**
* 检查 itemValue 是否匹配 targetValue
* 当 targetValue == null 表示目标无要求,itemValue 始终满足
* @param {*} itemValue 被匹配的值
* @param {boolean | null | undefined} targetValue 目标值
* @return boolean 是否匹配
*/
function valueMatches (itemValue, targetValue) {
return targetValue == null || targetValue === Boolean(itemValue)
}
function inChecked (checked, item) {
return item.value != null && includes(checked, item.value)
}
function respectSelectionOrder (checked, prevChecked) {
if (checked.length && prevChecked && prevChecked.length) {
let stillChecked = []
let newChecked = []
checked.forEach((ck) => {
let index = prevChecked.indexOf(ck)
if (index >= 0) {
stillChecked[index] = ck
} else {
newChecked.push(ck)
}
})
return stillChecked.filter((i) => i != null).concat(newChecked)
}
return checked
}