parent
62964ceb6f
commit
5ea54b104a
After Width: | Height: | Size: 11 KiB |
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 45 B |
After Width: | Height: | Size: 381 B |
@ -0,0 +1,3 @@
|
||||
'use strict'
|
||||
|
||||
module.exports = require('./src/')
|
@ -0,0 +1,610 @@
|
||||
import PropTypes from '../../_util/vue-types'
|
||||
import classNames from 'classnames'
|
||||
import warning from 'warning'
|
||||
import { initDefaultProps, getOptionProps, filterEmpty } from '../../_util/props-util'
|
||||
import {
|
||||
traverseTreeNodes, getStrictlyValue,
|
||||
getFullKeyList, getPosition, getDragNodesKeys,
|
||||
calcExpandedKeys, calcSelectedKeys,
|
||||
calcCheckedKeys, calcDropPosition,
|
||||
arrAdd, arrDel, posToArr,
|
||||
} from './util'
|
||||
|
||||
/**
|
||||
* Thought we still use `cloneElement` to pass `key`,
|
||||
* other props can pass with context for future refactor.
|
||||
*/
|
||||
export const contextTypes = {
|
||||
rcTree: PropTypes.shape({
|
||||
root: PropTypes.object,
|
||||
|
||||
prefixCls: PropTypes.string,
|
||||
selectable: PropTypes.bool,
|
||||
showIcon: PropTypes.bool,
|
||||
draggable: PropTypes.bool,
|
||||
checkable: PropTypes.oneOfType([
|
||||
PropTypes.bool,
|
||||
PropTypes.object,
|
||||
]),
|
||||
checkStrictly: PropTypes.bool,
|
||||
disabled: PropTypes.bool,
|
||||
openTransitionName: PropTypes.string,
|
||||
openAnimation: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
|
||||
|
||||
loadData: PropTypes.func,
|
||||
filterTreeNode: PropTypes.func,
|
||||
renderTreeNode: PropTypes.func,
|
||||
|
||||
isKeyChecked: PropTypes.func,
|
||||
|
||||
// onNodeExpand: PropTypes.func,
|
||||
// onNodeSelect: PropTypes.func,
|
||||
// onNodeMouseEnter: PropTypes.func,
|
||||
// onNodeMouseLeave: PropTypes.func,
|
||||
// onNodeContextMenu: PropTypes.func,
|
||||
// onNodeDragStart: PropTypes.func,
|
||||
// onNodeDragEnter: PropTypes.func,
|
||||
// onNodeDragOver: PropTypes.func,
|
||||
// onNodeDragLeave: PropTypes.func,
|
||||
// onNodeDragEnd: PropTypes.func,
|
||||
// onNodeDrop: PropTypes.func,
|
||||
// onBatchNodeCheck: PropTypes.func,
|
||||
// onCheckConductFinished: PropTypes.func,
|
||||
}),
|
||||
}
|
||||
|
||||
const Tree = {
|
||||
props: initDefaultProps({
|
||||
prefixCls: PropTypes.string,
|
||||
showLine: PropTypes.bool,
|
||||
showIcon: PropTypes.bool,
|
||||
focusable: PropTypes.bool,
|
||||
selectable: PropTypes.bool,
|
||||
disabled: PropTypes.bool,
|
||||
multiple: PropTypes.bool,
|
||||
checkable: PropTypes.oneOfType([
|
||||
PropTypes.bool,
|
||||
PropTypes.node,
|
||||
]),
|
||||
checkStrictly: PropTypes.bool,
|
||||
draggable: PropTypes.bool,
|
||||
autoExpandParent: PropTypes.bool,
|
||||
defaultExpandAll: PropTypes.bool,
|
||||
defaultExpandedKeys: PropTypes.arrayOf(PropTypes.string),
|
||||
expandedKeys: PropTypes.arrayOf(PropTypes.string),
|
||||
defaultCheckedKeys: PropTypes.arrayOf(PropTypes.string),
|
||||
checkedKeys: PropTypes.oneOfType([
|
||||
PropTypes.arrayOf(PropTypes.string),
|
||||
PropTypes.object,
|
||||
]),
|
||||
defaultSelectedKeys: PropTypes.arrayOf(PropTypes.string),
|
||||
selectedKeys: PropTypes.arrayOf(PropTypes.string),
|
||||
//onExpand: PropTypes.func,
|
||||
//onCheck: PropTypes.func,
|
||||
//onSelect: PropTypes.func,
|
||||
loadData: PropTypes.func,
|
||||
// onMouseEnter: PropTypes.func,
|
||||
// onMouseLeave: PropTypes.func,
|
||||
// onRightClick: PropTypes.func,
|
||||
// onDragStart: PropTypes.func,
|
||||
// onDragEnter: PropTypes.func,
|
||||
// onDragOver: PropTypes.func,
|
||||
// onDragLeave: PropTypes.func,
|
||||
// onDragEnd: PropTypes.func,
|
||||
// onDrop: PropTypes.func,
|
||||
filterTreeNode: PropTypes.func,
|
||||
openTransitionName: PropTypes.string,
|
||||
openAnimation: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
|
||||
}, {
|
||||
prefixCls: 'rc-tree',
|
||||
showLine: false,
|
||||
showIcon: true,
|
||||
selectable: true,
|
||||
multiple: false,
|
||||
checkable: false,
|
||||
disabled: false,
|
||||
checkStrictly: false,
|
||||
draggable: false,
|
||||
autoExpandParent: true,
|
||||
defaultExpandAll: false,
|
||||
defaultExpandedKeys: [],
|
||||
defaultCheckedKeys: [],
|
||||
defaultSelectedKeys: [],
|
||||
}),
|
||||
|
||||
// static childContextTypes = contextTypes;
|
||||
|
||||
data () {
|
||||
const props = getOptionProps(this)
|
||||
const {
|
||||
defaultExpandAll,
|
||||
defaultExpandedKeys,
|
||||
defaultCheckedKeys,
|
||||
defaultSelectedKeys,
|
||||
} = props
|
||||
|
||||
// Sync state with props
|
||||
const { checkedKeys = [], halfCheckedKeys = [] } =
|
||||
calcCheckedKeys(defaultCheckedKeys, props) || {}
|
||||
|
||||
// Cache for check status to optimize
|
||||
this.checkedBatch = null
|
||||
|
||||
return {
|
||||
sExpandedKeys: defaultExpandAll
|
||||
? getFullKeyList(this.$slots.default)
|
||||
: calcExpandedKeys(defaultExpandedKeys, props),
|
||||
sSelectedKeys: calcSelectedKeys(defaultSelectedKeys, props),
|
||||
sCheckedKeys,
|
||||
sHalfCheckedKeys,
|
||||
|
||||
...(this.getSyncProps(props) || {}),
|
||||
}
|
||||
},
|
||||
provide: {
|
||||
rcTree: this,
|
||||
},
|
||||
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
// React 16 will not trigger update if new state is null
|
||||
this.setState(this.getSyncProps(nextProps, this.props))
|
||||
},
|
||||
|
||||
onNodeDragStart (event, node) {
|
||||
const { expandedKeys } = this.state
|
||||
const { onDragStart } = this.props
|
||||
const { eventKey, children } = node.props
|
||||
|
||||
this.dragNode = node
|
||||
|
||||
this.setState({
|
||||
dragNodesKeys: getDragNodesKeys(children, node),
|
||||
expandedKeys: arrDel(expandedKeys, eventKey),
|
||||
})
|
||||
|
||||
if (onDragStart) {
|
||||
onDragStart({ event, node })
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* [Legacy] Select handler is less small than node,
|
||||
* so that this will trigger when drag enter node or select handler.
|
||||
* This is a little tricky if customize css without padding.
|
||||
* Better for use mouse move event to refresh drag state.
|
||||
* But let's just keep it to avoid event trigger logic change.
|
||||
*/
|
||||
onNodeDragEnter = (event, node) => {
|
||||
const { expandedKeys } = this.state
|
||||
const { onDragEnter } = this.props
|
||||
const { pos, eventKey } = node.props
|
||||
|
||||
const dropPosition = calcDropPosition(event, node)
|
||||
|
||||
// Skip if drag node is self
|
||||
if (
|
||||
this.dragNode.props.eventKey === eventKey &&
|
||||
dropPosition === 0
|
||||
) {
|
||||
this.setState({
|
||||
dragOverNodeKey: '',
|
||||
dropPosition: null,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Ref: https://github.com/react-component/tree/issues/132
|
||||
// Add timeout to let onDragLevel fire before onDragEnter,
|
||||
// so that we can clean drag props for onDragLeave node.
|
||||
// Macro task for this:
|
||||
// https://html.spec.whatwg.org/multipage/webappapis.html#clean-up-after-running-script
|
||||
setTimeout(() => {
|
||||
// Update drag over node
|
||||
this.setState({
|
||||
dragOverNodeKey: eventKey,
|
||||
dropPosition,
|
||||
})
|
||||
|
||||
// Side effect for delay drag
|
||||
if (!this.delayedDragEnterLogic) {
|
||||
this.delayedDragEnterLogic = {}
|
||||
}
|
||||
Object.keys(this.delayedDragEnterLogic).forEach((key) => {
|
||||
clearTimeout(this.delayedDragEnterLogic[key])
|
||||
})
|
||||
this.delayedDragEnterLogic[pos] = setTimeout(() => {
|
||||
const newExpandedKeys = arrAdd(expandedKeys, eventKey)
|
||||
this.setState({
|
||||
expandedKeys: newExpandedKeys,
|
||||
})
|
||||
|
||||
if (onDragEnter) {
|
||||
onDragEnter({ event, node, expandedKeys: newExpandedKeys })
|
||||
}
|
||||
}, 400)
|
||||
}, 0)
|
||||
};
|
||||
onNodeDragOver = (event, node) => {
|
||||
const { onDragOver } = this.props
|
||||
if (onDragOver) {
|
||||
onDragOver({ event, node })
|
||||
}
|
||||
};
|
||||
onNodeDragLeave = (event, node) => {
|
||||
const { onDragLeave } = this.props
|
||||
|
||||
this.setState({
|
||||
dragOverNodeKey: '',
|
||||
})
|
||||
|
||||
if (onDragLeave) {
|
||||
onDragLeave({ event, node })
|
||||
}
|
||||
};
|
||||
onNodeDragEnd = (event, node) => {
|
||||
const { onDragEnd } = this.props
|
||||
this.setState({
|
||||
dragOverNodeKey: '',
|
||||
})
|
||||
if (onDragEnd) {
|
||||
onDragEnd({ event, node })
|
||||
}
|
||||
};
|
||||
onNodeDrop = (event, node) => {
|
||||
const { dragNodesKeys, dropPosition } = this.state
|
||||
const { onDrop } = this.props
|
||||
const { eventKey, pos } = node.props
|
||||
|
||||
this.setState({
|
||||
dragOverNodeKey: '',
|
||||
dropNodeKey: eventKey,
|
||||
})
|
||||
|
||||
if (dragNodesKeys.indexOf(eventKey) !== -1) {
|
||||
warning(false, 'Can not drop to dragNode(include it\'s children node)')
|
||||
return
|
||||
}
|
||||
|
||||
const posArr = posToArr(pos)
|
||||
|
||||
const dropResult = {
|
||||
event,
|
||||
node,
|
||||
dragNode: this.dragNode,
|
||||
dragNodesKeys: dragNodesKeys.slice(),
|
||||
dropPosition: dropPosition + Number(posArr[posArr.length - 1]),
|
||||
}
|
||||
|
||||
if (dropPosition !== 0) {
|
||||
dropResult.dropToGap = true
|
||||
}
|
||||
|
||||
if (onDrop) {
|
||||
onDrop(dropResult)
|
||||
}
|
||||
};
|
||||
|
||||
onNodeSelect = (e, treeNode) => {
|
||||
let { selectedKeys } = this.state
|
||||
const { onSelect, multiple, children } = this.props
|
||||
const { selected, eventKey } = treeNode.props
|
||||
const targetSelected = !selected
|
||||
|
||||
// Update selected keys
|
||||
if (!targetSelected) {
|
||||
selectedKeys = arrDel(selectedKeys, eventKey)
|
||||
} else if (!multiple) {
|
||||
selectedKeys = [eventKey]
|
||||
} else {
|
||||
selectedKeys = arrAdd(selectedKeys, eventKey)
|
||||
}
|
||||
|
||||
// [Legacy] Not found related usage in doc or upper libs
|
||||
// [Legacy] TODO: add optimize prop to skip node process
|
||||
const selectedNodes = []
|
||||
if (selectedKeys.length) {
|
||||
traverseTreeNodes(children, ({ node, key }) => {
|
||||
if (selectedKeys.indexOf(key) !== -1) {
|
||||
selectedNodes.push(node)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
this.setUncontrolledState({ selectedKeys })
|
||||
|
||||
if (onSelect) {
|
||||
const eventObj = {
|
||||
event: 'select',
|
||||
selected: targetSelected,
|
||||
node: treeNode,
|
||||
selectedNodes,
|
||||
}
|
||||
onSelect(selectedKeys, eventObj)
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* This will cache node check status to optimize update process.
|
||||
* When Tree get trigger `onCheckConductFinished` will flush all the update.
|
||||
*/
|
||||
onBatchNodeCheck = (key, checked, halfChecked, startNode) => {
|
||||
if (startNode) {
|
||||
this.checkedBatch = {
|
||||
treeNode: startNode,
|
||||
checked,
|
||||
list: [],
|
||||
}
|
||||
}
|
||||
|
||||
// This code should never called
|
||||
if (!this.checkedBatch) {
|
||||
this.checkedBatch = {
|
||||
list: [],
|
||||
}
|
||||
warning(
|
||||
false,
|
||||
'Checked batch not init. This should be a bug. Please fire a issue.'
|
||||
)
|
||||
}
|
||||
|
||||
this.checkedBatch.list.push({ key, checked, halfChecked })
|
||||
};
|
||||
|
||||
/**
|
||||
* When top `onCheckConductFinished` called, will execute all batch update.
|
||||
* And trigger `onCheck` event.
|
||||
*/
|
||||
onCheckConductFinished = () => {
|
||||
const { checkedKeys, halfCheckedKeys } = this.state
|
||||
const { onCheck, checkStrictly, children } = this.props
|
||||
|
||||
// Use map to optimize update speed
|
||||
const checkedKeySet = {}
|
||||
const halfCheckedKeySet = {}
|
||||
|
||||
checkedKeys.forEach(key => {
|
||||
checkedKeySet[key] = true
|
||||
})
|
||||
halfCheckedKeys.forEach(key => {
|
||||
halfCheckedKeySet[key] = true
|
||||
})
|
||||
|
||||
// Batch process
|
||||
this.checkedBatch.list.forEach(({ key, checked, halfChecked }) => {
|
||||
checkedKeySet[key] = checked
|
||||
halfCheckedKeySet[key] = halfChecked
|
||||
})
|
||||
const newCheckedKeys = Object.keys(checkedKeySet).filter(key => checkedKeySet[key])
|
||||
const newHalfCheckedKeys = Object.keys(halfCheckedKeySet).filter(key => halfCheckedKeySet[key])
|
||||
|
||||
// Trigger onChecked
|
||||
let selectedObj
|
||||
|
||||
const eventObj = {
|
||||
event: 'check',
|
||||
node: this.checkedBatch.treeNode,
|
||||
checked: this.checkedBatch.checked,
|
||||
}
|
||||
|
||||
if (checkStrictly) {
|
||||
selectedObj = getStrictlyValue(newCheckedKeys, newHalfCheckedKeys)
|
||||
|
||||
// [Legacy] TODO: add optimize prop to skip node process
|
||||
eventObj.checkedNodes = []
|
||||
traverseTreeNodes(children, ({ node, key }) => {
|
||||
if (checkedKeySet[key]) {
|
||||
eventObj.checkedNodes.push(node)
|
||||
}
|
||||
})
|
||||
|
||||
this.setUncontrolledState({ checkedKeys: newCheckedKeys })
|
||||
} else {
|
||||
selectedObj = newCheckedKeys
|
||||
|
||||
// [Legacy] TODO: add optimize prop to skip node process
|
||||
eventObj.checkedNodes = []
|
||||
eventObj.checkedNodesPositions = [] // [Legacy] TODO: not in API
|
||||
eventObj.halfCheckedKeys = newHalfCheckedKeys // [Legacy] TODO: not in API
|
||||
traverseTreeNodes(children, ({ node, pos, key }) => {
|
||||
if (checkedKeySet[key]) {
|
||||
eventObj.checkedNodes.push(node)
|
||||
eventObj.checkedNodesPositions.push({ node, pos })
|
||||
}
|
||||
})
|
||||
|
||||
this.setUncontrolledState({
|
||||
checkedKeys: newCheckedKeys,
|
||||
halfCheckedKeys: newHalfCheckedKeys,
|
||||
})
|
||||
}
|
||||
|
||||
if (onCheck) {
|
||||
onCheck(selectedObj, eventObj)
|
||||
}
|
||||
|
||||
// Clean up
|
||||
this.checkedBatch = null
|
||||
};
|
||||
|
||||
onNodeExpand = (e, treeNode) => {
|
||||
let { expandedKeys } = this.state
|
||||
const { onExpand, loadData } = this.props
|
||||
const { eventKey, expanded } = treeNode.props
|
||||
|
||||
// Update selected keys
|
||||
const index = expandedKeys.indexOf(eventKey)
|
||||
const targetExpanded = !expanded
|
||||
|
||||
warning(
|
||||
(expanded && index !== -1) || (!expanded && index === -1)
|
||||
, 'Expand state not sync with index check')
|
||||
|
||||
if (targetExpanded) {
|
||||
expandedKeys = arrAdd(expandedKeys, eventKey)
|
||||
} else {
|
||||
expandedKeys = arrDel(expandedKeys, eventKey)
|
||||
}
|
||||
|
||||
this.setUncontrolledState({ expandedKeys })
|
||||
|
||||
if (onExpand) {
|
||||
onExpand(expandedKeys, { node: treeNode, expanded: targetExpanded })
|
||||
}
|
||||
|
||||
// Async Load data
|
||||
if (targetExpanded && loadData) {
|
||||
return loadData(treeNode).then(() => {
|
||||
// [Legacy] Refresh logic
|
||||
this.setUncontrolledState({ expandedKeys })
|
||||
})
|
||||
}
|
||||
|
||||
return null
|
||||
};
|
||||
|
||||
onNodeMouseEnter = (event, node) => {
|
||||
const { onMouseEnter } = this.props
|
||||
if (onMouseEnter) {
|
||||
onMouseEnter({ event, node })
|
||||
}
|
||||
};
|
||||
|
||||
onNodeMouseLeave = (event, node) => {
|
||||
const { onMouseLeave } = this.props
|
||||
if (onMouseLeave) {
|
||||
onMouseLeave({ event, node })
|
||||
}
|
||||
};
|
||||
|
||||
onNodeContextMenu = (event, node) => {
|
||||
const { onRightClick } = this.props
|
||||
if (onRightClick) {
|
||||
event.preventDefault()
|
||||
onRightClick({ event, node })
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Sync state with props if needed
|
||||
*/
|
||||
getSyncProps = (props = {}, prevProps) => {
|
||||
let needSync = false
|
||||
const newState = {}
|
||||
const myPrevProps = prevProps || {}
|
||||
|
||||
function checkSync (name) {
|
||||
if (props[name] !== myPrevProps[name]) {
|
||||
needSync = true
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Children change will affect check box status.
|
||||
// And no need to check when prev props not provided
|
||||
if (prevProps && checkSync('children')) {
|
||||
const { checkedKeys = [], halfCheckedKeys = [] } =
|
||||
calcCheckedKeys(props.checkedKeys || this.state.checkedKeys, props) || {}
|
||||
newState.checkedKeys = checkedKeys
|
||||
newState.halfCheckedKeys = halfCheckedKeys
|
||||
}
|
||||
|
||||
if (checkSync('expandedKeys')) {
|
||||
newState.expandedKeys = calcExpandedKeys(props.expandedKeys, props)
|
||||
}
|
||||
|
||||
if (checkSync('selectedKeys')) {
|
||||
newState.selectedKeys = calcSelectedKeys(props.selectedKeys, props)
|
||||
}
|
||||
|
||||
if (checkSync('checkedKeys')) {
|
||||
const { checkedKeys = [], halfCheckedKeys = [] } =
|
||||
calcCheckedKeys(props.checkedKeys, props) || {}
|
||||
newState.checkedKeys = checkedKeys
|
||||
newState.halfCheckedKeys = halfCheckedKeys
|
||||
}
|
||||
|
||||
return needSync ? newState : null
|
||||
};
|
||||
|
||||
/**
|
||||
* Only update the value which is not in props
|
||||
*/
|
||||
setUncontrolledState = (state) => {
|
||||
let needSync = false
|
||||
const newState = {}
|
||||
|
||||
Object.keys(state).forEach(name => {
|
||||
if (name in this.props) return
|
||||
|
||||
needSync = true
|
||||
newState[name] = state[name]
|
||||
})
|
||||
|
||||
this.setState(needSync ? newState : null)
|
||||
};
|
||||
|
||||
isKeyChecked = (key) => {
|
||||
const { checkedKeys = [] } = this.state
|
||||
return checkedKeys.indexOf(key) !== -1
|
||||
};
|
||||
|
||||
/**
|
||||
* [Legacy] Original logic use `key` as tracking clue.
|
||||
* We have to use `cloneElement` to pass `key`.
|
||||
*/
|
||||
renderTreeNode = (child, index, level = 0) => {
|
||||
const {
|
||||
expandedKeys = [], selectedKeys = [], halfCheckedKeys = [],
|
||||
dragOverNodeKey, dropPosition,
|
||||
} = this.state
|
||||
const {} = this.props
|
||||
const pos = getPosition(level, index)
|
||||
const key = child.key || pos
|
||||
|
||||
return React.cloneElement(child, {
|
||||
eventKey: key,
|
||||
expanded: expandedKeys.indexOf(key) !== -1,
|
||||
selected: selectedKeys.indexOf(key) !== -1,
|
||||
checked: this.isKeyChecked(key),
|
||||
halfChecked: halfCheckedKeys.indexOf(key) !== -1,
|
||||
pos,
|
||||
|
||||
// [Legacy] Drag props
|
||||
dragOver: dragOverNodeKey === key && dropPosition === 0,
|
||||
dragOverGapTop: dragOverNodeKey === key && dropPosition === -1,
|
||||
dragOverGapBottom: dragOverNodeKey === key && dropPosition === 1,
|
||||
})
|
||||
};
|
||||
|
||||
render () {
|
||||
const {
|
||||
prefixCls, className, focusable,
|
||||
showLine,
|
||||
children,
|
||||
} = this.props
|
||||
const domProps = {}
|
||||
|
||||
// [Legacy] Commit: 0117f0c9db0e2956e92cb208f51a42387dfcb3d1
|
||||
if (focusable) {
|
||||
domProps.tabIndex = '0'
|
||||
domProps.onKeyDown = this.onKeyDown
|
||||
}
|
||||
|
||||
return (
|
||||
<ul
|
||||
{...domProps}
|
||||
className={classNames(prefixCls, className, {
|
||||
[`${prefixCls}-show-line`]: showLine,
|
||||
})}
|
||||
role='tree-node'
|
||||
unselectable='on'
|
||||
>
|
||||
{React.Children.map(children, this.renderTreeNode, this)}
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default Tree
|
@ -0,0 +1,585 @@
|
||||
import PropTypes from '../../_util/vue-types'
|
||||
import classNames from 'classnames'
|
||||
import warning from 'warning'
|
||||
import { contextTypes } from './Tree'
|
||||
import { getPosition, getNodeChildren, isCheckDisabled, traverseTreeNodes } from './util'
|
||||
import { initDefaultProps, getOptionProps, filterEmpty } from '../../_util/props-util'
|
||||
|
||||
const ICON_OPEN = 'open'
|
||||
const ICON_CLOSE = 'close'
|
||||
|
||||
const LOAD_STATUS_NONE = 0
|
||||
const LOAD_STATUS_LOADING = 1
|
||||
const LOAD_STATUS_LOADED = 2
|
||||
const LOAD_STATUS_FAILED = 0 // Action align, let's make failed same as init.
|
||||
|
||||
const defaultTitle = '---'
|
||||
|
||||
let onlyTreeNodeWarned = false // Only accept TreeNode
|
||||
|
||||
export const nodeContextTypes = {
|
||||
...contextTypes,
|
||||
rcTreeNode: PropTypes.shape({
|
||||
onUpCheckConduct: PropTypes.func,
|
||||
}),
|
||||
}
|
||||
|
||||
const TreeNode = {
|
||||
props: initDefaultProps({
|
||||
eventKey: PropTypes.string, // Pass by parent `cloneElement`
|
||||
prefixCls: PropTypes.string,
|
||||
// className: PropTypes.string,
|
||||
root: PropTypes.object,
|
||||
// onSelect: PropTypes.func,
|
||||
|
||||
// By parent
|
||||
expanded: PropTypes.bool,
|
||||
selected: PropTypes.bool,
|
||||
checked: PropTypes.bool,
|
||||
halfChecked: PropTypes.bool,
|
||||
title: PropTypes.node,
|
||||
pos: PropTypes.string,
|
||||
dragOver: PropTypes.bool,
|
||||
dragOverGapTop: PropTypes.bool,
|
||||
dragOverGapBottom: PropTypes.bool,
|
||||
|
||||
// By user
|
||||
isLeaf: PropTypes.bool,
|
||||
selectable: PropTypes.bool,
|
||||
disabled: PropTypes.bool,
|
||||
disableCheckbox: PropTypes.bool,
|
||||
icon: PropTypes.any,
|
||||
}, {
|
||||
title: defaultTitle,
|
||||
}),
|
||||
|
||||
data () {
|
||||
return {
|
||||
loadStatus: LOAD_STATUS_NONE,
|
||||
dragNodeHighlight: false,
|
||||
}
|
||||
},
|
||||
inject: {
|
||||
context: { default: {}},
|
||||
},
|
||||
provide: {
|
||||
...this.context,
|
||||
rcTreeNode: this,
|
||||
},
|
||||
|
||||
// Isomorphic needn't load data in server side
|
||||
mounted () {
|
||||
this.$nextTick(() => {
|
||||
this.syncLoadData(this.$props)
|
||||
})
|
||||
},
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
this.syncLoadData(nextProps)
|
||||
},
|
||||
|
||||
onUpCheckConduct (treeNode, nodeChecked, nodeHalfChecked) {
|
||||
const { pos: nodePos } = getOptionProps(treeNode)
|
||||
const { eventKey, pos, checked, halfChecked } = this
|
||||
const {
|
||||
rcTree: { checkStrictly, isKeyChecked, onBatchNodeCheck, onCheckConductFinished },
|
||||
rcTreeNode: { onUpCheckConduct } = {},
|
||||
} = this.context
|
||||
|
||||
// Stop conduct when current node is disabled
|
||||
if (isCheckDisabled(this)) {
|
||||
onCheckConductFinished()
|
||||
return
|
||||
}
|
||||
|
||||
const children = this.getNodeChildren()
|
||||
|
||||
let checkedCount = nodeChecked ? 1 : 0
|
||||
|
||||
// Statistic checked count
|
||||
children.forEach((node, index) => {
|
||||
const childPos = getPosition(pos, index)
|
||||
|
||||
if (nodePos === childPos || isCheckDisabled(node)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (isKeyChecked(node.key || childPos)) {
|
||||
checkedCount += 1
|
||||
}
|
||||
})
|
||||
|
||||
// Static enabled children count
|
||||
const enabledChildrenCount = children
|
||||
.filter(node => !isCheckDisabled(node))
|
||||
.length
|
||||
|
||||
// checkStrictly will not conduct check status
|
||||
const nextChecked = checkStrictly ? checked : enabledChildrenCount === checkedCount
|
||||
const nextHalfChecked = checkStrictly // propagated or child checked
|
||||
? halfChecked : (nodeHalfChecked || (checkedCount > 0 && !nextChecked))
|
||||
|
||||
// Add into batch update
|
||||
if (checked !== nextChecked || halfChecked !== nextHalfChecked) {
|
||||
onBatchNodeCheck(eventKey, nextChecked, nextHalfChecked)
|
||||
|
||||
if (onUpCheckConduct) {
|
||||
onUpCheckConduct(this, nextChecked, nextHalfChecked)
|
||||
} else {
|
||||
// Flush all the update
|
||||
onCheckConductFinished()
|
||||
}
|
||||
} else {
|
||||
// Flush all the update
|
||||
onCheckConductFinished()
|
||||
}
|
||||
},
|
||||
|
||||
onDownCheckConduct (nodeChecked) {
|
||||
const { $slots } = this
|
||||
const children = $slots.default || []
|
||||
const { rcTree: { checkStrictly, isKeyChecked, onBatchNodeCheck }} = this.context
|
||||
if (checkStrictly) return
|
||||
|
||||
traverseTreeNodes(children, ({ node, key }) => {
|
||||
if (isCheckDisabled(node)) return false
|
||||
|
||||
if (nodeChecked !== isKeyChecked(key)) {
|
||||
onBatchNodeCheck(key, nodeChecked, false)
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
onSelectorClick (e) {
|
||||
if (this.isSelectable()) {
|
||||
this.onSelect(e)
|
||||
} else {
|
||||
this.onCheck(e)
|
||||
}
|
||||
},
|
||||
|
||||
onSelect (e) {
|
||||
if (this.isDisabled()) return
|
||||
|
||||
const { rcTree: { onNodeSelect }} = this.context
|
||||
e.preventDefault()
|
||||
onNodeSelect(e, this)
|
||||
},
|
||||
|
||||
onCheck (e) {
|
||||
if (this.isDisabled()) return
|
||||
|
||||
const { disableCheckbox, checked, eventKey } = this
|
||||
const {
|
||||
rcTree: { checkable, onBatchNodeCheck, onCheckConductFinished },
|
||||
rcTreeNode: { onUpCheckConduct } = {},
|
||||
} = this.context
|
||||
|
||||
if (!checkable || disableCheckbox) return
|
||||
|
||||
e.preventDefault()
|
||||
const targetChecked = !checked
|
||||
onBatchNodeCheck(eventKey, targetChecked, false, this)
|
||||
|
||||
// Children conduct
|
||||
this.onDownCheckConduct(targetChecked)
|
||||
|
||||
// Parent conduct
|
||||
if (onUpCheckConduct) {
|
||||
onUpCheckConduct(this, targetChecked, false)
|
||||
} else {
|
||||
onCheckConductFinished()
|
||||
}
|
||||
},
|
||||
|
||||
onMouseEnter (e) {
|
||||
const { rcTree: { onNodeMouseEnter }} = this.context
|
||||
onNodeMouseEnter(e, this)
|
||||
},
|
||||
|
||||
onMouseLeave (e) {
|
||||
const { rcTree: { onNodeMouseLeave }} = this.context
|
||||
onNodeMouseLeave(e, this)
|
||||
},
|
||||
|
||||
onContextMenu (e) {
|
||||
const { rcTree: { onNodeContextMenu }} = this.context
|
||||
onNodeContextMenu(e, this)
|
||||
},
|
||||
|
||||
onDragStart (e) {
|
||||
const { rcTree: { onNodeDragStart }} = this.context
|
||||
|
||||
e.stopPropagation()
|
||||
this.setState({
|
||||
dragNodeHighlight: true,
|
||||
})
|
||||
onNodeDragStart(e, this)
|
||||
|
||||
try {
|
||||
// ie throw error
|
||||
// firefox-need-it
|
||||
e.dataTransfer.setData('text/plain', '')
|
||||
} catch (error) {
|
||||
// empty
|
||||
}
|
||||
},
|
||||
|
||||
onDragEnter (e) {
|
||||
const { rcTree: { onNodeDragEnter }} = this.context
|
||||
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
onNodeDragEnter(e, this)
|
||||
},
|
||||
|
||||
onDragOver (e) {
|
||||
const { rcTree: { onNodeDragOver }} = this.context
|
||||
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
onNodeDragOver(e, this)
|
||||
},
|
||||
|
||||
onDragLeave (e) {
|
||||
const { rcTree: { onNodeDragLeave }} = this.context
|
||||
|
||||
e.stopPropagation()
|
||||
onNodeDragLeave(e, this)
|
||||
},
|
||||
|
||||
onDragEnd (e) {
|
||||
const { rcTree: { onNodeDragEnd }} = this.context
|
||||
|
||||
e.stopPropagation()
|
||||
this.setState({
|
||||
dragNodeHighlight: false,
|
||||
})
|
||||
onNodeDragEnd(e, this)
|
||||
},
|
||||
|
||||
onDrop (e) {
|
||||
const { rcTree: { onNodeDrop }} = this.context
|
||||
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
this.setState({
|
||||
dragNodeHighlight: false,
|
||||
})
|
||||
onNodeDrop(e, this)
|
||||
},
|
||||
|
||||
// Disabled item still can be switch
|
||||
onExpand (e) {
|
||||
const { rcTree: { onNodeExpand }} = this.context
|
||||
const callbackPromise = onNodeExpand(e, this)
|
||||
|
||||
// Promise like
|
||||
if (callbackPromise && callbackPromise.then) {
|
||||
this.setState({ loadStatus: LOAD_STATUS_LOADING })
|
||||
|
||||
callbackPromise.then(() => {
|
||||
this.setState({ loadStatus: LOAD_STATUS_LOADED })
|
||||
}).catch(() => {
|
||||
this.setState({ loadStatus: LOAD_STATUS_FAILED })
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
// Drag usage
|
||||
setSelectHandle (node) {
|
||||
this.selectHandle = node
|
||||
},
|
||||
|
||||
getNodeChildren () {
|
||||
const { $slots: { default: children }} = this
|
||||
const originList = filterEmpty(children)
|
||||
const targetList = getNodeChildren(originList)
|
||||
|
||||
if (originList.length !== targetList.length && !onlyTreeNodeWarned) {
|
||||
onlyTreeNodeWarned = true
|
||||
warning(false, 'Tree only accept TreeNode as children.')
|
||||
}
|
||||
|
||||
return targetList
|
||||
},
|
||||
|
||||
getNodeState () {
|
||||
const { expanded } = this
|
||||
|
||||
if (this.isLeaf()) {
|
||||
return null
|
||||
}
|
||||
|
||||
return expanded ? ICON_OPEN : ICON_CLOSE
|
||||
},
|
||||
|
||||
isLeaf () {
|
||||
const { isLeaf, loadStatus } = this
|
||||
const { rcTree: { loadData }} = this.context
|
||||
|
||||
const hasChildren = this.getNodeChildren().length !== 0
|
||||
|
||||
return (
|
||||
isLeaf ||
|
||||
(!loadData && !hasChildren) ||
|
||||
(loadData && loadStatus === LOAD_STATUS_LOADED && !hasChildren)
|
||||
)
|
||||
},
|
||||
|
||||
isDisabled () {
|
||||
const { disabled } = this
|
||||
const { rcTree: { disabled: treeDisabled }} = this.context
|
||||
|
||||
// Follow the logic of Selectable
|
||||
if (disabled === false) {
|
||||
return false
|
||||
}
|
||||
|
||||
return !!(treeDisabled || disabled)
|
||||
},
|
||||
|
||||
isSelectable () {
|
||||
const { selectable } = this
|
||||
const { rcTree: { selectable: treeSelectable }} = this.context
|
||||
|
||||
// Ignore when selectable is undefined or null
|
||||
if (typeof selectable === 'boolean') {
|
||||
return selectable
|
||||
}
|
||||
|
||||
return treeSelectable
|
||||
},
|
||||
|
||||
// Load data to avoid default expanded tree without data
|
||||
syncLoadData (props) {
|
||||
const { loadStatus } = this
|
||||
const { expanded } = props
|
||||
const { rcTree: { loadData }} = this.context
|
||||
|
||||
if (loadData && loadStatus === LOAD_STATUS_NONE && expanded && !this.isLeaf()) {
|
||||
this.setState({ loadStatus: LOAD_STATUS_LOADING })
|
||||
|
||||
loadData(this).then(() => {
|
||||
this.setState({ loadStatus: LOAD_STATUS_LOADED })
|
||||
}).catch(() => {
|
||||
this.setState({ loadStatus: LOAD_STATUS_FAILED })
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
// Switcher
|
||||
renderSwitcher () {
|
||||
const { expanded } = this
|
||||
const { rcTree: { prefixCls }} = this.context
|
||||
|
||||
if (this.isLeaf()) {
|
||||
return <span class={`${prefixCls}-switcher ${prefixCls}-switcher-noop`} />
|
||||
}
|
||||
|
||||
return (
|
||||
<span
|
||||
class={classNames(
|
||||
`${prefixCls}-switcher`,
|
||||
`${prefixCls}-switcher_${expanded ? ICON_OPEN : ICON_CLOSE}`,
|
||||
)}
|
||||
onClick={this.onExpand}
|
||||
/>
|
||||
)
|
||||
},
|
||||
|
||||
// Checkbox
|
||||
renderCheckbox () {
|
||||
const { checked, halfChecked, disableCheckbox } = this
|
||||
const { rcTree: { prefixCls, checkable }} = this.context
|
||||
const disabled = this.isDisabled()
|
||||
|
||||
if (!checkable) return null
|
||||
|
||||
// [Legacy] Custom element should be separate with `checkable` in future
|
||||
const $custom = typeof checkable !== 'boolean' ? checkable : null
|
||||
|
||||
return (
|
||||
<span
|
||||
class={classNames(
|
||||
`${prefixCls}-checkbox`,
|
||||
checked && `${prefixCls}-checkbox-checked`,
|
||||
!checked && halfChecked && `${prefixCls}-checkbox-indeterminate`,
|
||||
(disabled || disableCheckbox) && `${prefixCls}-checkbox-disabled`,
|
||||
)}
|
||||
onClick={this.onCheck}
|
||||
>
|
||||
{$custom}
|
||||
</span>
|
||||
)
|
||||
},
|
||||
|
||||
renderIcon () {
|
||||
const { loadStatus } = this
|
||||
const { rcTree: { prefixCls }} = this.context
|
||||
|
||||
return (
|
||||
<span
|
||||
class={classNames(
|
||||
`${prefixCls}-iconEle`,
|
||||
`${prefixCls}-icon__${this.getNodeState() || 'docu'}`,
|
||||
(loadStatus === LOAD_STATUS_LOADING) && `${prefixCls}-icon_loading`,
|
||||
)}
|
||||
/>
|
||||
)
|
||||
},
|
||||
|
||||
// Icon + Title
|
||||
renderSelector () {
|
||||
const { title, selected, icon, loadStatus, dragNodeHighlight } = this
|
||||
const { rcTree: { prefixCls, showIcon, draggable, loadData }} = this.context
|
||||
const disabled = this.isDisabled()
|
||||
|
||||
const wrapClass = `${prefixCls}-node-content-wrapper`
|
||||
|
||||
// Icon - Still show loading icon when loading without showIcon
|
||||
let $icon
|
||||
|
||||
if (showIcon) {
|
||||
$icon = icon ? (
|
||||
<span
|
||||
class={classNames(
|
||||
`${prefixCls}-iconEle`,
|
||||
`${prefixCls}-icon__customize`,
|
||||
)}
|
||||
>
|
||||
{typeof icon === 'function'
|
||||
? icon(this.$props) : icon}
|
||||
</span>
|
||||
) : this.renderIcon()
|
||||
} else if (loadData && loadStatus === LOAD_STATUS_LOADING) {
|
||||
$icon = this.renderIcon()
|
||||
}
|
||||
|
||||
// Title
|
||||
const $title = <span class={`${prefixCls}-title`}>{title}</span>
|
||||
|
||||
return (
|
||||
<span
|
||||
ref='selectHandle'
|
||||
title={typeof title === 'string' ? title : ''}
|
||||
class={classNames(
|
||||
`${wrapClass}`,
|
||||
`${wrapClass}-${this.getNodeState() || 'normal'}`,
|
||||
(!disabled && (selected || dragNodeHighlight)) && `${prefixCls}-node-selected`,
|
||||
(!disabled && draggable) && 'draggable'
|
||||
)}
|
||||
draggable={(!disabled && draggable) || undefined}
|
||||
aria-grabbed={(!disabled && draggable) || undefined}
|
||||
|
||||
onMouseenter={this.onMouseEnter}
|
||||
onMouseleave={this.onMouseLeave}
|
||||
onContexmenu={this.onContextMenu}
|
||||
onClick={this.onSelectorClick}
|
||||
onDragstart={this.onDragStart}
|
||||
>
|
||||
{$icon}{$title}
|
||||
</span>
|
||||
)
|
||||
},
|
||||
|
||||
// Children list wrapped with `Animation`
|
||||
renderChildren () {
|
||||
const { expanded, pos } = this
|
||||
const { rcTree: {
|
||||
prefixCls,
|
||||
openTransitionName, openAnimation,
|
||||
renderTreeNode,
|
||||
}} = this.context
|
||||
|
||||
// [Legacy] Animation control
|
||||
const renderFirst = this.renderFirst
|
||||
this.renderFirst = 1
|
||||
let transitionAppear = true
|
||||
if (!renderFirst && expanded) {
|
||||
transitionAppear = false
|
||||
}
|
||||
|
||||
const animProps = {}
|
||||
if (openTransitionName) {
|
||||
animProps.transitionName = openTransitionName
|
||||
} else if (typeof openAnimation === 'object') {
|
||||
animProps.animation = { ...openAnimation }
|
||||
if (!transitionAppear) {
|
||||
delete animProps.animation.appear
|
||||
}
|
||||
}
|
||||
|
||||
// Children TreeNode
|
||||
const nodeList = this.getNodeChildren()
|
||||
|
||||
if (nodeList.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
let $children
|
||||
if (expanded) {
|
||||
$children = (
|
||||
<ul
|
||||
class={classNames(
|
||||
`${prefixCls}-child-tree`,
|
||||
expanded && `${prefixCls}-child-tree-open`,
|
||||
)}
|
||||
data-expanded={expanded}
|
||||
>
|
||||
{nodeList.map((node, index) => (
|
||||
renderTreeNode(node, index, pos)
|
||||
))}
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Animate
|
||||
{...animProps}
|
||||
showProp='data-expanded'
|
||||
transitionAppear={transitionAppear}
|
||||
component=''
|
||||
>
|
||||
{$children}
|
||||
</Animate>
|
||||
)
|
||||
},
|
||||
|
||||
render () {
|
||||
const {
|
||||
dragOver, dragOverGapTop, dragOverGapBottom,
|
||||
} = this
|
||||
const { rcTree: {
|
||||
prefixCls,
|
||||
filterTreeNode,
|
||||
}} = this.context
|
||||
const disabled = this.isDisabled()
|
||||
|
||||
return (
|
||||
<li
|
||||
class={{
|
||||
[`${prefixCls}-treenode-disabled`]: disabled,
|
||||
'drag-over': !disabled && dragOver,
|
||||
'drag-over-gap-top': !disabled && dragOverGapTop,
|
||||
'drag-over-gap-bottom': !disabled && dragOverGapBottom,
|
||||
'filter-node': filterTreeNode && filterTreeNode(this),
|
||||
}}
|
||||
onDragenter={this.onDragEnter}
|
||||
onDragover={this.onDragOver}
|
||||
onDragleave={this.onDragLeave}
|
||||
onDrop={this.onDrop}
|
||||
onDragend={this.onDragEnd}
|
||||
>
|
||||
{this.renderSwitcher()}
|
||||
{this.renderCheckbox()}
|
||||
{this.renderSelector()}
|
||||
{this.renderChildren()}
|
||||
</li>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
TreeNode.isTreeNode = 1
|
||||
|
||||
export default TreeNode
|
@ -0,0 +1,6 @@
|
||||
import Tree from './Tree'
|
||||
import TreeNode from './TreeNode'
|
||||
Tree.TreeNode = TreeNode
|
||||
|
||||
export { TreeNode }
|
||||
export default Tree
|
@ -0,0 +1,399 @@
|
||||
/* eslint no-loop-func: 0*/
|
||||
import { Children } from 'react'
|
||||
import warning from 'warning'
|
||||
|
||||
export function arrDel (list, value) {
|
||||
const clone = list.slice()
|
||||
const index = clone.indexOf(value)
|
||||
if (index >= 0) {
|
||||
clone.splice(index, 1)
|
||||
}
|
||||
return clone
|
||||
}
|
||||
|
||||
export function arrAdd (list, value) {
|
||||
const clone = list.slice()
|
||||
if (clone.indexOf(value) === -1) {
|
||||
clone.push(value)
|
||||
}
|
||||
return clone
|
||||
}
|
||||
|
||||
export function posToArr (pos) {
|
||||
return pos.split('-')
|
||||
}
|
||||
|
||||
// Only used when drag, not affect SSR.
|
||||
export function getOffset (ele) {
|
||||
if (!ele.getClientRects().length) {
|
||||
return { top: 0, left: 0 }
|
||||
}
|
||||
|
||||
const rect = ele.getBoundingClientRect()
|
||||
if (rect.width || rect.height) {
|
||||
const doc = ele.ownerDocument
|
||||
const win = doc.defaultView
|
||||
const docElem = doc.documentElement
|
||||
|
||||
return {
|
||||
top: rect.top + win.pageYOffset - docElem.clientTop,
|
||||
left: rect.left + win.pageXOffset - docElem.clientLeft,
|
||||
}
|
||||
}
|
||||
|
||||
return rect
|
||||
}
|
||||
|
||||
export function getPosition (level, index) {
|
||||
return `${level}-${index}`
|
||||
}
|
||||
|
||||
export function getNodeChildren (children) {
|
||||
const childList = Array.isArray(children) ? children : [children]
|
||||
return childList
|
||||
.filter(child => child && child.type && child.type.isTreeNode)
|
||||
}
|
||||
|
||||
export function isCheckDisabled (node) {
|
||||
const { disabled, disableCheckbox } = node.props || {}
|
||||
return !!(disabled || disableCheckbox)
|
||||
}
|
||||
|
||||
export function traverseTreeNodes (treeNodes, subTreeData, callback) {
|
||||
if (typeof subTreeData === 'function') {
|
||||
callback = subTreeData
|
||||
subTreeData = false
|
||||
}
|
||||
|
||||
function processNode (node, index, parent) {
|
||||
const children = node ? node.props.children : treeNodes
|
||||
const pos = node ? getPosition(parent.pos, index) : 0
|
||||
|
||||
// Filter children
|
||||
const childList = getNodeChildren(children)
|
||||
|
||||
// Process node if is not root
|
||||
if (node) {
|
||||
const data = {
|
||||
node,
|
||||
index,
|
||||
pos,
|
||||
key: node.key || pos,
|
||||
parentPos: parent.node ? parent.pos : null,
|
||||
}
|
||||
|
||||
// Children data is not must have
|
||||
if (subTreeData) {
|
||||
// Statistic children
|
||||
const subNodes = []
|
||||
Children.forEach(childList, (subNode, subIndex) => {
|
||||
// Provide limit snapshot
|
||||
const subPos = getPosition(pos, index)
|
||||
subNodes.push({
|
||||
node: subNode,
|
||||
key: subNode.key || subPos,
|
||||
pos: subPos,
|
||||
index: subIndex,
|
||||
})
|
||||
})
|
||||
data.subNodes = subNodes
|
||||
}
|
||||
|
||||
// Can break traverse by return false
|
||||
if (callback(data) === false) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Process children node
|
||||
Children.forEach(childList, (subNode, subIndex) => {
|
||||
processNode(subNode, subIndex, { node, pos })
|
||||
})
|
||||
}
|
||||
|
||||
processNode(null)
|
||||
}
|
||||
|
||||
/**
|
||||
* [Legacy] Return halfChecked when it has value.
|
||||
* @param checkedKeys
|
||||
* @param halfChecked
|
||||
* @returns {*}
|
||||
*/
|
||||
export function getStrictlyValue (checkedKeys, halfChecked) {
|
||||
if (halfChecked) {
|
||||
return { checked: checkedKeys, halfChecked }
|
||||
}
|
||||
return checkedKeys
|
||||
}
|
||||
|
||||
export function getFullKeyList (treeNodes) {
|
||||
const keyList = []
|
||||
traverseTreeNodes(treeNodes, ({ key }) => {
|
||||
keyList.push(key)
|
||||
})
|
||||
return keyList
|
||||
}
|
||||
|
||||
/**
|
||||
* Check position relation.
|
||||
* @param parentPos
|
||||
* @param childPos
|
||||
* @param directly only directly parent can be true
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isParent (parentPos, childPos, directly = false) {
|
||||
if (!parentPos || !childPos || parentPos.length > childPos.length) return false
|
||||
|
||||
const parentPath = posToArr(parentPos)
|
||||
const childPath = posToArr(childPos)
|
||||
|
||||
// Directly check
|
||||
if (directly && parentPath.length !== childPath.length - 1) return false
|
||||
|
||||
const len = parentPath.length
|
||||
for (let i = 0; i < len; i += 1) {
|
||||
if (parentPath[i] !== childPath[i]) return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Statistic TreeNodes info
|
||||
* @param treeNodes
|
||||
* @returns {{}}
|
||||
*/
|
||||
export function getNodesStatistic (treeNodes) {
|
||||
const statistic = {
|
||||
keyNodes: {},
|
||||
posNodes: {},
|
||||
nodeList: [],
|
||||
}
|
||||
|
||||
traverseTreeNodes(treeNodes, true, ({ node, index, pos, key, subNodes, parentPos }) => {
|
||||
const data = { node, index, pos, key, subNodes, parentPos }
|
||||
statistic.keyNodes[key] = data
|
||||
statistic.posNodes[pos] = data
|
||||
statistic.nodeList.push(data)
|
||||
})
|
||||
|
||||
return statistic
|
||||
}
|
||||
|
||||
export function getDragNodesKeys (treeNodes, node) {
|
||||
const { eventKey, pos } = node.props
|
||||
const dragNodesKeys = []
|
||||
|
||||
traverseTreeNodes(treeNodes, ({ pos: nodePos, key }) => {
|
||||
if (isParent(pos, nodePos)) {
|
||||
dragNodesKeys.push(key)
|
||||
}
|
||||
})
|
||||
dragNodesKeys.push(eventKey || pos)
|
||||
return dragNodesKeys
|
||||
}
|
||||
|
||||
export function calcDropPosition (event, treeNode) {
|
||||
const offsetTop = getOffset(treeNode.selectHandle).top
|
||||
const offsetHeight = treeNode.selectHandle.offsetHeight
|
||||
const pageY = event.pageY
|
||||
const gapHeight = 2 // [Legacy] TODO: remove hard code
|
||||
if (pageY > offsetTop + offsetHeight - gapHeight) {
|
||||
return 1
|
||||
}
|
||||
if (pageY < offsetTop + gapHeight) {
|
||||
return -1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto expand all related node when sub node is expanded
|
||||
* @param keyList
|
||||
* @param props
|
||||
* @returns [string]
|
||||
*/
|
||||
export function calcExpandedKeys (keyList, props) {
|
||||
if (!keyList) {
|
||||
return []
|
||||
}
|
||||
|
||||
const { autoExpandParent, children } = props
|
||||
|
||||
// Do nothing if not auto expand parent
|
||||
if (!autoExpandParent) {
|
||||
return keyList
|
||||
}
|
||||
|
||||
// Fill parent expanded keys
|
||||
const { keyNodes, nodeList } = getNodesStatistic(children)
|
||||
const needExpandKeys = {}
|
||||
const needExpandPathList = []
|
||||
|
||||
// Fill expanded nodes
|
||||
keyList.forEach((key) => {
|
||||
const node = keyNodes[key]
|
||||
if (node) {
|
||||
needExpandKeys[key] = true
|
||||
needExpandPathList.push(node.pos)
|
||||
}
|
||||
})
|
||||
|
||||
// Match parent by path
|
||||
nodeList.forEach(({ pos, key }) => {
|
||||
if (needExpandPathList.some(childPos => isParent(pos, childPos))) {
|
||||
needExpandKeys[key] = true
|
||||
}
|
||||
})
|
||||
|
||||
const calcExpandedKeyList = Object.keys(needExpandKeys)
|
||||
|
||||
// [Legacy] Return origin keyList if calc list is empty
|
||||
return calcExpandedKeyList.length ? calcExpandedKeyList : keyList
|
||||
}
|
||||
|
||||
/**
|
||||
* Return selectedKeys according with multiple prop
|
||||
* @param selectedKeys
|
||||
* @param props
|
||||
* @returns [string]
|
||||
*/
|
||||
export function calcSelectedKeys (selectedKeys, props) {
|
||||
if (!selectedKeys) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const { multiple } = props
|
||||
if (multiple) {
|
||||
return selectedKeys.slice()
|
||||
}
|
||||
|
||||
if (selectedKeys.length) {
|
||||
return [selectedKeys[0]]
|
||||
}
|
||||
return selectedKeys
|
||||
}
|
||||
|
||||
/**
|
||||
* Check conduct is by key level. It pass though up & down.
|
||||
* When conduct target node is check means already conducted will be skip.
|
||||
* @param treeNodes
|
||||
* @param checkedKeys
|
||||
* @returns {{checkedKeys: Array, halfCheckedKeys: Array}}
|
||||
*/
|
||||
export function calcCheckStateConduct (treeNodes, checkedKeys) {
|
||||
const { keyNodes, posNodes } = getNodesStatistic(treeNodes)
|
||||
|
||||
const tgtCheckedKeys = {}
|
||||
const tgtHalfCheckedKeys = {}
|
||||
|
||||
// Conduct up
|
||||
function conductUp (key, halfChecked) {
|
||||
if (tgtCheckedKeys[key]) return
|
||||
|
||||
const { subNodes = [], parentPos, node } = keyNodes[key]
|
||||
if (isCheckDisabled(node)) return
|
||||
|
||||
const allSubChecked = !halfChecked && subNodes
|
||||
.filter(sub => !isCheckDisabled(sub.node))
|
||||
.every(sub => tgtCheckedKeys[sub.key])
|
||||
|
||||
if (allSubChecked) {
|
||||
tgtCheckedKeys[key] = true
|
||||
} else {
|
||||
tgtHalfCheckedKeys[key] = true
|
||||
}
|
||||
|
||||
if (parentPos !== null) {
|
||||
conductUp(posNodes[parentPos].key, !allSubChecked)
|
||||
}
|
||||
}
|
||||
|
||||
// Conduct down
|
||||
function conductDown (key) {
|
||||
if (tgtCheckedKeys[key]) return
|
||||
const { subNodes = [], node } = keyNodes[key]
|
||||
|
||||
if (isCheckDisabled(node)) return
|
||||
|
||||
tgtCheckedKeys[key] = true
|
||||
|
||||
subNodes.forEach((sub) => {
|
||||
conductDown(sub.key)
|
||||
})
|
||||
}
|
||||
|
||||
function conduct (key) {
|
||||
if (!keyNodes[key]) {
|
||||
warning(false, `'${key}' does not exist in the tree.`)
|
||||
return
|
||||
}
|
||||
|
||||
const { subNodes = [], parentPos, node } = keyNodes[key]
|
||||
if (isCheckDisabled(node)) return
|
||||
|
||||
tgtCheckedKeys[key] = true
|
||||
|
||||
// Conduct down
|
||||
subNodes
|
||||
.filter(sub => !isCheckDisabled(sub.node))
|
||||
.forEach((sub) => {
|
||||
conductDown(sub.key)
|
||||
})
|
||||
|
||||
// Conduct up
|
||||
if (parentPos !== null) {
|
||||
conductUp(posNodes[parentPos].key)
|
||||
}
|
||||
}
|
||||
|
||||
checkedKeys.forEach((key) => {
|
||||
conduct(key)
|
||||
})
|
||||
|
||||
return {
|
||||
checkedKeys: Object.keys(tgtCheckedKeys),
|
||||
halfCheckedKeys: Object.keys(tgtHalfCheckedKeys)
|
||||
.filter(key => !tgtCheckedKeys[key]),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the value of checked and halfChecked keys.
|
||||
* This should be only run in init or props changed.
|
||||
*/
|
||||
export function calcCheckedKeys (keys, props) {
|
||||
const { checkable, children, checkStrictly } = props
|
||||
|
||||
if (!checkable || !keys) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Convert keys to object format
|
||||
let keyProps
|
||||
if (Array.isArray(keys)) {
|
||||
// [Legacy] Follow the api doc
|
||||
keyProps = {
|
||||
checkedKeys: keys,
|
||||
halfCheckedKeys: undefined,
|
||||
}
|
||||
} else if (typeof keys === 'object') {
|
||||
keyProps = {
|
||||
checkedKeys: keys.checked || undefined,
|
||||
halfCheckedKeys: keys.halfChecked || undefined,
|
||||
}
|
||||
} else {
|
||||
warning(false, '`CheckedKeys` is not an array or an object')
|
||||
return null
|
||||
}
|
||||
|
||||
// Do nothing if is checkStrictly mode
|
||||
if (checkStrictly) {
|
||||
return keyProps
|
||||
}
|
||||
|
||||
// Conduct calculate the check status
|
||||
const { checkedKeys = [] } = keyProps
|
||||
return calcCheckStateConduct(children, checkedKeys)
|
||||
}
|
Loading…
Reference in new issue