ant-design-vue/components/vc-tree/src/Tree.jsx

632 lines
20 KiB
Vue

import PropTypes from '../../_util/vue-types'
import classNames from 'classnames'
import warning from 'warning'
import { initDefaultProps, getOptionProps, getSlots } from '../../_util/props-util'
import { cloneElement } from '../../_util/vnode'
import BaseMixin from '../../_util/BaseMixin'
import proxyComponent from '../../_util/proxyComponent'
import {
convertTreeToEntities, convertDataToTree,
getPosition, getDragNodesKeys,
parseCheckedKeys,
conductExpandParent, calcSelectedKeys,
calcDropPosition,
arrAdd, arrDel, posToArr,
mapChildren, conductCheck,
warnOnlyTreeNode,
} from './util'
/**
* Thought we still use `cloneElement` to pass `key`,
* other props can pass with context for future refactor.
*/
const Tree = {
name: 'Tree',
mixins: [BaseMixin],
props: initDefaultProps({
prefixCls: PropTypes.string,
tabIndex: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
children: PropTypes.any,
treeData: PropTypes.array, // Generate treeNode by children
showLine: PropTypes.bool,
showIcon: PropTypes.bool,
icon: PropTypes.oneOfType([PropTypes.object, PropTypes.func]),
focusable: PropTypes.bool,
selectable: PropTypes.bool,
disabled: PropTypes.bool,
multiple: PropTypes.bool,
checkable: PropTypes.oneOfType([PropTypes.object, PropTypes.bool]),
checkStrictly: PropTypes.bool,
draggable: PropTypes.bool,
defaultExpandParent: 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.oneOfType([PropTypes.string, PropTypes.number])),
PropTypes.object,
]),
defaultSelectedKeys: PropTypes.arrayOf(PropTypes.string),
selectedKeys: PropTypes.arrayOf(PropTypes.string),
// onClick: PropTypes.func,
// onDoubleClick: PropTypes.func,
// onExpand: PropTypes.func,
// onCheck: PropTypes.func,
// onSelect: PropTypes.func,
loadData: PropTypes.func,
loadedKeys: PropTypes.arrayOf(PropTypes.string),
// 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]),
switcherIcon: PropTypes.any,
_propsSymbol: PropTypes.any,
}, {
prefixCls: 'rc-tree',
showLine: false,
showIcon: true,
selectable: true,
multiple: false,
checkable: false,
disabled: false,
checkStrictly: false,
draggable: false,
defaultExpandParent: true,
autoExpandParent: false,
defaultExpandAll: false,
defaultExpandedKeys: [],
defaultCheckedKeys: [],
defaultSelectedKeys: [],
}),
data () {
const state = {
_posEntities: {},
_keyEntities: {},
_expandedKeys: [],
_selectedKeys: [],
_checkedKeys: [],
_halfCheckedKeys: [],
_loadedKeys: [],
_loadingKeys: [],
_treeNode: [],
_prevProps: null,
_dragOverNodeKey: '',
_dropPosition: null,
_dragNodesKeys: [],
}
return {
...state,
// ...this.getSyncProps(props),
// dragOverNodeKey: '',
// dropPosition: null,
// dragNodesKeys: [],
// sLoadedKeys: [],
// sLoadingKeys: [],
...this.getDerivedStateFromProps(getOptionProps(this), state),
}
},
provide () {
return {
vcTree: this,
}
},
watch: {
__propsSymbol__ () {
this.setState(this.getDerivedStateFromProps(getOptionProps(this), this.$data))
},
},
methods: {
getDerivedStateFromProps (props, prevState) {
const { _prevProps } = prevState
const newState = {
_prevProps: { ...props },
}
function needSync (name) {
return (!_prevProps && name in props) || (_prevProps && _prevProps[name] !== props[name])
}
// ================== Tree Node ==================
let treeNode = null
// Check if `treeData` or `children` changed and save into the state.
if (needSync('treeData')) {
treeNode = convertDataToTree(this.$createElement, props.treeData)
} else if (needSync('children')) {
treeNode = props.children
}
// Tree support filter function which will break the tree structure in the vdm.
// We cache the treeNodes in state so that we can return the treeNode in event trigger.
if (treeNode) {
newState._treeNode = treeNode
// Calculate the entities data for quick match
const entitiesMap = convertTreeToEntities(treeNode)
newState._posEntities = entitiesMap.posEntities
newState._keyEntities = entitiesMap.keyEntities
}
const keyEntities = newState._keyEntities || prevState._keyEntities
// ================ expandedKeys =================
if (needSync('expandedKeys') || (_prevProps && needSync('autoExpandParent'))) {
newState._expandedKeys = (props.autoExpandParent || (!_prevProps && props.defaultExpandParent))
? conductExpandParent(props.expandedKeys, keyEntities) : props.expandedKeys
} else if (!_prevProps && props.defaultExpandAll) {
newState._expandedKeys = Object.keys(keyEntities)
} else if (!_prevProps && props.defaultExpandedKeys) {
newState._expandedKeys = (props.autoExpandParent || props.defaultExpandParent)
? conductExpandParent(props.defaultExpandedKeys, keyEntities) : props.defaultExpandedKeys
}
// ================ selectedKeys =================
if (props.selectable) {
if (needSync('selectedKeys')) {
newState._selectedKeys = calcSelectedKeys(props.selectedKeys, props)
} else if (!_prevProps && props.defaultSelectedKeys) {
newState._selectedKeys = calcSelectedKeys(props.defaultSelectedKeys, props)
}
}
// ================= checkedKeys =================
if (props.checkable) {
let checkedKeyEntity
if (needSync('checkedKeys')) {
checkedKeyEntity = parseCheckedKeys(props.checkedKeys) || {}
} else if (!_prevProps && props.defaultCheckedKeys) {
checkedKeyEntity = parseCheckedKeys(props.defaultCheckedKeys) || {}
} else if (treeNode) {
// If treeNode changed, we also need check it
checkedKeyEntity = {
checkedKeys: prevState._checkedKeys,
halfCheckedKeys: prevState._halfCheckedKeys,
}
}
if (checkedKeyEntity) {
let { checkedKeys = [], halfCheckedKeys = [] } = checkedKeyEntity
if (!props.checkStrictly) {
const conductKeys = conductCheck(checkedKeys, true, keyEntities)
checkedKeys = conductKeys.checkedKeys
halfCheckedKeys = conductKeys.halfCheckedKeys
}
newState._checkedKeys = checkedKeys
newState._halfCheckedKeys = halfCheckedKeys
}
}
// ================= loadedKeys ==================
if (needSync('loadedKeys')) {
newState._loadedKeys = props.loadedKeys
}
return newState
},
onNodeDragStart (event, node) {
const { _expandedKeys } = this.$data
const { eventKey } = node
const children = getSlots(node).default
this.dragNode = node
this.setState({
_dragNodesKeys: getDragNodesKeys(children, node),
_expandedKeys: arrDel(_expandedKeys, 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 { _expandedKeys: expandedKeys } = this.$data
const { pos, eventKey } = node
if (!this.dragNode || !node.$refs.selectHandle) return
const dropPosition = calcDropPosition(event, node)
// Skip if drag node is self
if (
this.dragNode.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: 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,
})
this.__emit('dragenter', { event, node, expandedKeys: newExpandedKeys })
}, 400)
}, 0)
},
onNodeDragOver (event, node) {
const { eventKey } = node
const { _dragOverNodeKey, _dropPosition } = this.$data
// Update drag position
if (this.dragNode && eventKey === _dragOverNodeKey && node.$refs.selectHandle) {
const dropPosition = calcDropPosition(event, node)
if (dropPosition === _dropPosition) return
this.setState({
_dropPosition,
})
}
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 })
this.dragNode = null
},
onNodeDrop (event, node) {
const { _dragNodesKeys = [], _dropPosition } = this.$data
const { eventKey, pos } = node
this.setState({
_dragOverNodeKey: '',
})
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)
this.dragNode = null
},
onNodeClick (e, treeNode) {
this.__emit('click', e, treeNode)
},
onNodeDoubleClick (e, treeNode) {
this.__emit('doubleclick', e, treeNode)
},
onNodeSelect (e, treeNode) {
let { _selectedKeys: selectedKeys } = this.$data
const { _keyEntities: keyEntities } = this.$data
const { multiple } = this.$props
const { selected, eventKey } = getOptionProps(treeNode)
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
const selectedNodes = selectedKeys.map(key => {
const entity = keyEntities[key]
if (!entity) return null
return entity.node
}).filter(node => node)
this.setUncontrolledState({ _selectedKeys: selectedKeys })
const eventObj = {
event: 'select',
selected: targetSelected,
node: treeNode,
selectedNodes,
nativeEvent: e,
}
this.__emit('select', selectedKeys, eventObj)
},
onNodeCheck (e, treeNode, checked) {
const { _keyEntities: keyEntities, _checkedKeys: oriCheckedKeys, _halfCheckedKeys: oriHalfCheckedKeys } = this.$data
const { checkStrictly } = this.$props
const { eventKey } = getOptionProps(treeNode)
// Prepare trigger arguments
let checkedObj
const eventObj = {
event: 'check',
node: treeNode,
checked,
nativeEvent: e,
}
if (checkStrictly) {
const checkedKeys = checked ? arrAdd(oriCheckedKeys, eventKey) : arrDel(oriCheckedKeys, eventKey)
const halfCheckedKeys = arrDel(oriHalfCheckedKeys, eventKey)
checkedObj = { checked: checkedKeys, halfChecked: halfCheckedKeys }
eventObj.checkedNodes = checkedKeys
.map(key => keyEntities[key])
.filter(entity => entity)
.map(entity => entity.node)
this.setUncontrolledState({ _checkedKeys: checkedKeys })
} else {
const { checkedKeys, halfCheckedKeys } = conductCheck([eventKey], checked, keyEntities, {
checkedKeys: oriCheckedKeys, halfCheckedKeys: oriHalfCheckedKeys,
})
checkedObj = checkedKeys
// [Legacy] This is used for `rc-tree-select`
eventObj.checkedNodes = []
eventObj.checkedNodesPositions = []
eventObj.halfCheckedKeys = halfCheckedKeys
checkedKeys.forEach((key) => {
const entity = keyEntities[key]
if (!entity) return
const { node, pos } = entity
eventObj.checkedNodes.push(node)
eventObj.checkedNodesPositions.push({ node, pos })
})
this.setUncontrolledState({
_checkedKeys: checkedKeys,
_halfCheckedKeys: halfCheckedKeys,
})
}
this.__emit('check', checkedObj, eventObj)
},
onNodeLoad (treeNode) {
return new Promise((resolve) => {
// We need to get the latest state of loading/loaded keys
this.setState(({ _loadedKeys: loadedKeys = [], _loadingKeys: loadingKeys = [] }) => {
const { loadData } = this.$props
const { eventKey } = getOptionProps(treeNode)
if (!loadData || loadedKeys.indexOf(eventKey) !== -1 || loadingKeys.indexOf(eventKey) !== -1) {
return {}
}
// Process load data
const promise = loadData(treeNode)
promise.then(() => {
const newLoadedKeys = arrAdd(this.$data._loadedKeys, eventKey)
const newLoadingKeys = arrDel(this.$data._loadingKeys, eventKey)
// onLoad should trigger before internal setState to avoid `loadData` trigger twice.
// https://github.com/ant-design/ant-design/issues/12464
const eventObj = {
event: 'load',
node: treeNode,
}
this.__emit('load', eventObj)
this.setUncontrolledState({
_loadedKeys: newLoadedKeys,
})
this.setState({
_loadingKeys: newLoadingKeys,
})
resolve()
})
return {
_loadingKeys: arrAdd(loadingKeys, eventKey),
}
})
})
},
onNodeExpand (e, treeNode) {
let { _expandedKeys: expandedKeys } = this.$data
const { loadData } = this.$props
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: expandedKeys })
this.__emit('expand', expandedKeys, {
node: treeNode,
expanded: targetExpanded,
nativeEvent: e,
})
// Async Load data
if (targetExpanded && loadData) {
const loadPromise = this.onNodeLoad(treeNode)
return loadPromise ? loadPromise.then(() => {
// [Legacy] Refresh logic
this.setUncontrolledState({ _expandedKeys: expandedKeys })
}) : null
}
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 })
},
/**
* 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.replace('_', '') in props) return
needSync = true
newState[name] = state[name]
})
if (needSync) {
this.setState(newState)
}
},
isKeyChecked (key) {
const { _checkedKeys: checkedKeys = [] } = this.$data
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 {
_keyEntities: keyEntities,
_expandedKeys: expandedKeys = [],
_selectedKeys: selectedKeys = [],
_halfCheckedKeys: halfCheckedKeys = [],
_loadedKeys: loadedKeys = [],
_loadingKeys: loadingKeys = [],
_dragOverNodeKey: dragOverNodeKey,
_dropPosition: dropPosition,
} = this.$data
const pos = getPosition(level, index)
const key = child.key || pos
if (!keyEntities[key]) {
warnOnlyTreeNode()
return null
}
return cloneElement(child, {
props: {
key,
eventKey: key,
expanded: expandedKeys.indexOf(key) !== -1,
selected: selectedKeys.indexOf(key) !== -1,
loaded: loadedKeys.indexOf(key) !== -1,
loading: loadingKeys.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 { _treeNode: treeNode } = this.$data
const {
prefixCls, focusable,
showLine, tabIndex = 0,
} = this.$props
const domProps = {}
return (
<ul
{...domProps}
class={classNames(prefixCls, {
[`${prefixCls}-show-line`]: showLine,
})}
role='tree'
unselectable='on'
tabIndex={focusable ? tabIndex : null}
onKeydown={focusable ? this.onKeydown : () => {}}
>
{mapChildren(treeNode, (node, index) => (
this.renderTreeNode(node, index)
))}
</ul>
)
},
}
export { Tree }
export default proxyComponent(Tree)