603 lines
18 KiB
JavaScript
603 lines
18 KiB
JavaScript
import PropTypes from '../../_util/vue-types'
|
|
import classNames from 'classnames'
|
|
import warning from 'warning'
|
|
import { initDefaultProps, getOptionProps } from '../../_util/props-util'
|
|
import { cloneElement } from '../../_util/vnode'
|
|
import BaseMixin from '../../_util/BaseMixin'
|
|
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 = {
|
|
name: 'Tree',
|
|
mixins: [BaseMixin],
|
|
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]),
|
|
children: PropTypes.any,
|
|
}, {
|
|
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
|
|
const children = this.$slots.default
|
|
// Sync state with props
|
|
const { checkedKeys = [], halfCheckedKeys = [] } =
|
|
calcCheckedKeys(defaultCheckedKeys, props, children) || {}
|
|
|
|
// Cache for check status to optimize
|
|
this.checkedBatch = null
|
|
this.propsToStateMap = {
|
|
expandedKeys: 'sExpandedKeys',
|
|
selectedKeys: 'sSelectedKeys',
|
|
checkedKeys: 'sCheckedKeys',
|
|
halfCheckedKeys: 'sHalfCheckedKeys',
|
|
}
|
|
return {
|
|
sExpandedKeys: defaultExpandAll
|
|
? getFullKeyList(children)
|
|
: calcExpandedKeys(defaultExpandedKeys, props, children),
|
|
sSelectedKeys: calcSelectedKeys(defaultSelectedKeys, props, children),
|
|
sCheckedKeys: checkedKeys,
|
|
sHalfCheckedKeys: halfCheckedKeys,
|
|
|
|
...(this.getSyncProps(props) || {}),
|
|
dragOverNodeKey: '',
|
|
dropPosition: null,
|
|
}
|
|
},
|
|
provide () {
|
|
return {
|
|
vcTree: this,
|
|
}
|
|
},
|
|
|
|
watch: {
|
|
children (val) {
|
|
const { checkedKeys = [], halfCheckedKeys = [] } = calcCheckedKeys(this.checkedKeys || this.sCheckedKeys, this.$props, this.$slots.default) || {}
|
|
this.sCheckedKeys = checkedKeys
|
|
this.sHalfCheckedKeys = halfCheckedKeys
|
|
},
|
|
expandedKeys (val) {
|
|
this.sExpandedKeys = calcExpandedKeys(this.expandedKeys, this.$props, this.$slots.default)
|
|
},
|
|
selectedKeys (val) {
|
|
this.sSelectedKeys = calcSelectedKeys(this.selectedKeys, this.$props, this.$slots.default)
|
|
},
|
|
checkedKeys (val) {
|
|
const { checkedKeys = [], halfCheckedKeys = [] } = calcCheckedKeys(this.checkedKeys, this.$props, this.$slots.default) || {}
|
|
this.sCheckedKeys = checkedKeys
|
|
this.sHalfCheckedKeys = halfCheckedKeys
|
|
},
|
|
},
|
|
|
|
// componentWillReceiveProps (nextProps) {
|
|
// // React 16 will not trigger update if new state is null
|
|
// this.setState(this.getSyncProps(nextProps, this.props))
|
|
// },
|
|
|
|
methods: {
|
|
onNodeDragStart (event, node) {
|
|
const { sExpandedKeys } = this
|
|
const { eventKey, children } = node.props
|
|
|
|
this.dragNode = node
|
|
|
|
this.setState({
|
|
dragNodesKeys: getDragNodesKeys(children, node),
|
|
sExpandedKeys: arrDel(sExpandedKeys, eventKey),
|
|
})
|
|
this.__emit('dragstart', { 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 { sExpandedKeys } = this
|
|
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(sExpandedKeys, eventKey)
|
|
this.setState({
|
|
sExpandedKeys: newExpandedKeys,
|
|
})
|
|
this.__emit('dragenter', { event, node, expandedKeys: newExpandedKeys })
|
|
}, 400)
|
|
}, 0)
|
|
},
|
|
onNodeDragOver (event, node) {
|
|
this.__emit('dragover', { event, node })
|
|
},
|
|
onNodeDragLeave (event, node) {
|
|
this.setState({
|
|
dragOverNodeKey: '',
|
|
})
|
|
this.__emit('dragleave', { event, node })
|
|
},
|
|
onNodeDragEnd (event, node) {
|
|
this.setState({
|
|
dragOverNodeKey: '',
|
|
})
|
|
this.__emit('dragend', { event, node })
|
|
},
|
|
onNodeDrop (event, node) {
|
|
const { dragNodesKeys, dropPosition } = this
|
|
|
|
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
|
|
}
|
|
this.__emit('drop', dropResult)
|
|
},
|
|
|
|
onNodeSelect (e, treeNode) {
|
|
const { sSelectedKeys, multiple, $slots: { default: children }} = this
|
|
const { selected, eventKey } = getOptionProps(treeNode)
|
|
const targetSelected = !selected
|
|
let selectedKeys = sSelectedKeys
|
|
// 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 })
|
|
|
|
const eventObj = {
|
|
event: 'select',
|
|
selected: targetSelected,
|
|
node: treeNode,
|
|
selectedNodes,
|
|
}
|
|
this.__emit('select', 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 { sCheckedKeys, sHalfCheckedKeys, checkStrictly, $slots: { default: children }} = this
|
|
|
|
// Use map to optimize update speed
|
|
const checkedKeySet = {}
|
|
const halfCheckedKeySet = {}
|
|
|
|
sCheckedKeys.forEach(key => {
|
|
checkedKeySet[key] = true
|
|
})
|
|
sHalfCheckedKeys.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,
|
|
})
|
|
}
|
|
this.__emit('check', selectedObj, eventObj)
|
|
|
|
// Clean up
|
|
this.checkedBatch = null
|
|
},
|
|
|
|
onNodeExpand (e, treeNode) {
|
|
const { sExpandedKeys, loadData } = this
|
|
let expandedKeys = [...sExpandedKeys]
|
|
const { eventKey, expanded } = getOptionProps(treeNode)
|
|
|
|
// 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 })
|
|
this.__emit('expand', 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) {
|
|
this.__emit('mouseenter', { event, node })
|
|
},
|
|
|
|
onNodeMouseLeave (event, node) {
|
|
this.__emit('mouseleave', { event, node })
|
|
},
|
|
|
|
onNodeContextMenu (event, node) {
|
|
event.preventDefault()
|
|
this.__emit('rightClick', { event, node })
|
|
},
|
|
|
|
/**
|
|
* Sync state with props if needed
|
|
*/
|
|
getSyncProps (props = {}, prevProps) {
|
|
let needSync = false
|
|
const newState = {}
|
|
const myPrevProps = prevProps || {}
|
|
const children = this.$slots.default
|
|
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.sCheckedKeys, props, children) || {}
|
|
newState.sCheckedKeys = checkedKeys
|
|
newState.sHalfCheckedKeys = halfCheckedKeys
|
|
}
|
|
|
|
if (checkSync('expandedKeys')) {
|
|
newState.sExpandedKeys = calcExpandedKeys(props.expandedKeys, props, children)
|
|
}
|
|
|
|
if (checkSync('selectedKeys')) {
|
|
newState.sSelectedKeys = calcSelectedKeys(props.selectedKeys, props, children)
|
|
}
|
|
|
|
if (checkSync('checkedKeys')) {
|
|
const { checkedKeys = [], halfCheckedKeys = [] } =
|
|
calcCheckedKeys(props.checkedKeys, props, children) || {}
|
|
newState.sCheckedKeys = checkedKeys
|
|
newState.sHalfCheckedKeys = halfCheckedKeys
|
|
}
|
|
|
|
return needSync ? newState : null
|
|
},
|
|
|
|
/**
|
|
* Only update the value which is not in props
|
|
*/
|
|
setUncontrolledState (state) {
|
|
let needSync = false
|
|
const newState = {}
|
|
const props = getOptionProps(this)
|
|
Object.keys(state).forEach(name => {
|
|
if (name in props) return
|
|
|
|
needSync = true
|
|
const key = this.propsToStateMap[name]
|
|
newState[key] = state[name]
|
|
})
|
|
|
|
this.setState(needSync ? newState : null)
|
|
},
|
|
|
|
isKeyChecked (key) {
|
|
const { sCheckedKeys = [] } = this
|
|
return sCheckedKeys.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 {
|
|
sExpandedKeys = [], sSelectedKeys = [], sHalfCheckedKeys = [],
|
|
dragOverNodeKey, dropPosition,
|
|
} = this
|
|
const pos = getPosition(level, index)
|
|
const key = child.key || pos
|
|
|
|
return cloneElement(child, {
|
|
props: {
|
|
eventKey: key,
|
|
expanded: sExpandedKeys.indexOf(key) !== -1,
|
|
selected: sSelectedKeys.indexOf(key) !== -1,
|
|
checked: this.isKeyChecked(key),
|
|
halfChecked: sHalfCheckedKeys.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, focusable,
|
|
showLine,
|
|
$slots: { default: children = [] },
|
|
} = this
|
|
const domProps = {}
|
|
|
|
return (
|
|
<ul
|
|
{...domProps}
|
|
class={classNames(prefixCls, {
|
|
[`${prefixCls}-show-line`]: showLine,
|
|
})}
|
|
role='tree-node'
|
|
unselectable='on'
|
|
tabIndex={focusable ? '0' : null}
|
|
onKeydown={focusable ? this.onKeydown : () => {}}
|
|
>
|
|
{children.map(this.renderTreeNode)}
|
|
</ul>
|
|
)
|
|
},
|
|
}
|
|
|
|
export default Tree
|