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 ( ) }, } export { Tree } export default proxyComponent(Tree)